一、技能重组(动作-对象命名) - 旧目录 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)按"框架与创作分开"未入本提交。
97 lines
5.0 KiB
Python
97 lines
5.0 KiB
Python
#!/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))
|