muse-agent-example/db/ddl/98-example运行与回执与质量结果.sql
zizi bb1dcc1a58 框架(数据库): 落库 97/98 运行级账本四表并加固,db skill 增参数化通道与只读 query
- 新增 example_llm_call(97)、example_run/example_run_receipt/example_quality_result(98)
- 加固:全对象幂等(IF NOT EXISTS/OR REPLACE)、append-only 表加 TRUNCATE 防护、
  运行终态闭合约束、token/成本非负与 model_match 一致性 CHECK(已 DROP 空表重 apply 验证)
- db.py:新增 execparams 参数化写(%s 服务端绑定+语句白名单+stdin 大对象);
  query 改只读连接(拒 INSERT...RETURNING);tables 用 sql.Identifier 安全拼接;connect() 统一入口
- 表映射登记 97/98、96 标不启用(仅本轮改动,未含他处未提交内容)
2026-07-31 00:30:54 +08:00

181 lines
11 KiB
PL/PgSQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- 运行级账本三表:example_run(运行注册)+ example_run_receipt(运行身份回执)+ example_quality_result(质量评判)。
-- 设计拍板 2026-07-30(docs/2026-07-30-落库与看板设计.md §2.2,已吸收四路评审 + Codex 加固)。
-- 三表分工:run 记"这是哪一次运行、哪个作品、谁触发、终态"(可变,运行时更新终态);
-- receipt 记"这次运行的每一段身份与哈希链"(append-only,字段平移自 runtime ExecutionReceipt ⊕ CAS 身份);
-- quality_result 记"这次运行判了什么"(append-only,检测/评分/审核/实验,绑候选哈希、带量表版本)。
-- 公共列分界(落库设计 §1):run 是可变注册表,带公共列六件套 + 软删;
-- receipt/quality_result 是 append-only 账本,只带归属列(creator/create_time/tenant_id),
-- 不带 updater/update_time/deleted 三死列(禁改删时永不写入)。
-- 哈希列按落库设计 §1 归一规则存裸 64 位小写 hex(剥 sha256: 前缀)。
-- 注意:列名用 trigger_source 而非 trigger——trigger 是 SQL 保留字。
-- 全部对象用 IF NOT EXISTS / OR REPLACE,重复 apply 幂等。
-- ══ example_run:运行注册(一次运行一行,所有运行级子表挂它;回应"run_id 无主")══
CREATE TABLE IF NOT EXISTS example_run (
run_id VARCHAR(64) PRIMARY KEY, -- 运行标识(自然键,全局唯一,子表软引用它)
work_id BIGINT, -- → muse_content_work.id(软引用;回答"哪个作品",08 §3 硬要求)
target_chapter INTEGER, -- 目标章序(可空:非章级运行,如拆书/诊断)
trigger_source VARCHAR(32) NOT NULL DEFAULT 'user', -- user/replay_eval/diagnostic(触发来源)
trigger_detail JSONB, -- 触发上下文(用户意图摘要等)
started_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
finished_at TIMESTAMP,
terminal_state VARCHAR(20) NOT NULL DEFAULT 'running', -- running/completed/failed
creator VARCHAR(64) NOT NULL DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updater VARCHAR(64) NOT NULL DEFAULT '',
update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
deleted BOOLEAN NOT NULL DEFAULT FALSE,
tenant_id BIGINT NOT NULL DEFAULT 0,
CONSTRAINT chk_example_run_trigger CHECK (trigger_source IN ('user','replay_eval','diagnostic')),
CONSTRAINT chk_example_run_terminal CHECK (terminal_state IN ('running','completed','failed')),
CONSTRAINT chk_example_run_finish_order CHECK (finished_at IS NULL OR finished_at >= started_at),
-- 终态闭合:终态必须有结束时间;running 不得带结束时间(看板不显示互相矛盾的运行事实)
CONSTRAINT chk_example_run_terminal_needs_finish
CHECK (terminal_state NOT IN ('completed','failed') OR finished_at IS NOT NULL),
CONSTRAINT chk_example_run_running_no_finish
CHECK (terminal_state <> 'running' OR finished_at IS NULL)
);
CREATE INDEX IF NOT EXISTS idx_example_run_work ON example_run(tenant_id, work_id, started_at DESC);
CREATE OR REPLACE TRIGGER trg_example_run_updated_at BEFORE UPDATE ON example_run FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 终态不得回退到 running(completed/failed 之间可修正,但不能退回进行中)
CREATE OR REPLACE FUNCTION reject_example_run_terminal_rollback()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF OLD.terminal_state IN ('completed','failed') AND NEW.terminal_state NOT IN ('completed','failed') THEN
RAISE EXCEPTION '运行已到终态 %,不得回退到 %', OLD.terminal_state, NEW.terminal_state;
END IF;
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER trg_example_run_no_terminal_rollback
BEFORE UPDATE ON example_run
FOR EACH ROW EXECUTE FUNCTION reject_example_run_terminal_rollback();
-- ══ example_run_receipt:运行身份回执(append-only;05 §3 绑定键 run_id+attempt+candidate_version+candidate_sha256)══
-- 身份沿用 CAS:revision 是"每个样本内"的单调号(不是按整次运行),故唯一键必须含 sample_id。
-- candidate_version/candidate_sha256 是数据列兼到候选表的连接键,不是回执自身身份;回执↔候选是四列 1:N。
-- revision/attempt/loop_seq 单调性由 CAS 写路径保证;DB 侧只做非负下界约束(完整链校验暂缓,见落库设计 §2.2)。
CREATE TABLE IF NOT EXISTS example_run_receipt (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
run_id VARCHAR(64) NOT NULL, -- → example_run.run_id(软引用)
sample_id VARCHAR(64) NOT NULL, -- CAS 样本标识(生产侧也造运行内标识,不得空)
revision INTEGER NOT NULL, -- CAS 样本内单调号(>= 1)
arm VARCHAR(32), -- 实验臂
attempt INTEGER NOT NULL DEFAULT 1, -- 失败重试次数(与补证/重写的 loop_seq 分工)
adapter_role VARCHAR(32), -- writer/planner/detector/judge…
stage_kind VARCHAR(20) NOT NULL DEFAULT 'generation', -- generation/detection/supplement/rewrite/accept
loop_seq INTEGER NOT NULL DEFAULT 0, -- 补证/重写循环序号(05 §2 的 ≤3/≤2 合同靠它计数)
candidate_version VARCHAR(32), -- 候选版本(候选连接键)
candidate_sha256 CHAR(64), -- 正文哈希(裸 64 hex;候选连接键,非回执身份)
context_sha256 CHAR(64), -- → example_context_freeze.context_sha256(冻结上下文,注意不是 manifest_sha256)
previous_state_sha256 CHAR(64), -- CAS 哈希链上一环
requested_model_id VARCHAR(64),
actual_model_id VARCHAR(64),
model_match BOOLEAN,
effort VARCHAR(16),
total_cost_usd NUMERIC(14,8),
usage JSONB, -- token 用量
stop_reason VARCHAR(32),
terminal_reason VARCHAR(32),
is_error BOOLEAN NOT NULL DEFAULT FALSE,
safe_summary JSONB, -- 检测/质量结论 + 失败类别的安全摘要(完整 prompt/response 落 raw)
result_sha256 CHAR(64),
raw_content_id BIGINT, -- → example_raw_content.id
creator VARCHAR(64) NOT NULL DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
tenant_id BIGINT NOT NULL DEFAULT 0,
CONSTRAINT uk_example_run_receipt UNIQUE (tenant_id, run_id, sample_id, revision),
CONSTRAINT chk_example_run_receipt_counters
CHECK (revision >= 1 AND attempt >= 1 AND loop_seq >= 0),
CONSTRAINT chk_example_run_receipt_stage
CHECK (stage_kind IN ('generation','detection','supplement','rewrite','accept')),
CONSTRAINT chk_example_run_receipt_candidate_sha256
CHECK (candidate_sha256 IS NULL OR candidate_sha256 ~ '^[0-9a-f]{64}$'),
CONSTRAINT chk_example_run_receipt_context_sha256
CHECK (context_sha256 IS NULL OR context_sha256 ~ '^[0-9a-f]{64}$'),
CONSTRAINT chk_example_run_receipt_prev_sha256
CHECK (previous_state_sha256 IS NULL OR previous_state_sha256 ~ '^[0-9a-f]{64}$'),
CONSTRAINT chk_example_run_receipt_result_sha256
CHECK (result_sha256 IS NULL OR result_sha256 ~ '^[0-9a-f]{64}$')
);
CREATE INDEX IF NOT EXISTS idx_example_run_receipt_run ON example_run_receipt(tenant_id, run_id);
CREATE INDEX IF NOT EXISTS idx_example_run_receipt_candidate ON example_run_receipt(tenant_id, candidate_sha256) WHERE candidate_sha256 IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_example_run_receipt_context ON example_run_receipt(context_sha256) WHERE context_sha256 IS NOT NULL;
CREATE OR REPLACE FUNCTION example_reject_truncate()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION '% 是 append-only 表,禁止 TRUNCATE', TG_TABLE_NAME;
END;
$$;
CREATE OR REPLACE FUNCTION reject_example_run_receipt_mutation()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'example_run_receipt 是 append-only 表,禁止 UPDATE/DELETE';
END;
$$;
CREATE OR REPLACE TRIGGER trg_example_run_receipt_append_only
BEFORE UPDATE OR DELETE ON example_run_receipt
FOR EACH ROW EXECUTE FUNCTION reject_example_run_receipt_mutation();
CREATE OR REPLACE TRIGGER trg_example_run_receipt_no_truncate
BEFORE TRUNCATE ON example_run_receipt
FOR EACH STATEMENT EXECUTE FUNCTION example_reject_truncate();
-- ══ example_quality_result:质量评判账本(append-only;有评判就记录、就可见,06 §3/§5)══
-- 本期:每次检测/评分/审核/实验落一条,看板运行详情逐条可见;跨运行聚合视图二期(06 §9)。
CREATE TABLE IF NOT EXISTS example_quality_result (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
run_id VARCHAR(64) NOT NULL, -- → example_run.run_id(评判必属一次运行)
receipt_id BIGINT, -- → example_run_receipt.id(软引用)
candidate_sha256 CHAR(64), -- 绑候选(06 §5;书级评判可空)
judge_kind VARCHAR(20) NOT NULL, -- detection/scoring/review/experiment
dimension VARCHAR(32), -- 评分维度(评分类用,检测类空)
scale_version VARCHAR(32), -- 量表/质量政策版本(06 §3 量表版本化)
score NUMERIC, -- 分值(非评分类空)
conclusion VARCHAR(20), -- pass/fail/revise…
failure_class VARCHAR(32), -- 失败类别
detail JSONB, -- 审核的问题/修复/复验;实验的基线/假设/干预/预期/结论
raw_content_id BIGINT, -- → example_raw_content.id
creator VARCHAR(64) NOT NULL DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
tenant_id BIGINT NOT NULL DEFAULT 0,
CONSTRAINT chk_example_quality_result_kind
CHECK (judge_kind IN ('detection','scoring','review','experiment')),
CONSTRAINT chk_example_quality_result_candidate_sha256
CHECK (candidate_sha256 IS NULL OR candidate_sha256 ~ '^[0-9a-f]{64}$')
);
-- 幂等唯一键用表达式索引(dimension/candidate_sha256 可空,UNIQUE 约束不能含表达式,故用唯一索引 + COALESCE)
CREATE UNIQUE INDEX IF NOT EXISTS uk_example_quality_result
ON example_quality_result(tenant_id, run_id, judge_kind, COALESCE(dimension, ''), COALESCE(candidate_sha256, ''));
CREATE INDEX IF NOT EXISTS idx_example_quality_result_candidate ON example_quality_result(candidate_sha256) WHERE candidate_sha256 IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_example_quality_result_run ON example_quality_result(run_id);
CREATE INDEX IF NOT EXISTS idx_example_quality_result_kind ON example_quality_result(judge_kind);
CREATE OR REPLACE FUNCTION reject_example_quality_result_mutation()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'example_quality_result 是 append-only 表,禁止 UPDATE/DELETE';
END;
$$;
CREATE OR REPLACE TRIGGER trg_example_quality_result_append_only
BEFORE UPDATE OR DELETE ON example_quality_result
FOR EACH ROW EXECUTE FUNCTION reject_example_quality_result_mutation();
CREATE OR REPLACE TRIGGER trg_example_quality_result_no_truncate
BEFORE TRUNCATE ON example_quality_result
FOR EACH STATEMENT EXECUTE FUNCTION example_reject_truncate();