145 lines
6.7 KiB
Python
145 lines
6.7 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
|
||
|
||
from muse_db import connect
|
||
|
||
CREATOR = "read-context"
|
||
EVIDENCE_SCRIPTS = Path(__file__).resolve().parents[2] / "record-run-evidence" / "scripts"
|
||
if str(EVIDENCE_SCRIPTS) not in sys.path:
|
||
sys.path.insert(0, str(EVIDENCE_SCRIPTS))
|
||
|
||
|
||
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 _propose_freeze_lesson(
|
||
*,
|
||
run_id: str,
|
||
work_id,
|
||
target_chapter,
|
||
as_of,
|
||
manifest_sha: str,
|
||
context_sha: str,
|
||
section_count: int,
|
||
) -> dict:
|
||
"""上下文冻结提交后登记效果信号。"""
|
||
|
||
from lesson_registry import propose_lesson_dedup # noqa: WPS433
|
||
|
||
return propose_lesson_dedup(
|
||
kind="win",
|
||
title="写作上下文已冻结",
|
||
detail={
|
||
"skill": "assemble-context",
|
||
"work_id": work_id,
|
||
"target_chapter": target_chapter,
|
||
"as_of": as_of,
|
||
"manifest_sha256": manifest_sha,
|
||
"context_sha256": context_sha,
|
||
"section_count": section_count,
|
||
},
|
||
work_id=work_id if isinstance(work_id, int) else None,
|
||
run_id=str(run_id),
|
||
creator=CREATOR,
|
||
)
|
||
|
||
|
||
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, context) 对唯一:同一冻结重放幂等;同清单不同上下文的冻结各得其所。
|
||
# 表是 append-only,冲突只能回读,不能 UPDATE。
|
||
# WHY:检索清单可跨运行不变而上下文每次不同(预防合同/时间进 SHA),
|
||
# manifest 单键会让新上下文永远落不了库,接受前置检查按 context_sha 必查不到。
|
||
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, context_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 AND context_sha256=%s",
|
||
(manifest_sha, context_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()
|
||
lesson = None
|
||
run_id = ctx.get("runId")
|
||
if run_id:
|
||
lesson = _propose_freeze_lesson(
|
||
run_id=run_id,
|
||
work_id=work_id,
|
||
target_chapter=target_chapter,
|
||
as_of=as_of,
|
||
manifest_sha=manifest_sha,
|
||
context_sha=context_sha,
|
||
section_count=len(sections),
|
||
)
|
||
return {"status": "frozen", "id": row[0], "manifest_sha256": row[1],
|
||
"context_sha256": row[2], "lesson": lesson}
|
||
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))
|