158 lines
6.3 KiB
Python
158 lines
6.3 KiB
Python
"""Deterministic Phase 3 review checks for the Python core."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import re
|
||
from datetime import datetime, timezone
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
from scripts.runtime.artifacts import load_manifest, write_manifest
|
||
from scripts.runtime.tasks import validate_packet
|
||
|
||
|
||
def utc_now_iso() -> str:
|
||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
||
|
||
|
||
def _source_ids_from_jsonl(path: Path) -> set[str]:
|
||
ids: set[str] = set()
|
||
if not path.exists():
|
||
return ids
|
||
for line in path.read_text(encoding="utf-8").splitlines():
|
||
if not line.strip():
|
||
continue
|
||
try:
|
||
obj = json.loads(line)
|
||
except json.JSONDecodeError:
|
||
continue
|
||
source_id = obj.get("id") or obj.get("source_id")
|
||
if source_id:
|
||
ids.add(str(source_id))
|
||
return ids
|
||
|
||
|
||
def _draft_citations(drafts: list[Path]) -> set[str]:
|
||
cited: set[str] = set()
|
||
for draft in drafts:
|
||
cited.update(re.findall(r"\[(src_[A-Za-z0-9_-]+)\]", draft.read_text(encoding="utf-8")))
|
||
return cited
|
||
|
||
|
||
def _ready_packet_stems(project_root: Path) -> set[str]:
|
||
ready: set[str] = set()
|
||
for path in sorted((project_root / "phase2" / "packets").glob("*.json")):
|
||
try:
|
||
packet = json.loads(path.read_text(encoding="utf-8"))
|
||
validate_packet(packet)
|
||
except Exception:
|
||
continue
|
||
ready.add(path.stem)
|
||
return ready
|
||
|
||
|
||
def _draft_quality_findings(drafts: list[Path]) -> list[dict[str, Any]]:
|
||
findings: list[dict[str, Any]] = []
|
||
generic_markers = [
|
||
"需要进一步完善",
|
||
"应当加强",
|
||
"持续改进",
|
||
"系统性",
|
||
"闭环管理",
|
||
"质量文化",
|
||
]
|
||
for draft in drafts:
|
||
text = draft.read_text(encoding="utf-8")
|
||
zh_chars = sum(1 for char in text if "\u4e00" <= char <= "\u9fff")
|
||
citations = re.findall(r"\[(src_[A-Za-z0-9_-]+)\]", text)
|
||
evidence_table_present = "证据落点" in text and "待补证据" in text
|
||
if zh_chars >= 1200 and len(set(citations)) < 5:
|
||
findings.append({"severity": "P1", "message": f"{draft.name} 引用来源过少,可能未充分使用 evidence packet。"})
|
||
if zh_chars >= 1200 and not evidence_table_present:
|
||
findings.append({"severity": "P1", "message": f"{draft.name} 缺少“证据落点与待补证据”小节,难以判断 evidence 是否真正落到纸面。"})
|
||
generic_count = sum(text.count(marker) for marker in generic_markers)
|
||
if zh_chars >= 1200 and generic_count >= 18:
|
||
findings.append({"severity": "P1", "message": f"{draft.name} 泛化管理表述过多,需要回炉为具体审计发现、风险影响和整改动作。"})
|
||
return findings
|
||
|
||
|
||
def build_phase3_critique(project_root: Path) -> Path:
|
||
manifest = load_manifest(project_root)
|
||
drafts = sorted((project_root / "phase2" / "drafts").glob("ch*.md"))
|
||
packets = sorted((project_root / "phase2" / "packets").glob("*.json"))
|
||
ready_stems = _ready_packet_stems(project_root)
|
||
packet_errors = [
|
||
path for path in sorted((project_root / "phase2" / "packet_errors").glob("*.json"))
|
||
if path.stem not in ready_stems
|
||
]
|
||
chapter_errors = sorted((project_root / "phase2" / "chapter_errors").glob("*.json"))
|
||
sources = _source_ids_from_jsonl(project_root / "phase2" / "sources.jsonl")
|
||
cited = _draft_citations(drafts)
|
||
missing_sources = sorted(cited - sources) if sources else sorted(cited)
|
||
uncited_sources = sorted(sources - cited) if cited else sorted(sources)
|
||
|
||
findings: list[dict[str, Any]] = []
|
||
if not drafts:
|
||
findings.append({"severity": "P1", "message": "Phase 2 drafts 缺失,尚不能进入 Phase 4 成稿。"})
|
||
if packet_errors:
|
||
findings.append({"severity": "P1", "message": f"存在 {len(packet_errors)} 个 packet 失败,需要回炉补证据。"})
|
||
if chapter_errors:
|
||
findings.append({"severity": "P1", "message": f"存在 {len(chapter_errors)} 个章节组装失败,需要修复引用或重写该章。"})
|
||
quality_holds = manifest.get("quality_holds") or []
|
||
if quality_holds:
|
||
findings.append({"severity": "P1", "message": "存在质量暂停标记:" + ", ".join(quality_holds)})
|
||
findings.extend(_draft_quality_findings(drafts))
|
||
if missing_sources:
|
||
findings.append({"severity": "P1", "message": f"正文引用未在 sources.jsonl 中登记:{', '.join(missing_sources)}"})
|
||
if not findings:
|
||
findings.append({"severity": "P2", "message": "基础产物完整;仍需人工或大上下文模型审校逻辑链、反方证据和章节叙事。"})
|
||
|
||
lines = [
|
||
"# Phase 3 审校 critique",
|
||
"",
|
||
f"- 项目:{manifest.get('topic', project_root.name)}",
|
||
f"- 运行时:python-core-v0.20",
|
||
f"- drafts:{len(drafts)}",
|
||
f"- packets:{len(packets)}",
|
||
f"- sources:{len(sources)}",
|
||
f"- cited_source_ids:{', '.join(sorted(cited)) if cited else '无'}",
|
||
"",
|
||
"## Findings",
|
||
"",
|
||
]
|
||
for item in findings:
|
||
lines.append(f"- [{item['severity']}] {item['message']}")
|
||
lines.extend(
|
||
[
|
||
"",
|
||
"## Residual Risks",
|
||
"",
|
||
"- 本 deterministic review 只做结构、引用和错误包检查;深层逻辑审校仍建议交给 `phase3_review` 角色执行。",
|
||
"- 若 sources 为空,本审校会把所有正文引用视为待登记来源。",
|
||
"",
|
||
"## Next",
|
||
"",
|
||
"- 若存在 P1,先回到 Phase 2 修复 packet/chapter 错误。",
|
||
"- 若仅有 P2,可进入 `dr.py finalize` 的中文原生成稿路径。",
|
||
"",
|
||
]
|
||
)
|
||
out = project_root / "phase3" / "critique.md"
|
||
out.parent.mkdir(parents=True, exist_ok=True)
|
||
out.write_text("\n".join(lines), encoding="utf-8")
|
||
phase3 = manifest.setdefault("phase3", {})
|
||
phase3.update(
|
||
{
|
||
"status": "completed",
|
||
"critique_path": "phase3/critique.md",
|
||
"findings_total": len(findings),
|
||
"missing_sources": missing_sources,
|
||
"uncited_sources": uncited_sources,
|
||
"updated_at": utc_now_iso(),
|
||
}
|
||
)
|
||
manifest["updated_at"] = utc_now_iso()
|
||
write_manifest(project_root, manifest)
|
||
return out
|