- 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)
112 lines
6.6 KiB
PL/PgSQL
112 lines
6.6 KiB
PL/PgSQL
-- 候选与决策两表: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();
|