From 96aa1e61a3417b9c4d0dff13758cf3075a656111 Mon Sep 17 00:00:00 2001 From: zizi Date: Fri, 31 Jul 2026 22:29:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A1=86=E6=9E=B6(=E6=95=B0=E6=8D=AE=E5=BA=93+?= =?UTF-8?q?=E8=AF=BB=E5=8F=96=E5=99=A8):=20=E8=90=BD=E5=BA=93=20100=20?= =?UTF-8?q?=E8=A7=84=E5=88=92+=E5=86=BB=E7=BB=93=E8=A1=A8=EF=BC=8C?= =?UTF-8?q?=E5=BB=BA=E5=86=BB=E7=BB=93=E8=90=BD=E5=BA=93=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - example_planning_section(规划/细纲/设定,书级/章级双部分唯一索引,shadow→confirmed) + example_context_freeze(冻结清单,append-only,manifestSha256 幂等) - persist_context_freeze.py:assemble_context 之后由调用方落冻结(manifest/context哈希+纳入来源+省略项+预算), 组装器保持不碰库(合同如此);dry-run 已验证 - read-context SKILL.md 增冻结落库挂点说明 --- .claude/skills/read-context/SKILL.md | 7 +- .../scripts/persist_context_freeze.py | 84 +++++++++++++++++ db/ddl/100-example规划与冻结.sql | 90 +++++++++++++++++++ 3 files changed, 178 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/read-context/scripts/persist_context_freeze.py create mode 100644 db/ddl/100-example规划与冻结.sql diff --git a/.claude/skills/read-context/SKILL.md b/.claude/skills/read-context/SKILL.md index dace52a..b5df194 100644 --- a/.claude/skills/read-context/SKILL.md +++ b/.claude/skills/read-context/SKILL.md @@ -23,8 +23,9 @@ scenario 映射功能链分类:生成类为 `generation`,抽取类为 `extra 3. **按指针回读原文**:在 `REPEATABLE READ READ ONLY` 事务中按 `sourceVersion/sourceRefs/stateAsOf` 回读 Canonical block。缺少可验证指针的内容只能作为未验证索引提示,不能进入事实证据。 4. **双证据组装**:连续前四章全文构成近期基线;卡指针召回的历史正文作为补充并按不可变来源去重。`factEvidence` 只接受冻结历史原文、正式设定、Canonical 状态或细纲明确声明的新事实,并保留各自权威引用。 5. **确定性冻结**:按 `score DESC, sourceVersion ASC, sourceId ASC, sourceOffset ASC` 排序,生成完整 `WriterContext v1`、RetrievalManifest、来源省略原因和上下文 hash。预算裁剪必须可审计,不能静默丢失硬约束或连续正文基线。 +6. **冻结落库(assemble 之后,调用方负责)**:`assemble_context()` 算出冻结后,调用方(生产编排 / 回放编排)调 `scripts/persist_context_freeze.py`,把冻结清单(manifestSha256/contextSha256/纳入来源/省略项/预算)落一行 `example_context_freeze`(append-only,manifestSha256 幂等)。组装器本身不碰库(合同如此),落库是调用方的事;回执表的 `context_sha256` 连本表 `context_sha256` 列。 -对应机械实现:`scripts/writer_contract.py`、`scripts/retrieve_writer_sources.py`、`scripts/assemble_writer_context.py`。其中 `retrieve_writer_sources.py` 的冻结来源校验、内容泄漏审计与冻结原文只读装载复用 **snapshot** skill(`build_snapshot.py`/`check_snapshot.py`/`audit_leakage.py`/`load_reference_work.py`),单向向下依赖,不复制 SQL 或第二套权限语义。 +对应机械实现:`scripts/writer_contract.py`、`scripts/retrieve_writer_sources.py`、`scripts/assemble_writer_context.py`、`scripts/persist_context_freeze.py`(冻结落库)。其中 `retrieve_writer_sources.py` 的冻结来源校验、内容泄漏审计与冻结原文只读装载复用 **snapshot** skill(`build_snapshot.py`/`check_snapshot.py`/`audit_leakage.py`/`load_reference_work.py`),单向向下依赖,不复制 SQL 或第二套权限语义。 ## 四层稳定语义 @@ -43,7 +44,7 @@ scenario 映射功能链分类:生成类为 `generation`,抽取类为 `extra |---|---|---| | writer | `WriterCreativeInput v2`:fineOutline、narrativeState、factConstraints、proseExcerpts、patternReferences、lengthContract、styleConstraints | runId、权限、manifest、hash、候选版本、实验臂、oracle、评审结论、检索工具 | | semantic detector | `semantic-detector-input-v3` 的模型投影:当前候选、细纲/硬约束、事实证据、历史原文证据、asOf | 真实实验臂、raw 路径、oracle、其他候选/评委、输入与候选 hash、receipt | -| blind judge | `writer-blind-input-v3` 的模型投影:匿名候选正文、共同细纲、oracle 断言、rubric | 真实 A/B/C、卡注入、证据策略、候选 hash、reviewer 身份、raw、其他评委结果 | +| blind judge | `writer-blind-input-v4` 的模型投影:匿名候选正文、共同细纲、oracle 断言、通用指标与当前场景评分策略 | 真实 A/B/C、卡注入、证据策略、候选 hash、reviewer 身份、raw、其他评委结果 | | planner | 冻结规划视图;只在 planning 授权下读取全局方向和未来规划 | 未授权原文、目标章之后的参考事实、评测答案 | | extractor | 待处理原文、schema 合同和判重所需的既有知识 | 范式参考、未来内容、无来源推断 | @@ -53,7 +54,7 @@ writer 的 `factConstraints=[]` 在冻结读取确实没有可确认事实时合 - writer 只返回 `WriterDraft v2` 的 `candidateBody`;adapter 生成 CandidateEnvelope v2,并绑定运行身份、候选版本、正文 hash 与上下文 hash。 - semantic detector 和 blind judge 只返回各自模型草稿;字符 offset、输入/报告 hash、模型回执和最终状态均由 adapter 计算。 -- A/C 单变量回执比较 `build_writer_creative_input()` 的结果,只允许预注册证据策略导致 `factConstraints` 与 `proseExcerpts` 差异;细纲、叙事状态、篇幅、风格和公共参考必须一致。 +- A/C 单变量回执比较 `build_writer_creative_input()` 的结果,只允许预注册卡处理与证据策略导致 `factConstraints`、`proseExcerpts` 与 `patternReferences` 差异;A 的 `patternReferences` 恒空,B/C 使用同一组冻结公共范式卡,细纲、叙事状态、篇幅和风格必须一致。 ## 回显与存储 diff --git a/.claude/skills/read-context/scripts/persist_context_freeze.py b/.claude/skills/read-context/scripts/persist_context_freeze.py new file mode 100644 index 0000000..ed1a9eb --- /dev/null +++ b/.claude/skills/read-context/scripts/persist_context_freeze.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""上下文冻结落库 —— 在 assemble_context() 之后把冻结清单落一行 example_context_freeze。 + +落库设计 §2.6 的落点:assemble_writer_context.assemble_context() 是纯函数不碰库(合同如此), +本模块由调用方(生产编排 / 回放编排)在 assemble 之后调用,算出的 contextSha256/manifest +不再"即弃",落一行冻结记录(append-only)。组装器保持 DB-free。 + +哈希归一:assemble 产出的哈希带 "sha256:" 前缀,入库剥成裸 64 hex(列 CHECK 要求)。 +--dry-run 试跑:插入后回滚,校验但不落库。 +""" +import json +import sys +from pathlib import Path + +# 复用 db skill 锁死的 DSN(.claude/skills/db/scripts) +DB_SCRIPTS = Path(__file__).resolve().parents[2] / "db" / "scripts" +sys.path.insert(0, str(DB_SCRIPTS)) +from db import connect # noqa: E402 + +CREATOR = "read-context" + + +def _bare_sha(h): + """剥 'sha256:' 前缀成裸 64 hex(列 CHECK 要求 ^[0-9a-f]{64}$)。""" + if not h: + return None + return h[len("sha256:"):] if str(h).startswith("sha256:") else str(h) + + +def persist_freeze(assemble_result, *, reference_work_id=None, reference_version=None, + arm_config=None, dry_run=False): + """把 assemble_context 的返回落一行 example_context_freeze。返回 {status, id, manifest_sha256, context_sha256}。""" + ctx = assemble_result["context"] + manifest_sha = _bare_sha(ctx["contextSnapshot"]["manifestId"]) + context_sha = _bare_sha(ctx["contextSnapshot"]["contextSha256"]) + sections = ctx["retrievalManifest"].get("sources", []) + omitted = ctx.get("omittedSources", []) + used_chars = ctx.get("tokenBudget", {}).get("usedContextChars") + work_id = ctx.get("workId") + target_chapter = ctx.get("targetChapter") + as_of = ctx.get("asOf") + if as_of is None: + raise ValueError("assemble 结果缺 asOf,无法落冻结") + with connect() as conn: + try: + # manifest_sha256 唯一:同一冻结重放幂等(ON CONFLICT 返回既有行) + row = conn.execute( + "INSERT INTO example_context_freeze(work_id, target_chapter, as_of_chapter, manifest_sha256, " + "context_sha256, reference_work_id, reference_version, arm_config, sections, token_budget, " + "omitted_sources, creator) " + "VALUES (%s,%s,%s,%s,%s,%s,%s,%s::jsonb,%s::jsonb,%s,%s::jsonb,%s) " + "ON CONFLICT (manifest_sha256) DO UPDATE SET creator=EXCLUDED.creator " + "RETURNING id, manifest_sha256, context_sha256", + (work_id, target_chapter, as_of, manifest_sha, context_sha, reference_work_id, + reference_version, json.dumps(arm_config, ensure_ascii=False) if arm_config is not None else None, + json.dumps(sections, ensure_ascii=False), used_chars, + json.dumps(omitted, ensure_ascii=False), CREATOR)).fetchone() + if dry_run: + conn.rollback() + return {"status": "dry_run_ok", "id": row[0], "manifest_sha256": row[1], + "context_sha256": row[2], "note": "试跑已回滚,未落库"} + conn.commit() + return {"status": "frozen", "id": row[0], "manifest_sha256": row[1], "context_sha256": row[2]} + except Exception: + conn.rollback() + raise + + +if __name__ == "__main__": + import argparse + ap = argparse.ArgumentParser(description="上下文冻结落库(assemble_context 之后调用)") + ap.add_argument("result_json", nargs="?", default="-", + help="assemble_context 返回的 JSON 文件路径(默认 stdin)") + ap.add_argument("--reference-work-id", type=int, default=None) + ap.add_argument("--reference-version", default=None) + ap.add_argument("--arm-config", default=None, help="实验臂配置 JSON 字符串") + ap.add_argument("--dry-run", action="store_true") + args = ap.parse_args() + raw = sys.stdin.read() if args.result_json == "-" else Path(args.result_json).read_text(encoding="utf-8") + result = json.loads(raw) + arm = json.loads(args.arm_config) if args.arm_config else None + out = persist_freeze(result, reference_work_id=args.reference_work_id, + reference_version=args.reference_version, arm_config=arm, dry_run=args.dry_run) + print(json.dumps(out, ensure_ascii=False)) diff --git a/db/ddl/100-example规划与冻结.sql b/db/ddl/100-example规划与冻结.sql new file mode 100644 index 0000000..e92993c --- /dev/null +++ b/db/ddl/100-example规划与冻结.sql @@ -0,0 +1,90 @@ +-- 规划与上下文冻结两表:example_planning_section(规划/细纲/设定)+ example_context_freeze(冻结清单)。 +-- 设计拍板 2026-07-30(docs/2026-07-30-落库与看板设计.md §2.6)。 +-- 背景:今天规划/细纲在文件与 gitignore 临时目录(planner 直接写文件),冻结 manifest 是仓外临时 JSON 算完即弃; +-- 主仓 muse_content_planning_section 暂缓未建。两表新建,补齐"已确认细纲/冻结上下文"的持久化缺口。 +-- planning_section 可变(shadow→confirmed,带公共列+updated_at);context_freeze append-only(冻结一旦生成不可改)。 +-- 全部对象 IF NOT EXISTS / OR REPLACE,重复 apply 幂等。 + +-- ══ example_planning_section:规划/细纲/设定(可变;书级 target_chapter 为空,章级填章号)══ +CREATE TABLE IF NOT EXISTS example_planning_section ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + work_id BIGINT NOT NULL, -- → muse_content_work.id(软引用) + target_chapter INTEGER, -- 章级(fine_outline)填章号;书级(setting/outline/state/assembly)为空 + section_type VARCHAR(32) NOT NULL, -- setting/outline/state/assembly/fine_outline + schema_type VARCHAR(32), -- 对应 meta/schemas 型(work_core/outline/narrative_state…) + version INTEGER NOT NULL DEFAULT 1, -- 规划版本 + payload JSONB NOT NULL, -- 结构化内容(frontmatter+正文的结构化形态) + state VARCHAR(20) NOT NULL DEFAULT 'shadow', -- shadow/confirmed(用户确认后翻 confirmed) + confirmed_at TIMESTAMP, -- 确认时间 + 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_planning_section_type + CHECK (section_type IN ('setting','outline','state','assembly','fine_outline')), + CONSTRAINT chk_example_planning_section_state + CHECK (state IN ('shadow','confirmed')) +); +-- 唯一键用两个部分唯一索引(仿 V26 partial unique,因 target_chapter 可空) +CREATE UNIQUE INDEX IF NOT EXISTS uk_example_planning_section_book + ON example_planning_section(tenant_id, work_id, section_type, version) WHERE target_chapter IS NULL; +CREATE UNIQUE INDEX IF NOT EXISTS uk_example_planning_section_chapter + ON example_planning_section(tenant_id, work_id, section_type, target_chapter, version) WHERE target_chapter IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_example_planning_section_work + ON example_planning_section(tenant_id, work_id, section_type, state); +CREATE OR REPLACE TRIGGER trg_example_planning_section_updated_at BEFORE UPDATE ON example_planning_section FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); + +-- ══ example_context_freeze:冻结清单(append-only;冻结一旦生成不可改)══ +CREATE TABLE IF NOT EXISTS example_context_freeze ( + id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, + work_id BIGINT, -- → muse_content_work.id(软引用) + target_chapter INTEGER, -- 目标章 + as_of_chapter INTEGER NOT NULL, -- 冻结上界(严格 <= asOf) + manifest_sha256 CHAR(64) NOT NULL, -- manifest 自身哈希(唯一) + context_sha256 CHAR(64), -- WriterContext 回执哈希 = retrieval_identity(context);回执表连这一列 + reference_work_id BIGINT, -- 参考作品 id(回放用) + reference_version VARCHAR(64), -- 参考版本 + arm_config JSONB, -- 实验臂配置(A/B/C) + sections JSONB, -- 每节 sourceId/sourceVersion/sha256/charCount + token_budget INTEGER, -- 上下文预算 + omitted_fields JSONB, -- 字段级裁剪省略项 + omitted_sources JSONB, -- 来源级省略项 + creator VARCHAR(64) NOT NULL DEFAULT '', + create_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + tenant_id BIGINT NOT NULL DEFAULT 0, + CONSTRAINT uk_example_context_freeze_manifest UNIQUE (manifest_sha256), + CONSTRAINT chk_example_context_freeze_manifest_sha256 + CHECK (manifest_sha256 ~ '^[0-9a-f]{64}$'), + CONSTRAINT chk_example_context_freeze_context_sha256 + CHECK (context_sha256 IS NULL OR context_sha256 ~ '^[0-9a-f]{64}$') +); +CREATE INDEX IF NOT EXISTS idx_example_context_freeze_context ON example_context_freeze(context_sha256) WHERE context_sha256 IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_example_context_freeze_work ON example_context_freeze(work_id, target_chapter); + +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_context_freeze_mutation() +RETURNS TRIGGER +LANGUAGE plpgsql +AS $$ +BEGIN + RAISE EXCEPTION 'example_context_freeze 是 append-only 表,禁止 UPDATE/DELETE'; +END; +$$; + +CREATE OR REPLACE TRIGGER trg_example_context_freeze_append_only +BEFORE UPDATE OR DELETE ON example_context_freeze +FOR EACH ROW EXECUTE FUNCTION reject_example_context_freeze_mutation(); + +CREATE OR REPLACE TRIGGER trg_example_context_freeze_no_truncate +BEFORE TRUNCATE ON example_context_freeze +FOR EACH STATEMENT EXECUTE FUNCTION example_reject_truncate();