muse-agent-example/docs/write-chapter/step2a_retrofit_ch1_context.py
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

142 lines
6.8 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
"""真写一章 · 阶段二a 补建:第 1 章的开篇上下文与冻结记录。
原则:不存在没有上下文的写作。第 1 章之前也有设定、大纲、细纲——它的上下文是
asOf=0(开篇前冻结线):证据只有设定包与细纲宣告的新事实,历史正文基线为空。
此前第 1 章按"基线章无上下文"的旧口径写成,看板运行详情因此缺一块上下文冻结;
本脚本按合同补建真实的第 1 章上下文并落冻结行(append-only,幂等),补齐记账。
只组装 + 落冻结,不改写第 1 章正文(正文早已接受为正式内容)。
"""
import json
import sys
import uuid
from datetime import datetime, timezone
from pathlib import Path
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",
"record-run-evidence/scripts",
"access-database/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 retrieve_writer_sources import ( # noqa: E402
ProductionCardIndexRepository, FrozenProseRepository,
build_retrieval_plan, retrieve_writer_sources,
)
from persist_context_freeze import persist_freeze # noqa: E402
from run_writer import calculate_dynamic_output_contract # noqa: E402
WORK_ID = 12
TARGET_CHAPTER = 1
AS_OF = 0
SETTING_SOURCE_REF = {"sourceId": "planning-section:3", "sourceVersion": "v1"}
OUTLINE_SOURCE_REF = {"sourceId": "fine-outline:work12-ch1", "sourceVersion": "v1"}
def main():
run_id = f"retrofit-work12-ch1-context-{uuid.uuid4().hex[:8]}"
generated_at = datetime.now(timezone.utc).isoformat()
with connect(readonly=True) as conn:
setting_row = conn.execute(
"SELECT payload FROM example_planning_section WHERE id=3 AND work_id=%s "
"AND section_type='setting' AND state='confirmed' AND deleted=false",
(WORK_ID,)).fetchone()
outline_row = conn.execute(
"SELECT payload FROM example_planning_section WHERE work_id=%s AND "
"section_type='fine_outline' AND target_chapter=1 AND state='confirmed' AND "
"deleted=false ORDER BY version DESC LIMIT 1", (WORK_ID,)).fetchone()
if not setting_row or not outline_row:
raise SystemExit("设定包或第1章细纲缺失,无法补建上下文。")
setting = setting_row[0] if isinstance(setting_row[0], dict) else json.loads(setting_row[0])
fine_outline = outline_row[0] if isinstance(outline_row[0], dict) else json.loads(outline_row[0])
# 第1章细纲的 declaredNewFacts 是旧形状(纯字符串);合同要 {factId,text,sourceRef}。
# 不重写已确认规划行,在读取侧按合同投影。
normalized_declared = []
for index, fact in enumerate(fine_outline.get("declaredNewFacts", []), 1):
if isinstance(fact, dict):
normalized_declared.append(fact)
else:
normalized_declared.append({
"factId": f"fine-outline-ch1:{index}", "text": str(fact),
"sourceRef": OUTLINE_SOURCE_REF,
})
fine_outline = {**fine_outline, "declaredNewFacts": normalized_declared}
# 权威事实证据:设定包(正式设定)+ 细纲宣告的新事实
authoritative_facts = []
for key, label in (("world", "世界观"), ("power_system", "力量体系"),
("core_premise", "核心设定")):
if setting.get(key):
authoritative_facts.append({
"factId": f"setting:{key}", "fact": f"{label}:{setting[key]}",
"sourceType": "formal_setting", "sourceRef": SETTING_SOURCE_REF,
"riskLevel": "low",
})
for index, fact in enumerate(fine_outline.get("declaredNewFacts", []), 1):
text = fact["text"] if isinstance(fact, dict) else str(fact)
authoritative_facts.append({
"factId": f"fine-outline-ch1:{index}", "fact": text,
"sourceType": "fine_outline_declared_new", "sourceRef": OUTLINE_SOURCE_REF,
"riskLevel": "low",
})
# 检索计划与执行(asOf=0;新书无卡,卡线诚实返空;事实线取权威事实)
token_budget = {"maxContextChars": 200000}
plan = build_retrieval_plan(
run_id=run_id, work_id=WORK_ID, target_chapter=TARGET_CHAPTER, 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),
authoritative_facts=authoritative_facts)
print(f"检索: 卡={len(retrieval['cards'])} 事实={len(retrieval['factEvidence'])} "
f"原文={len(retrieval['proseEvidence'])}(asOf=0,历史正文必为空)")
# 组装第 1 章上下文(asOf=0,无历史正文基线)并落冻结
output_contract = calculate_dynamic_output_contract(
fine_outline=fine_outline, recent_chapter_bodies=[])
narrative_state = {
"time": "故事开篇之前",
"location": "深渊星域",
"characterPositions": {},
"immediateSituation": "一切尚未发生;按设定与第1章细纲展开。",
}
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_CHAPTER, 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=[],
output_contract=output_contract, token_budget=token_budget,
pattern_references=[], generated_at=generated_at,
evidence_strategy="production_dual_evidence")
(SCRIPT_DIR / "artifacts").mkdir(exist_ok=True)
(SCRIPT_DIR / "artifacts" / f"{run_id}-writer-context.json").write_text(
assembled["contextJson"], encoding="utf-8")
frozen = persist_freeze(assembled)
print(f"第1章上下文冻结: {json.dumps(frozen, ensure_ascii=False)}")
print(f"事实证据 {len(assembled['context']['factEvidence'])} 条,"
f"正文证据 {len(assembled['context']['proseEvidence'])} 条(应为 0)")
print(f"RUN_ID={run_id}")
if __name__ == "__main__":
main()