#!/usr/bin/env python3 """真写一章 · 阶段二:走完整生产链写下一章(默认写目标作品的下一章)。 生产链(meta/chains continuation 登记的保护节点序列落地): 读已确认细纲 + 前章正文基线 → build_retrieval_plan + retrieve_writer_sources(生产仓储;新书无卡诚实返空) → assemble_context 冻结 WriterContext v1 → run_writer_pipeline(持久 CAS 状态链 + 机械门 + 语义 detector,有限补证/重写合同) writer 真调经 run_writer_with_receipt:runtime 自动把模型输入/输出原文与调用明细原子落库; 篇幅越界在 writer 适配层自动重抽(最多三遍);语义 detector 走冻结 detector profile 真调; 生产链暂未建补证重组装,证据缺口失败关闭(拒绝候选,留痕待人工处理) → persist_writer_execution(冻结 + 运行注册 + 候选[含语义状态] + 回执 + 机械/语义质量证据一次落库) → accept_preflight(check_writer_acceptance 纯函数 + acceptance_state 实时重读) → confirm.accept 接受为正式正文(DB 级兜底:state=passed 且 semantic_status=passed 才可写) 用法:.venv/bin/python docs/write-chapter/step2_write_chapter.py [目标章号] [--dry-run] 缺省写下一章(库内最大章序 +1)。前置:该章已建且有 confirmed 细纲(镜像 step2_setup_chapter2.py 建章 + 落细纲),且门锚合同 GATE_ANCHORS 已登记该章。 --dry-run:全链试跑——writer 与语义 detector 真调、候选与证据真实落 Shadow、接受事务试跑后 整体回滚,正典正文不动;候选留库待用户三决策。 """ import hashlib import json import subprocess import sys import uuid from datetime import datetime, timezone from decimal import Decimal from pathlib import Path from typing import Any, Mapping SCRIPT_DIR = Path(__file__).resolve().parent AGENT_ROOT = SCRIPT_DIR.parents[1] SKILLS = AGENT_ROOT / ".claude" / "skills" for sub in ( "assemble-context/scripts", "write-next-chapter/scripts", "execute-claude-task/scripts", "record-run-evidence/scripts", "access-database/scripts", "check-content-consistency/scripts", "decide-candidate/scripts", "prevent-ai-flavor/scripts", "diagnose-ai-flavor/scripts", ): p = str(SKILLS / sub) if p not in sys.path: sys.path.insert(0, p) from db import connect, DSN # noqa: E402 from assemble_writer_context import assemble_context # noqa: E402 from prevent_ai_flavor import ( # noqa: E402 PreventionContractError, build_prevention_contract, persist_prevention, render_writer_constraints, ) from diagnose_ai_flavor import run_diagnosis, persist_diagnosis # noqa: E402 from retrieve_writer_sources import ( # noqa: E402 ProductionCardIndexRepository, FrozenProseRepository, RetrievalError, build_retrieval_plan, retrieve_writer_sources, load_confirmed_fine_outline, load_confirmed_pattern_bindings, load_confirmed_style, ) from run_writer import ( # noqa: E402 WriterAdapterError, build_writer_execution_profile, calculate_dynamic_output_contract, run_writer_with_receipt, ) from run_writer_pipeline import PipelineError, run_writer_pipeline # noqa: E402 from candidate_cas import PostgresCasStateStore # noqa: E402 from run_writer_semantic_detector import ( # noqa: E402 SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA, build_safe_semantic_diagnostic, build_semantic_input_v3, run_writer_semantic_detector, ) from claude_runtime import ExecutionProfile, run_claude, sha256_json # noqa: E402 from persist_llm_call import persist_call as persist_llm_event # noqa: E402 from persist_writer_run import persist_writer_execution # noqa: E402 from run_registry import finish_run, start_run # noqa: E402 from check_writer_acceptance import AcceptanceError, check_writer_acceptance # noqa: E402 from acceptance_state import LiveStateError, build_live_acceptance_state # noqa: E402 from write_canonical import accept # noqa: E402 WORK_ID = 12 # 2026-08-14 运行绑定再登记:本机 Claude CLI 自动升级 2.1.211→2.1.231, # 仅同步可执行文件版本与哈希(模型/预算/提示词/schema 一律未动)。 CLAUDE_BIN = "/Users/qingse/.nvm/versions/node/v24.15.0/bin/claude" CLAUDE_SHA256 = "ba790279cab6ef77b713864d4bf5f764fcea87d3a3eb7591a41f741e45212b5c" CLAUDE_VERSION = "2.1.231" RESOLVED_MODEL_ID = "claude-opus-4-8[1m]" MAX_ATTEMPTS = 3 # 篇幅越界重抽上限(编排层响应,对齐 replay 自纠环语义) GATE_A_CONFIG = json.loads((SKILLS / "evaluate-frozen-replay" / "configs" / "writer-gate-a-deep-space-v1.json").read_text(encoding="utf-8")) GATE_A_WRITER = GATE_A_CONFIG["executionProfiles"]["writer"] GATE_A_DETECTOR = GATE_A_CONFIG["executionProfiles"]["semantic_detector"] SYSTEM_PROMPT = GATE_A_WRITER["systemPrompt"] SYSTEM_PROMPT_ID = GATE_A_WRITER["systemPromptId"] SYSTEM_PROMPT_SHA256 = "sha256:" + hashlib.sha256(SYSTEM_PROMPT.encode("utf-8")).hexdigest() assert SYSTEM_PROMPT_SHA256 == GATE_A_WRITER["systemPromptSha256"], "冻结提示词漂移,停止" ARTIFACTS = SCRIPT_DIR / "artifacts" # 门锚合同按章登记:锚点是章级创作判断,any-hit 子串匹配。新章必须先登记再跑。 GATE_ANCHORS = { 2: { "requiredEvents": [ {"requirementId": "event-1-isolation", "anchors": ["隔离", "收押", "禁闭", "关押", "封锁"]}, {"requirementId": "event-2-interrogation", "anchors": ["审讯", "审问", "询问", "盘问", "讯问"]}, {"requirementId": "event-3-conceal", "anchors": ["隐瞒", "没有告诉", "没说", "没有说", "咽了回去", "沉默", "闭上嘴"]}, {"requirementId": "event-4-hunger", "anchors": ["饥饿", "渴望", "吞噬", "进食", "吃", "贪"]}, ], "requiredCharacters": ["林深", "何岚"], "foreshadowingActions": [ {"requirementId": "foreshadow-upgrade", "anchors": ["异种核心", "融合", "升级"]}, ], "chapterEndHook": { "requirementId": "hook-ch2", "anchors": ["调令", "实战", "出击", "部署", "任务", "出征", "离不开", "不愿离开"], "maxDistanceFromEnd": 900, }, }, 3: { # 接第2章结尾硬钩子(茧撕开舱门出击、要吃掉更强核心)。锚点 any-hit 子串匹配; # requiredCharacters 只硬约束主角(避免过度约束触发 costly 重抽),其余靠事件锚点。 "requiredEvents": [ {"requirementId": "event-1-sortie", "anchors": ["出击", "实战", "战斗", "交火", "搏杀", "拦截", "扑向", "战场"]}, {"requirementId": "event-2-devour", "anchors": ["吞噬", "吞食", "吃掉", "进食", "撕碎", "吸收", "吞下", "吞"]}, {"requirementId": "event-3-upgrade", "anchors": ["升级", "蜕变", "进化", "变强", "增强", "新的力量", "蜕变"]}, {"requirementId": "event-4-pollution", "anchors": ["黑纹", "污染", "扩散", "蔓延", "加深", "恶化"]}, ], "requiredCharacters": ["林深"], "foreshadowingActions": [ {"requirementId": "foreshadow-voice-merge", "anchors": ["分不清", "像他自己", "脑内的声音", "低语", "渴望", "哪个念头", "另一个"]}, ], "chapterEndHook": { "requirementId": "hook-ch3", "anchors": ["深渊", "更深", "回应", "召唤", "更大", "下一", "不止", "饥饿", "注视", "凝视"], "maxDistanceFromEnd": 900, }, }, } def make_logging_runner(log_path: Path): """透传 subprocess.run,只把 sandbox 调用现场原样留档(审计用,不影响落库)。""" def runner(command, *, input, text, capture_output, timeout, check, cwd, env, start_new_session): record = {"command": list(command), "envKeys": sorted(env.keys()), "cwd": cwd, "inputChars": len(input), "startedAt": datetime.now().isoformat()} completed = subprocess.run(list(command), input=input, text=text, capture_output=capture_output, timeout=timeout, check=check, cwd=cwd, env=dict(env), start_new_session=start_new_session) record.update({"returncode": completed.returncode, "stdout": completed.stdout or "", "stderr": completed.stderr or ""}) log_path.write_text(json.dumps(record, ensure_ascii=False, indent=1), encoding="utf-8") return completed return runner def make_sequential_logging_runner(directory: Path, prefix: str): """多次模型调用(重抽 / detector 纠错环)逐次留档,不互相覆盖。""" counter = {"n": 0} def runner(command, *, input, text, capture_output, timeout, check, cwd, env, start_new_session): counter["n"] += 1 return make_logging_runner(directory / f"{prefix}-{counter['n']}.json")( command, input=input, text=text, capture_output=capture_output, timeout=timeout, check=check, cwd=cwd, env=env, start_new_session=start_new_session) return runner def build_semantic_detector_profile() -> ExecutionProfile: """从冻结配置构造语义 detector 的 ExecutionProfile(哈希在构造时自检,漂移即停)。""" cfg = GATE_A_DETECTOR assert sha256_json(SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA) == cfg["jsonSchemaSha256"], \ "语义 detector schema 与冻结配置漂移,停止" return ExecutionProfile( profile_version=cfg["profileVersion"], adapter_role="semantic_detector", claude_executable_path=cfg["claudeExecutablePath"], claude_executable_sha256=cfg["claudeExecutableSha256"], claude_cli_version=cfg["claudeCliVersion"], model_alias=cfg["modelAlias"], resolved_model_id=cfg["resolvedModelId"], effort=cfg["effort"], max_budget_usd_per_call=Decimal(cfg["maxBudgetUsdPerCall"]), timeout_seconds=cfg["timeoutSeconds"], max_context_chars=cfg["maxContextChars"], json_schema_id=cfg["jsonSchemaId"], json_schema=SEMANTIC_DETECTOR_REPORT_JSON_SCHEMA, json_schema_sha256=cfg["jsonSchemaSha256"], system_prompt_id=cfg["systemPromptId"], system_prompt=cfg["systemPrompt"], system_prompt_sha256=cfg["systemPromptSha256"], normal_terminal_reasons=tuple(cfg["normalTerminalReasons"]), ) class ProductionSemanticRunner: """语义 detector 生产 runner:与 ClaudeRuntimeModelRunner 同合同,但调用随 run_id 落库。""" def __init__(self, profile: ExecutionProfile, *, run_id: str, log_runner) -> None: self.profile = profile self.run_id = run_id self.log_runner = log_runner def run(self, *, adapter_role: str, model_input: Mapping[str, Any], output_schema: Mapping[str, Any]) -> Mapping[str, Any]: if self.profile.adapter_role != adapter_role or self.profile.json_schema != output_schema: raise PipelineError("SEMANTIC_DETECTOR_FAILED", "ExecutionProfile 与语义 detector 适配不一致") def persist_detector_event(event): # runtime 默认把非 writer 调用标为 evaluation;生产链语义审查改标 production_detection。 event = dict(event) event["purpose"] = "production_detection" return persist_llm_event(event) result = run_claude( self.profile, model_input, runner=self.log_runner, run_id=self.run_id, caller="semantic_detector", persist_call=persist_detector_event) receipt = result.receipt receipt_dict = receipt.as_dict() if hasattr(receipt, "as_dict") else receipt return {"structuredOutput": dict(result.structured_output), "modelReceiptSha256": sha256_json(receipt_dict)} def resolve_target_chapter(requested: int | None) -> int: with connect(readonly=True) as conn: if requested is not None: return requested row = conn.execute( "SELECT COALESCE(MAX(order_no),0)+1 FROM muse_content_chapter " "WHERE work_id=%s AND deleted=false", (WORK_ID,)).fetchone() return int(row[0]) def _dump(path: Path, value: Any) -> None: path.write_text(json.dumps(value, ensure_ascii=False, indent=1), encoding="utf-8") def main(): args = [arg for arg in sys.argv[1:] if not arg.startswith("--")] dry_run = "--dry-run" in sys.argv[1:] target = resolve_target_chapter(int(args[0]) if args else None) as_of = target - 1 run_id = f"run-prod-work12-ch{target}-{uuid.uuid4().hex[:8]}" generated_at = datetime.now(timezone.utc).isoformat() ARTIFACTS.mkdir(exist_ok=True) if target not in GATE_ANCHORS: raise SystemExit(f"第{target}章门锚合同未登记(GATE_ANCHORS),先登记锚点再跑。") # 1) 已确认细纲(read-context 统一消费点)+ 前章全文基线(asOf 起连续四章;不足四章从第1章起) with connect(readonly=True) as conn: try: fine_outline = load_confirmed_fine_outline(conn, work_id=WORK_ID, target_chapter=target) except RetrievalError as exc: raise SystemExit(f"{exc}(镜像 step2_setup_chapter2.py 建章并落细纲)") first = max(1, as_of - 3) recent_rows = conn.execute( "SELECT c.order_no, b.id, b.revision, b.content_text FROM muse_content_chapter c " "JOIN muse_content_block b ON b.chapter_id=c.id AND b.deleted=false " "WHERE c.work_id=%s AND c.deleted=false AND c.order_no BETWEEN %s AND %s " "ORDER BY c.order_no", (WORK_ID, first, as_of)).fetchall() style_constraints = load_confirmed_style(conn, work_id=WORK_ID) pattern_references = load_confirmed_pattern_bindings(conn, work_id=WORK_ID) recent_chapters = [{ "chapter": order_no, "sourceRef": {"sourceId": f"content-block:{block_id}", "sourceVersion": f"rev{revision}", "blockId": int(block_id), "chapter": order_no, "startCodePoint": 0, "endCodePoint": len(body)}, "text": body, } for order_no, block_id, revision, body in recent_rows] expected = list(range(first, as_of + 1)) got = [item["chapter"] for item in recent_chapters] if got != expected: raise SystemExit(f"连续前章基线缺章:期望 {expected},实际 {got}。") print(f"目标第{target}章(asOf={as_of});细纲已读,基线 {got}," f"基线总字数 {sum(len(item['text']) for item in recent_chapters)}") # 1.5) 人感前置预防:规则/声音账先形成合同,再冻结进 WriterContext。 try: humanization_contract = build_prevention_contract( f"work:{WORK_ID}", load_database=True ) humanization_contract["writer_constraints"] = render_writer_constraints(humanization_contract) prevention_receipt = persist_prevention(humanization_contract) except PreventionContractError as exc: raise SystemExit(f"人感前置预防合同失败:{exc}") from exc print(f"人感前置预防:约束 {len(humanization_contract['writer_constraints'])} 条," f"规则库 {humanization_contract['built_from']['rule_library_version']}," f"run_id={prevention_receipt['run_id']}") # 2) 检索计划 + 执行(生产仓储;新书无卡诚实返空) token_budget = {"maxContextChars": 200000} plan = build_retrieval_plan( run_id=run_id, work_id=WORK_ID, target_chapter=target, as_of=as_of, fine_outline=fine_outline, card_index_version="knowledge-index-v1", prose_index_version="content-block-v1", token_budget=token_budget) retrieval = retrieve_writer_sources( plan=plan, card_repository=ProductionCardIndexRepository(), prose_repository=FrozenProseRepository(dsn=DSN, tenant_id=0)) _dump(ARTIFACTS / f"{run_id}-retrieval.json", {"plan": plan, "resultCounts": {k: len(v) for k, v in retrieval.items() if isinstance(v, list)}}) print(f"检索: 卡={len(retrieval['cards'])} 事实={len(retrieval['factEvidence'])} " f"原文={len(retrieval['proseEvidence'])}") # 3) 组装冻结 WriterContext v1 output_contract = calculate_dynamic_output_contract( fine_outline=fine_outline, recent_chapter_bodies=[item["text"] for item in recent_chapters]) narrative_state = { "time": f"第{as_of}章结束后", "location": "承接上一章结尾的场景", "characterPositions": {}, "immediateSituation": "按细纲 chapterGoal 展开(上一章结尾状态见基线正文)。", } authorization_snapshot = {"snapshotId": "auth-work12-production-v1", "allowedPurpose": "production_generation", "verifiedAt": generated_at, "sourceVersion": "v1"} assembled = assemble_context( run_id=run_id, attempt=1, mode="production", purpose="production", quality_policy_version="writer-production-v1", work_id=WORK_ID, target_chapter=target, as_of=as_of, source_version="outline@v1", authorization_snapshot=authorization_snapshot, source_status="active", retrieval_plan=plan, retrieval_result=retrieval, fine_outline=fine_outline, narrative_state=narrative_state, recent_chapters=recent_chapters, output_contract=output_contract, token_budget=token_budget, pattern_references=pattern_references, style_constraints=style_constraints, humanization_contract=humanization_contract, generated_at=generated_at, evidence_strategy="production_dual_evidence") writer_context = assembled["context"] (ARTIFACTS / f"{run_id}-writer-context.json").write_text( assembled["contextJson"], encoding="utf-8") print(f"上下文冻结: contextSha256={writer_context['contextSnapshot']['contextSha256'][:24]}... " f"篇幅合同 {output_contract['minChars']}-{output_contract['maxChars']}" f"(目标 {output_contract['targetChars']}),范式绑定 {len(pattern_references)} 张," f"文风约束 {len(style_constraints)} 条") # 4) 生产 pipeline:持久 CAS + writer 真调 + 机械门 + 语义 detector(先审后入) writer_profile = build_writer_execution_profile( claude_executable_path=CLAUDE_BIN, claude_executable_sha256=CLAUDE_SHA256, claude_cli_version=CLAUDE_VERSION, resolved_model_id=RESOLVED_MODEL_ID, effort="high", max_budget_usd_per_call=Decimal("5.0"), timeout_seconds=1200, max_context_chars=token_budget["maxContextChars"], system_prompt=SYSTEM_PROMPT) detector_profile = build_semantic_detector_profile() semantic_runner = ProductionSemanticRunner( detector_profile, run_id=run_id, log_runner=make_sequential_logging_runner(ARTIFACTS, f"{run_id}-semantic-call")) state_store = PostgresCasStateStore( work_id=WORK_ID, target_chapter=target, creator="continuation") receipts_by_version: dict[int, Any] = {} candidates_by_version: dict[int, dict] = {} contexts_by_attempt: dict[int, dict] = {} def production_writer(current_context: Mapping[str, Any], candidate_version: int): """writer 适配:篇幅越界自动重抽(≤3 遍),成功回执按候选版本留证。""" contexts_by_attempt[current_context["attempt"]] = dict(current_context) for retry in range(1, MAX_ATTEMPTS + 1): try: candidate, receipt = run_writer_with_receipt( current_context, profile=writer_profile, candidate_version=candidate_version, runner=make_sequential_logging_runner( ARTIFACTS, f"{run_id}-sandbox-call-v{candidate_version}"), persist_call=persist_llm_event) receipts_by_version[candidate_version] = receipt candidates_by_version[candidate_version] = candidate # 人感技能 3:每个候选先做只读诊断并自动落质量账;不在这里改正文。 try: deai_artifact = run_diagnosis( candidate["candidateBody"], work_ref=f"work:{WORK_ID}", chapter_ref=f"chapter:{target}", mode="Audit", ) _dump(ARTIFACTS / f"{run_id}-ai-flavor-diagnosis-v{candidate_version}.json", deai_artifact) persist_diagnosis(deai_artifact, text=candidate["candidateBody"]) except Exception as exc: raise PipelineError( "AI_FLAVOR_DIAGNOSIS_FAILED", "候选 AI 味诊断或落库失败", details={"errorType": type(exc).__name__, "message": str(exc)}, ) from exc print(f"AI 味诊断(v{candidate_version}):发现 {len(deai_artifact['findings'])} 条") return candidate except WriterAdapterError as exc: if exc.code != "candidate_length_out_of_range" or retry == MAX_ATTEMPTS: raise PipelineError(exc.code, f"writer 真调失败: {exc}", details=exc.details) from exc print(f" 第{retry}遍越界({exc.details.get('actualHanChars')} 字),重抽……") def production_semantic_detector(current_context, candidate, mechanical_report): """语义 detector 适配:构造冻结输入、真调模型、留档输入输出。""" version = candidate["candidateVersion"] detector_input = build_semantic_input_v3( run_id=run_id, sample_id=f"writer-ch{target}", opaque_arm_id="production", writer_context=current_context, candidate=candidate) _dump(ARTIFACTS / f"{run_id}-semantic-input-v{version}.json", detector_input) outcome = run_writer_semantic_detector(detector_input, model_runner=semantic_runner) _dump(ARTIFACTS / f"{run_id}-semantic-output-v{version}.json", outcome) if outcome.get("ok") is not True or not isinstance(outcome.get("report"), Mapping): diagnostic = build_safe_semantic_diagnostic(outcome) raise PipelineError( "SEMANTIC_DETECTOR_FAILED", f"语义 detector 未产生有效报告: {diagnostic['primaryCode']}", details={"safeDiagnostic": diagnostic}) print(f"语义 detector(v{version}): status={outcome['status']} " f"调用={outcome['attemptCount']}次 纠错={outcome['correctionCount']}次") return outcome["report"] def production_evidence_provider(_context, gaps, _attempt): """生产链补证重组装未建:证据缺口失败关闭,候选拒绝留痕,交人工处理。""" raise PipelineError( "PRODUCTION_EVIDENCE_SUPPLEMENT_UNBUILT", f"语义审查发现 {len(gaps)} 个证据缺口,生产链暂不支持补证重组装,候选拒绝待人工处理") # 候选版本接续:候选表对 (作品,章,candidate_version) 唯一,重跑同章必须从已有最大版本+1 起, # 否则与上一轮留库的被拒候选撞版本。 with connect(readonly=True) as conn: max_version_row = conn.execute( "SELECT COALESCE(MAX(CASE WHEN candidate_version ~ '^[0-9]+$' " "THEN candidate_version::integer END),0) FROM example_candidate " "WHERE tenant_id=0 AND work_id=%s AND target_chapter=%s AND deleted=false", (WORK_ID, target)).fetchone() initial_candidate_version = int(max_version_row[0]) + 1 start_run(run_id=run_id, work_id=WORK_ID, target_chapter=target, trigger_detail={"stage": "writer-production-pipeline", "contextSha256": writer_context["contextSnapshot"]["contextSha256"]}, creator="continuation") try: pipeline_result = run_writer_pipeline( context=writer_context, requirements=GATE_ANCHORS[target], writer=production_writer, evidence_provider=production_evidence_provider, semantic_detector=production_semantic_detector, state_store=state_store, result_path=ARTIFACTS / f"{run_id}-pipeline-result.json", initial_candidate_version=initial_candidate_version, ) except PipelineError as exc: # 被拒候选留痕:凡跑过机械门的版本都落 Shadow(state=rejected + 机械/语义证据) trace = (exc.result or {}).get("trace") or [] audit_entry = next((entry for entry in reversed(trace) if isinstance(entry.get("mechanicalReport"), Mapping)), None) if audit_entry is not None: version = audit_entry.get("candidateVersion") failed_candidate = candidates_by_version.get(version) failed_receipt = receipts_by_version.get(version) if failed_candidate is not None and failed_receipt is not None: try: persisted = persist_writer_execution( contexts_by_attempt.get(failed_candidate.get("attempt"), writer_context), failed_candidate, failed_receipt, audit_entry["mechanicalReport"], semantic_report=audit_entry.get("semanticReport"), assemble_result=assembled) print(f"[被拒候选留库] candidate_id={persisted['candidate_id']} " f"state={persisted['state']} semantic={persisted.get('semantic_status')}") except Exception as persist_exc: # 留痕失败不掩盖原始失败码 print(f"[警告] 被拒候选留库失败: {persist_exc}", file=sys.stderr) finish_run(run_id, "failed", creator="continuation", trigger_detail={"stage": "writer-production-pipeline", "failureCode": exc.code}) print(f"[停止] 生产 pipeline 未通过: code={exc.code};{exc}") print(f"复核 artifacts/{run_id}-pipeline-result.json 后决定下一步。") print(f"\nRUN_ID={run_id}") raise SystemExit(1) except Exception as exc: # 非 PipelineError(库连接断、适配层异常等)也要收口运行态,不留 running 悬挂 try: finish_run(run_id, "failed", creator="continuation", trigger_detail={"stage": "writer-production-pipeline", "error": type(exc).__name__}) except Exception: pass raise # 5) pipeline 通过:机械门 + 语义 detector 双证据落库(候选 semantic_status=passed) candidate = pipeline_result["candidateArtifact"] final_context = contexts_by_attempt.get(pipeline_result["attempt"], writer_context) final_trace = pipeline_result["trace"][-1] receipt = receipts_by_version[pipeline_result["candidateVersion"]] persisted = persist_writer_execution( final_context, candidate, receipt, final_trace["mechanicalReport"], semantic_report=final_trace.get("semanticReport"), assemble_result=assembled) cand_id = persisted["candidate_id"] print(f"writer 产出: sha256={candidate['candidateSha256'][:24]}..., " f"实际模型={receipt.actual_model_id}, 成本=${receipt.total_cost_usd}") print(f"落库: candidate_id={cand_id}, receipt_id={persisted['receipt_id']}, " f"raw_content_id={persisted['raw_content_id']}, state={persisted['state']}, " f"semantic={persisted['semantic_status']}") # 6) 接受前置检查:实时状态重读 + 纯函数全检(上下文/授权/来源/有效期/detector 终态) try: live_state = build_live_acceptance_state(final_context) preflight = check_writer_acceptance( decision="accept", confirmed=True, context=final_context, candidate=candidate, detector_result=pipeline_result, live_state=live_state, expected_revision=live_state["canonicalRevision"]) except (LiveStateError, AcceptanceError) as exc: finish_run(run_id, "failed", creator="continuation", trigger_detail={"stage": "accept-preflight", "error": getattr(exc, "code", type(exc).__name__)}) print(f"[停止] 接受前置检查未通过: {getattr(exc, 'code', '')} {exc}") print(f"候选 {cand_id} 已留库(state=passed, semantic=passed),人工复核后决定。") print(f"\nCANDIDATE_ID={cand_id}\nRUN_ID={run_id}") raise SystemExit(1) print(f"接受前置检查: {preflight['status']} canonicalRevision={live_state['canonicalRevision']}") # 7) 接受为正式正文(confirm 唯一通道;DB 兜底复检 state + semantic_status) semantic_report_sha = str(final_trace.get("semanticReport", {}).get("reportSha256") or "")[:24] accept_result = accept( cand_id, decided_by="1", rationale=(f"第{target}章生产路径:冻结上下文(asOf={as_of}) → writer sandbox 真调 → " f"机械门通过 → 语义 detector 通过(报告sha256={semantic_report_sha}...) " f"→ accept_preflight {preflight['status']}。"), basis_ref=f"细纲@v1 (第{target}章 confirmed) + 前章正文基线", expected_revision=live_state["canonicalRevision"], command_id=f"accept-work12-ch{target}-{run_id}", projection_kinds=("extraction",), # 提交后副作用:章后抽取投影随提交登记,异步执行 dry_run=dry_run) print(f"接受结果: {json.dumps(accept_result, ensure_ascii=False)}") if dry_run: print("[试跑] 全链验证完成:正文未动,候选已留 Shadow,等待用户三决策。") print(f"\nCANDIDATE_ID={cand_id}\nRUN_ID={run_id}") if __name__ == "__main__": main()