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

268 lines
12 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
"""把 AI 味案例卡与来源重验证回执写入 agent-example 的 muse-example。
采集脚本保持确定性、可离线回放;本脚本是唯一的持久化边界,复用
``access-database`` 的连接入口。案例卡做幂等当前投影,重验证批次/回执做
append-only 账本。研究限定来源只写 hash、位置和观察,不写第三方正文。
"""
from __future__ import annotations
import argparse
import hashlib
import json
import sys
from pathlib import Path
from typing import Any
import yaml
SCRIPT_DIR = Path(__file__).resolve().parent
DB_SCRIPTS = SCRIPT_DIR.parents[1] / "access-database" / "scripts"
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
if str(DB_SCRIPTS) not in sys.path:
sys.path.insert(0, str(DB_SCRIPTS))
from capture_cases import ( # noqa: E402
CaseCardError,
REVALIDATION_SCHEMA_VERSION,
SCHEMA_VERSION,
load_verification,
validate_card,
)
from db import connect # noqa: E402
TENANT_ID = 1
CREATOR = "1"
def _logical_ref(path: Path) -> str:
"""落库只保留可迁移的证据名,不把本机绝对路径当跨环境引用。"""
return f"capture://ai-flavor/{path.name}"
def _sha256_file(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _json(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def _load_object(path: Path) -> dict:
try:
value = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
except (OSError, UnicodeError, yaml.YAMLError) as exc:
raise CaseCardError(f"{path}: 读取失败: {exc}") from exc
if not isinstance(value, dict):
raise CaseCardError(f"{path}: 顶层必须是对象")
return value
def load_inventory(path: Path) -> dict:
"""加载并逐卡验证 inventory;返回可安全写入的报告对象。"""
report = _load_object(path)
if report.get("schema_version") != "ai-flavor-inventory-v1":
raise CaseCardError(f"{path}: 不是 ai-flavor-inventory-v1 清单")
cards = report.get("cards")
if not isinstance(cards, list):
raise CaseCardError(f"{path}: cards 必须是数组")
seen: set[str] = set()
for card in cards:
validate_card(card)
if card["id"] in seen:
raise CaseCardError(f"{path}: 案例卡 id 重复: {card['id']}")
seen.add(card["id"])
totals = report.get("totals") or {}
if totals.get("cards") != len(cards):
raise CaseCardError(f"{path}: totals.cards 与 cards 数量不一致")
return report
def _validate_pair(inventory: dict, verification: dict, *, inventory_ref: str,
report_ref: str, inventory_sha256: str, report_sha256: str) -> dict:
"""校验一次检测产出的卡片集合和同运行重验证回执。"""
if inventory.get("schema_version") != "ai-flavor-inventory-v1":
raise CaseCardError("inventory 不是 ai-flavor-inventory-v1 清单")
cards = inventory.get("cards")
if not isinstance(cards, list):
raise CaseCardError("inventory.cards 必须是数组")
for card in cards:
validate_card(card)
verification = verification if isinstance(verification, dict) else {}
if verification.get("schema_version") != REVALIDATION_SCHEMA_VERSION:
raise CaseCardError("重验证版本不支持")
if verification.get("report_ref") and verification["report_ref"] != report_ref:
raise CaseCardError("重验证 report_ref 与导入引用不一致")
results = verification["cards"]
if not isinstance(results, list):
raise CaseCardError("重验证 cards 必须是数组")
card_ids = {card["id"] for card in cards}
result_ids = {item.get("card_id") for item in results}
if card_ids != result_ids:
missing = sorted(card_ids - result_ids)
extra = sorted(result_ids - card_ids)
raise CaseCardError(f"清单/重验证卡片集合不一致: missing={missing[:3]} extra={extra[:3]}")
by_id = {card["id"]: card for card in cards}
for result in results:
card = by_id[result["card_id"]]
source_sha = card["source"]["source_sha256"]
if result.get("expected_source_sha256") != source_sha:
raise CaseCardError(f"{result['card_id']}: expected_source_sha256 与卡片不一致")
if result.get("checked_on") != verification.get("checked_on"):
raise CaseCardError(f"{result['card_id']}: checked_on 与批次不一致")
return {
"inventory": inventory,
"verification": verification,
"inventory_sha256": inventory_sha256,
"report_sha256": report_sha256,
"inventory_ref": inventory_ref,
"report_ref": report_ref,
}
def prepare_import(inventory_path: Path, verification_path: Path) -> dict:
"""准备恢复/迁移导入;正常检测不需要调用此函数。"""
return _validate_pair(
load_inventory(inventory_path), load_verification(verification_path),
inventory_ref=_logical_ref(inventory_path), report_ref=_logical_ref(verification_path),
inventory_sha256=_sha256_file(inventory_path), report_sha256=_sha256_file(verification_path),
)
def persist_generated(*, inventory: dict, verification: dict, inventory_ref: str,
report_ref: str, inventory_sha256: str, report_sha256: str,
creator: str = CREATOR, tenant_id: int = TENANT_ID) -> dict:
"""检测命令的自动写入口;不要求先生成可导入文件。"""
prepared = _validate_pair(
inventory, verification, inventory_ref=inventory_ref, report_ref=report_ref,
inventory_sha256=inventory_sha256, report_sha256=report_sha256,
)
return persist(prepared, creator=creator, tenant_id=tenant_id)
def _card_params(card: dict, prepared: dict, *, creator: str, tenant_id: int) -> tuple:
source = card["source"]
return (
card["id"], card["schema_version"], card["card_type"], card["state"],
card["label"], card["layer"], card["carrier"], card["capture_mode"],
source["kind"], source["license"], source["source_sha256"], source["excerpt_sha256"],
source.get("source_ref", ""), source.get("work_ref"), _json(source["location"]),
_json(source.get("surface_location")) if source.get("surface_location") else None,
card.get("excerpt", ""), card.get("context", ""), _json(card["observation"]),
_json(card["feedback"]) if card.get("feedback") is not None else None,
_json(card["review"]) if card.get("review") is not None else None,
_json(card.get("rule_candidate_ids", [])), prepared["inventory_ref"],
prepared["inventory_sha256"], creator, creator, tenant_id,
)
def persist(prepared: dict, *, creator: str = CREATOR, tenant_id: int = TENANT_ID) -> dict:
"""事务性导入;重复运行不重复插入批次/逐卡回执。"""
inventory = prepared["inventory"]
verification = prepared["verification"]
cards = inventory["cards"]
results = verification["cards"]
batch_sql = (
"INSERT INTO example_ai_flavor_revalidation_batch "
"(batch_ref,report_sha256,schema_version,checked_on,source_root_ref,usable,totals,works,report_path,creator,tenant_id) "
"VALUES (%s,%s,%s,%s,%s,%s,%s::jsonb,%s::jsonb,%s,%s,%s) "
"ON CONFLICT (tenant_id,report_sha256) DO NOTHING RETURNING id"
)
card_sql = (
"INSERT INTO example_ai_flavor_case "
"(card_id,schema_version,card_type,state,label,layer,carrier,capture_mode,source_kind,source_license,"
"source_sha256,excerpt_sha256,source_ref,work_ref,source_location,surface_location,excerpt,context,observation,"
"feedback,review,rule_candidate_ids,inventory_ref,inventory_sha256,creator,updater,tenant_id) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s::jsonb,%s::jsonb,%s,%s,%s::jsonb,%s::jsonb,%s::jsonb,%s::jsonb,%s,%s,%s,%s,%s) "
"ON CONFLICT (tenant_id,card_id) DO UPDATE SET "
"schema_version=EXCLUDED.schema_version,card_type=EXCLUDED.card_type,source_kind=EXCLUDED.source_kind,"
"source_license=EXCLUDED.source_license,source_sha256=EXCLUDED.source_sha256,excerpt_sha256=EXCLUDED.excerpt_sha256,"
"source_ref=EXCLUDED.source_ref,work_ref=EXCLUDED.work_ref,source_location=EXCLUDED.source_location,"
"surface_location=EXCLUDED.surface_location,observation=EXCLUDED.observation,feedback=EXCLUDED.feedback,"
"rule_candidate_ids=EXCLUDED.rule_candidate_ids,inventory_ref=EXCLUDED.inventory_ref,"
"inventory_sha256=EXCLUDED.inventory_sha256,updater=EXCLUDED.updater,update_time=CURRENT_TIMESTAMP "
"WHERE example_ai_flavor_case.state NOT IN ('canonical','rejected','archived')"
)
result_sql = (
"INSERT INTO example_ai_flavor_revalidation "
"(batch_id,card_id,work_ref,source_ref,expected_source_sha256,actual_source_sha256,status,reason,checked_on,creator,tenant_id) "
"VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s) "
"ON CONFLICT (tenant_id,batch_id,card_id) DO NOTHING"
)
with connect() as conn:
try:
batch_row = conn.execute(
batch_sql,
(prepared["report_ref"], prepared["report_sha256"], verification["schema_version"],
verification["checked_on"], verification.get("source_root_ref"), verification["usable"],
_json(verification.get("totals", {})), _json(verification.get("works", [])),
prepared["report_ref"], creator, tenant_id),
).fetchone()
if batch_row:
batch_id = batch_row[0]
else:
batch_id = conn.execute(
"SELECT id FROM example_ai_flavor_revalidation_batch WHERE tenant_id=%s AND report_sha256=%s",
(tenant_id, prepared["report_sha256"]),
).fetchone()[0]
for card in cards:
conn.execute(card_sql, _card_params(card, prepared, creator=creator, tenant_id=tenant_id))
for result in results:
conn.execute(result_sql, (
batch_id, result["card_id"], result.get("work_ref"), result.get("source_ref", ""),
result["expected_source_sha256"], result.get("actual_source_sha256"), result["status"],
result.get("reason", ""), result["checked_on"], creator, tenant_id,
))
counts = conn.execute(
"SELECT count(*) FILTER (WHERE deleted=false), "
"count(*) FILTER (WHERE deleted=false AND state='shadow') "
"FROM example_ai_flavor_case WHERE tenant_id=%s", (tenant_id,),
).fetchone()
receipt_count = conn.execute(
"SELECT count(*) FROM example_ai_flavor_revalidation WHERE tenant_id=%s AND batch_id=%s",
(tenant_id, batch_id),
).fetchone()[0]
conn.commit()
except Exception:
conn.rollback()
raise
return {
"status": "written",
"batch_id": batch_id,
"inventory_cards": len(cards),
"case_rows": int(counts[0]),
"shadow_rows": int(counts[1]),
"revalidation_rows": int(receipt_count),
"report_sha256": prepared["report_sha256"],
}
def _parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="AI 味案例卡与重验证回执落库")
parser.add_argument("inventory", type=Path, help="ai-flavor-inventory-v1 JSON/YAML")
parser.add_argument("verification", type=Path, help="ai-flavor-revalidation-v1 JSON/YAML")
parser.add_argument("--creator", default=CREATOR)
parser.add_argument("--tenant-id", type=int, default=TENANT_ID)
return parser
def main(argv: list[str] | None = None) -> int:
args = _parser().parse_args(argv)
prepared = prepare_import(args.inventory, args.verification)
print(json.dumps(persist(prepared, creator=args.creator, tenant_id=args.tenant_id),
ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (CaseCardError, OSError, KeyError, TypeError, IndexError) as exc:
print(f"CASE_CARD_PERSIST_FAILED: {exc}", file=sys.stderr)
raise SystemExit(2)