#!/usr/bin/env python3 """技能 3「诊断」确定性脚本层(专题-09 §5.3)。 职责边界:只查不改——对目标正文跑 humanization/rules 的 active 规则, 产出诊断产物(产物头 + 发现清单)。语义层判定由外部模型/人产出后经 merge_model_findings 注入,同样过合同校验;诊断不修改任何正文。 落库合同(检测完成即落库): - example_run:一次诊断一行(run_id 由 作品+文本哈希+规则库版本 决定,重跑幂等); - example_quality_result:judge_kind=detection,绑被诊断文本的 sha256; - 只有显式 --offline 才不写库(仅产文件,供回放)。 """ import argparse import hashlib import json import sys from pathlib import Path # 共享执行骨架(humanization 副本)与数据库通道(access-database)的引导 SCRIPT_DIR = Path(__file__).resolve().parent AGENT_ROOT = SCRIPT_DIR.parents[3] for _p in (AGENT_ROOT / "humanization" / "src", AGENT_ROOT / ".claude" / "skills" / "access-database" / "scripts"): if str(_p) not in sys.path: sys.path.insert(0, str(_p)) from deai import diagnose, load # noqa: E402 TENANT_ID = 1 CREATOR = "1" class DiagnoseContractError(ValueError): """诊断合同失败:下游修订/前置预防必须失败关闭。""" def rule_library_version(rules: dict) -> str: """兼容入口;真实指纹由共享装载器覆盖完整规则内容。""" return load.rule_library_version(rules) def load_active_library() -> tuple[dict, str]: """装载规则库并强制激活门:样例不齐的规则装载器直接拒绝(专题-09 §4.1)。""" samples = load.load_samples() rules = load.load_rules(samples=samples) return rules, rule_library_version(rules) def run_diagnosis(text: str, *, work_ref: str, chapter_ref: str | None = None, external_findings: list | None = None, mode: str = "Audit") -> dict: """产出完整诊断产物;产物头缺项由 validate_artifact 兜底拒绝。""" if not text: raise DiagnoseContractError("诊断文本为空") if not work_ref: raise DiagnoseContractError("诊断必须带 work_ref") rules, lib_version = load_active_library() artifact = diagnose.run_deterministic_rules(text, load.active_rules(rules), lib_version, mode) if external_findings: diagnose.merge_model_findings(artifact, external_findings, rules=rules, text=text) diagnose.validate_artifact(artifact) artifact["work_ref"] = work_ref if chapter_ref: artifact["chapter_ref"] = chapter_ref return artifact def _run_id(*parts: str) -> str: return "diag-" + hashlib.sha256("|".join(parts).encode("utf-8")).hexdigest()[:40] def persist_diagnosis(artifact: dict, *, text: str, creator: str = CREATOR, tenant_id: int = TENANT_ID) -> dict: """诊断运行落库:example_run(幂等 upsert)+ example_quality_result(append-only)。""" from db import connect if not isinstance(text, str) or not text: raise DiagnoseContractError("落库诊断文本为空") try: diagnose.validate_artifact(artifact) except (ValueError, KeyError, TypeError) as exc: raise DiagnoseContractError(f"落库诊断产物不可读: {exc}") from exc expected_hash = diagnose.text_hash(text) if artifact["text_hash"] != expected_hash: raise DiagnoseContractError("落库诊断产物 text_hash 与正文不一致") if not artifact.get("work_ref"): raise DiagnoseContractError("落库诊断必须带 work_ref") _, current_library_version = load_active_library() if artifact["rule_library_version"] != current_library_version: raise DiagnoseContractError("落库诊断产物使用了过期规则库") run_id = _run_id(artifact["work_ref"], artifact["text_hash"], artifact["rule_library_version"]) text_sha = hashlib.sha256(text.encode("utf-8")).hexdigest() per_rule: dict[str, int] = {} for f in artifact["findings"]: per_rule[f["rule_id"]] = per_rule.get(f["rule_id"], 0) + 1 per_layer: dict[str, int] = {} per_decision: dict[str, int] = {} for finding in artifact["findings"]: per_layer[finding["layer"]] = per_layer.get(finding["layer"], 0) + 1 decision = finding["decision_proposal"] per_decision[decision] = per_decision.get(decision, 0) + 1 detail = { "text_hash": artifact["text_hash"], "rule_library_version": artifact["rule_library_version"], "mode": artifact["mode"], "work_ref": artifact["work_ref"], "chapter_ref": artifact.get("chapter_ref"), "findings_total": len(artifact["findings"]), "findings_per_rule": per_rule, "findings_per_layer": per_layer, "decision_proposals": per_decision, } run_sql = ( "INSERT INTO example_run (run_id, work_id, trigger_source, trigger_detail, " "terminal_state, finished_at, creator, tenant_id) " "VALUES (%s, NULL, 'diagnostic', %s::jsonb, 'completed', CURRENT_TIMESTAMP, %s, %s) " "ON CONFLICT (run_id) DO UPDATE SET terminal_state='completed', " "finished_at=CURRENT_TIMESTAMP, trigger_detail=EXCLUDED.trigger_detail, " "updater=EXCLUDED.creator, update_time=CURRENT_TIMESTAMP" ) quality_sql = ( "INSERT INTO example_quality_result " "(run_id, candidate_sha256, judge_kind, dimension, scale_version, conclusion, detail, creator, tenant_id) " "VALUES (%s, %s, 'detection', NULL, %s, %s, %s::jsonb, %s, %s)" ) with connect() as conn: with conn.transaction(): conn.execute(run_sql, (run_id, json.dumps(detail, ensure_ascii=False), creator, tenant_id)) # append-only 表不能 ON CONFLICT 更新;按幂等键预检,重复运行不重复记账 exists = conn.execute( "SELECT 1 FROM example_quality_result WHERE tenant_id=%s AND run_id=%s " "AND judge_kind='detection' AND COALESCE(candidate_sha256,'')=%s", (tenant_id, run_id, text_sha), ).fetchone() if exists is None: conn.execute(quality_sql, ( run_id, text_sha, artifact["rule_library_version"], "has_findings" if artifact["findings"] else "clean", json.dumps(detail, ensure_ascii=False), creator, tenant_id, )) return {"run_id": run_id, "text_sha256": text_sha, "findings": len(artifact["findings"])} def _parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser(description="技能 3 诊断:只查不改,产出诊断产物并落库") sub = parser.add_subparsers(dest="command", required=True) run = sub.add_parser("run") run.add_argument("--text-file", type=Path, required=True) run.add_argument("--work-ref", required=True) run.add_argument("--chapter-ref") run.add_argument("--external-findings", type=Path, help="语义层外部判定(JSON 数组),注入前逐条过 finding 合同") run.add_argument("--mode", default="Audit", choices=["Audit", "Patch"]) run.add_argument("--output", type=Path, required=True) run.add_argument("--offline", action="store_true", help="只产文件,不写 muse-example") return parser def main(argv: list[str] | None = None) -> int: args = _parser().parse_args(argv) try: text = args.text_file.read_text(encoding="utf-8") external = None if args.external_findings: external = json.loads(args.external_findings.read_text(encoding="utf-8")) artifact = run_diagnosis(text, work_ref=args.work_ref, chapter_ref=args.chapter_ref, external_findings=external, mode=args.mode) args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(json.dumps(artifact, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") if args.offline: persistence = {"status": "offline", "reason": "显式 --offline,未写 muse-example"} else: persistence = persist_diagnosis(artifact, text=text) print(json.dumps({ "findings": len(artifact["findings"]), "text_hash": artifact["text_hash"], "rule_library_version": artifact["rule_library_version"], "output": str(args.output), "persistence": persistence, }, ensure_ascii=False)) return 0 except (DiagnoseContractError, diagnose.ArtifactIncomplete, load.LoadError, ValueError, OSError, UnicodeError) as exc: print(f"DIAGNOSE_CONTRACT_FAILED: {exc}") return 2 if __name__ == "__main__": raise SystemExit(main())