muse-agent-example/db/ddl/99-example候选与决策.sql
zizi aa82445ec7 框架(数据库+确认通道): 落库 99 候选+决策,建正文 Canonical 写入层,confirm 改 DB 语义
- example_candidate(七态状态机,DB 级评测候选禁接受硬拒 05§8.4) + example_user_decision(append-only+理据)
- write_canonical.py:accept/discard 单事务(写正文块CAS revision+1→来源归因落块→命令幂等→决策归档→翻候选态),
  任一失败整体回滚;dry-run + eval/draft 守卫已验证;复用 db skill DSN
- confirm/SKILL.md 从 git commit 语义改为写库语义(正文走 write_canonical;知识卡走双轨;规划暂 git)
2026-07-31 11:02:16 +08:00

112 lines
6.6 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_candidate(待审候选 Shadow)+ example_user_decision(用户决策归档)。
-- 设计拍板 2026-07-30(docs/2026-07-30-落库与看板设计.md §2.4/§2.5,含评审加固)。
-- 分工:candidate 登记 AI 待审候选(可变,状态机流转,字段平移自 CandidateEnvelope v2);
-- user_decision 记录用户 accept/merge/discard + 理据(append-only,= 父仓 Candidate Decision Archive
-- 的单用户落地,不是 ADR-018 删的 Decision Envelope;不缓存裁决,接受时实时校验 05 §5)。
-- 候选正文单存 candidate_body,不重复塞 raw 表;候选表整体按 raw 级访问控制。
-- 全部对象 IF NOT EXISTS / OR REPLACE,重复 apply 幂等。
-- ══ example_candidate:待审候选(可变,状态机;05 §3)══
CREATE TABLE IF NOT EXISTS example_candidate (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
work_id BIGINT NOT NULL, -- → muse_content_work.id(软引用)
target_chapter INTEGER NOT NULL, -- 目标章序
run_id VARCHAR(64), -- → example_run.run_id(merge 候选=复检重跑那次运行)
attempt INTEGER, -- 尝试号
run_type VARCHAR(20) NOT NULL, -- production/eval/diagnostic;评测候选不得成正文(05 §8.4)
candidate_version VARCHAR(32) NOT NULL, -- 候选版本
candidate_sha256 CHAR(64) NOT NULL, -- 候选正文哈希(裸 64 hex)
candidate_body TEXT, -- 候选正文(看板待审视图直接读;按 raw 级访问控制)
context_sha256 CHAR(64), -- → example_context_freeze.context_sha256(冻结上下文)
quality_policy_version VARCHAR(32), -- 质量政策版本
mode VARCHAR(20), -- continuation/rewrite/expansion/polish
source_role VARCHAR(20), -- 产生角色(writer/planner)
state VARCHAR(20) NOT NULL DEFAULT 'draft', -- draft/checking/passed/rejected/accepted/discarded/archived
acceptance_eligible BOOLEAN, -- 由 run_type 派生的缓存位(以 run_type 为准)
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 uk_example_candidate UNIQUE (tenant_id, work_id, target_chapter, candidate_version),
CONSTRAINT chk_example_candidate_run_type
CHECK (run_type IN ('production','eval','diagnostic')),
CONSTRAINT chk_example_candidate_state
CHECK (state IN ('draft','checking','passed','rejected','accepted','discarded','archived')),
CONSTRAINT chk_example_candidate_mode
CHECK (mode IS NULL OR mode IN ('continuation','rewrite','expansion','polish')),
CONSTRAINT chk_example_candidate_sha256
CHECK (candidate_sha256 ~ '^[0-9a-f]{64}$'),
CONSTRAINT chk_example_candidate_context_sha256
CHECK (context_sha256 IS NULL OR context_sha256 ~ '^[0-9a-f]{64}$')
);
CREATE INDEX IF NOT EXISTS idx_example_candidate_work_state ON example_candidate(tenant_id, work_id, state);
CREATE INDEX IF NOT EXISTS idx_example_candidate_run_type ON example_candidate(tenant_id, run_type);
CREATE OR REPLACE TRIGGER trg_example_candidate_updated_at BEFORE UPDATE ON example_candidate FOR EACH ROW EXECUTE FUNCTION update_updated_at_column();
-- 机械隔离(05 §8.4):评测/诊断候选不得被接受为正式正文——DB 级硬拒,不靠调用方自觉
CREATE OR REPLACE FUNCTION reject_non_production_accept()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.state = 'accepted' AND NEW.run_type <> 'production' THEN
RAISE EXCEPTION '评测/诊断候选(run_type=%)不得接受为正式正文(05 §8.4 机械隔离)', NEW.run_type;
END IF;
RETURN NEW;
END;
$$;
CREATE OR REPLACE TRIGGER trg_example_candidate_no_eval_accept
BEFORE INSERT OR UPDATE ON example_candidate
FOR EACH ROW EXECUTE FUNCTION reject_non_production_accept();
-- ══ example_user_decision:用户决策归档(append-only;05 §4)══
CREATE TABLE IF NOT EXISTS example_user_decision (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
candidate_id BIGINT, -- → example_candidate.id(软引用)
work_id BIGINT, -- 决策当时的作品(快照,append-only 不失步)
target_chapter INTEGER, -- 决策当时的章(快照)
decision VARCHAR(20) NOT NULL, -- accept/merge/discard
rationale TEXT, -- 理据(为什么这么定)
basis_ref VARCHAR(256), -- 依据(如 大纲@日期 / 细纲版本)
decided_by VARCHAR(64), -- 决策人(单用户=创始人)
expected_revision INTEGER, -- 接受时比对的正文 revision(冲突报 REVISION_CONFLICT)
command_id VARCHAR(120), -- 幂等键,直连 muse_content_command_log.command_id
canonical_block_id BIGINT, -- → muse_content_block.id(冗余正向指针;来源权威在正文块归因,§2.9)
creator VARCHAR(64) NOT NULL DEFAULT '',
create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
tenant_id BIGINT NOT NULL DEFAULT 0,
CONSTRAINT chk_example_user_decision_kind
CHECK (decision IN ('accept','merge','discard'))
);
CREATE INDEX IF NOT EXISTS idx_example_user_decision_work ON example_user_decision(tenant_id, work_id);
CREATE INDEX IF NOT EXISTS idx_example_user_decision_candidate ON example_user_decision(candidate_id) WHERE candidate_id 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_user_decision_mutation()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
BEGIN
RAISE EXCEPTION 'example_user_decision 是 append-only 表,禁止 UPDATE/DELETE';
END;
$$;
CREATE OR REPLACE TRIGGER trg_example_user_decision_append_only
BEFORE UPDATE OR DELETE ON example_user_decision
FOR EACH ROW EXECUTE FUNCTION reject_example_user_decision_mutation();
CREATE OR REPLACE TRIGGER trg_example_user_decision_no_truncate
BEFORE TRUNCATE ON example_user_decision
FOR EACH STATEMENT EXECUTE FUNCTION example_reject_truncate();