zizi b0bc7a8745 框架: 技能按动作-对象重组 + 先审后入创作闭环
一、技能重组(动作-对象命名)
- 旧目录 clean/confirm/continuation/db/detect/embed/… 重组为
  clean-book-text/decide-candidate/write-next-chapter/access-database/
  check-content-consistency/embed-knowledge/…(git 识别为 rename,内容保持)
- agents/*.md、AGENTS.md/CLAUDE.md 收编、example_skill 登记表同步新名

二、先审后入创作闭环(本次核心)
正文接受从"机械门一过就写正典"改为"机械门+语义审查双通过+用户批准+单事务原子提交",
DB 级兜底,编排层跳步即被硬拒。
- candidate_cas.py + example_candidate_cas(109):持久化 CAS 状态链
- fact_delta.py + example_fact_delta/example_fact_ledger(106):结构化事实增量,
  模型只提六型闭集增量+正文证据引文,仅用户批准的增量随正文同事务入账本
- projection_registry.py + example_projection_run(107):投影登记与恢复
- acceptance_state.py:接受前置实时状态重读
- lesson_registry.py + example_lesson(108):经验升格链,禁止自动升格
- DDL 105:example_candidate 增 semantic_status/semantic_report_sha256
- write_canonical.accept:语义兜底+同事务合并增量+登记投影;
  run_writer_pipeline/persist_writer_run/run_writer_semantic_detector/step2 接入全链
- claude_runtime:兼容新 CLI modelUsage 信息字段

三、审查修复(独立子代理四维审查后)
- 事实增量 propose→approve 翻态正道,不撞唯一键
- 冻结配置探针重刷(CLI 2.1.211→2.1.231 漂移),profileSha256/adapterVersion 再登记
- 可视化合同悬空路径/五六空间矛盾、 SoT 旧技能名漂移、行尾空白清理

测试:离线 65 套 + 真实库集成 5 套(CAS/接受故障注入/事实增量/投影/经验升格)+ 回放 79 项全绿。
创作内容(docs/design、生成正文 artifacts)按"框架与创作分开"未入本提交。
2026-08-14 10:24:08 +08:00

97 lines
5.0 KiB
Python
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.

#!/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
# 复用 access-database Skill 锁死的 DSN
DB_SCRIPTS = Path(__file__).resolve().parents[2] / "access-database" / "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,无法落冻结")
# 授权快照随冻结落库:接受前置检查(acceptance_state)重读它做实时比对,
# 不再信任编排层内存里的快照副本。
authorization = ctx.get("authorizationSnapshot")
auth_json = json.dumps(authorization, ensure_ascii=False) if isinstance(authorization, dict) else None
with connect() as conn:
try:
# manifest_sha256 唯一:同一冻结重放幂等。表是 append-only,冲突只能回读,不能 UPDATE。
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, authorization_snapshot, creator) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s::jsonb,%s::jsonb,%s,%s::jsonb,%s::jsonb,%s) "
"ON CONFLICT (manifest_sha256) DO NOTHING "
"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), auth_json, CREATOR)).fetchone()
if not row:
row = conn.execute(
"SELECT id,manifest_sha256,context_sha256 FROM example_context_freeze "
"WHERE manifest_sha256=%s",
(manifest_sha,),
).fetchone()
if not row:
raise RuntimeError("冻结幂等回读失败")
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))