Files
FreeCADWasmDev/tools/run_full_parity_gate.py
wangdequan 9a7c4f0335 Save FreeCAD Web workspace sources and user validation evidence
IFC native binding remains incomplete; preserve accurate test results.
2026-09-15 22:01:58 +08:00

250 lines
10 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
"""Aggregate the source-backed P6 gates without upgrading boundaries to parity.
The detailed object, format, Python, workbench, view, document and quality
artifacts remain authoritative. This gate only verifies their coverage and
combines their terminal dispositions; it never synthesizes native results.
"""
from __future__ import annotations
import hashlib
import json
import subprocess
from collections import Counter
from datetime import datetime, timezone
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
DOCS = ROOT / "docs"
P6 = DOCS / "p6"
STATE_PATH = DOCS / "current" / "task-state.json"
def sha256(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def load_json(relative: str) -> tuple[dict, dict]:
path = ROOT / relative
value = json.loads(path.read_text(encoding="utf-8"))
return value, {
"path": relative,
"sha256": sha256(path),
}
def source_revision() -> str:
try:
return subprocess.run(
["git", "-C", str(ROOT / "freecad-source"), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
).stdout.strip()
except (OSError, subprocess.CalledProcessError):
return "unknown"
def status_from_counts(counts: dict[str, int], boundaries: list[str]) -> str:
unresolved = sum(
value
for key, value in counts.items()
if key in {"blocked", "not_implemented", "restricted", "planned", "failed"}
)
if unresolved:
return "pending"
# Explicit boundaries are conclusions, but are not approved parity.
return "pending" if boundaries else "passed"
def area(
area_id: str,
gate_file: str,
inventory_records: int | None,
count_keys: tuple[str, ...],
) -> tuple[dict, dict]:
gate, artifact = load_json(gate_file)
coverage = gate.get("coverage", {})
counts = coverage.get("counts") or coverage.get("dispositions") or {}
counts = {str(key): int(value) for key, value in counts.items()}
terminal = coverage.get("terminalRecords")
if terminal is None:
terminal = coverage.get("records")
if isinstance(terminal, list):
terminal = len(terminal)
if terminal is None and inventory_records is not None:
terminal = inventory_records
boundaries = [str(item) for item in gate.get("boundaries", [])]
expected = inventory_records if inventory_records is not None else coverage.get("inventoryRecords")
complete_records = (
terminal == expected
if expected is not None
else gate.get("result") in {"pass", "pass_with_boundaries", "pass_with_blocked_records"}
)
normalized = {key: counts.get(key, 0) for key in count_keys}
unresolved = sum(
value
for key, value in counts.items()
if key in {"blocked", "not_implemented", "restricted", "planned", "failed"}
)
result = {
"id": area_id,
"taskId": gate.get("taskId"),
"gateResult": gate.get("result"),
"status": status_from_counts(counts, boundaries),
"inventoryRecords": expected,
"terminalRecords": terminal,
"allRecordsTerminal": complete_records,
"counts": normalized,
"unresolvedRecords": unresolved,
"boundaries": boundaries,
"approvedBoundaries": [],
"unapprovedBoundaries": boundaries,
"evidence": [artifact],
}
return result, artifact
def main() -> int:
state = json.loads(STATE_PATH.read_text(encoding="utf-8"))
audit, audit_artifact = load_json("docs/p6/full-parity-audit.json")
area_specs = [
("core-shell", "docs/p6/core-shell-gate.json", None, ()),
("document-persistence", "docs/p6/document-contract-gate.json", None, ()),
("object-coverage", "docs/p6/object-contract-gate.json", 403, ("passed", "passed_boundary", "blocked")),
("format-coverage", "docs/p6/format-coverage-gate.json", 236, ("passed", "passed_boundary", "blocked")),
("python-api", "docs/p6/python-api-full-gate.json", 13385, ("exposed", "restricted", "not_implemented")),
("workbench-gui", "docs/p6/workbench-command-gate.json", 778, ("native_handler", "restricted", "not_implemented")),
("rendering-interaction", "docs/p6/view-interaction-gate.json", None, ()),
("quality-release", "docs/p6/quality-gate.json", None, ()),
]
areas: list[dict] = []
evidence: list[dict] = [audit_artifact]
for spec in area_specs:
item, artifact = area(*spec)
areas.append(item)
evidence.append(artifact)
task_counts = Counter(state.get("tasks", {}).values())
fpa_tasks = {
key: value
for key, value in state.get("tasks", {}).items()
if key.startswith("FW-P6-FPA-")
}
blocked_tasks = sorted(key for key, value in fpa_tasks.items() if value == "blocked")
planned_recovery_tasks = sorted(
key for key, value in fpa_tasks.items() if value == "planned" and ("-R" in key or "-FEM" in key or "-MESH" in key)
)
unresolved = sum(int(item["unresolvedRecords"]) for item in areas)
boundary_count = sum(len(item["unapprovedBoundaries"]) for item in areas)
all_terminal = all(bool(item["allRecordsTerminal"]) for item in areas)
parity_ready = unresolved == 0 and boundary_count == 0 and all_terminal and not blocked_tasks
result = "passed" if parity_ready else "pending"
report = {
"schema": "freecad-web-full-parity-gate/v1",
"taskId": "FW-P6-FPA-Y",
"generatedAt": datetime.now(timezone.utc).isoformat(),
"sourceRevision": source_revision(),
"result": result,
"parityReady": parity_ready,
"rule": "Full Parity requires every inventory record to have an implemented native conclusion and zero unapproved boundaries; module completed flags are not sufficient.",
"baseline": audit.get("baseline", {}),
"areas": areas,
"summary": {
"areas": len(areas),
"areasPassingSourceGate": sum(item["gateResult"] in {"pass", "pass_with_boundaries"} for item in areas),
"areasPendingFullParity": sum(item["status"] != "passed" for item in areas),
"unresolvedRecords": unresolved,
"unapprovedBoundaryCount": boundary_count,
"allRecordsTerminal": all_terminal,
},
"ledger": {
"activeTask": state.get("active_task"),
"fpaTaskCounts": dict(task_counts),
"blockedTasks": blocked_tasks,
"plannedRecoveryTasks": planned_recovery_tasks,
},
"evidence": evidence,
"boundaries": [
"This aggregate does not replace detailed per-record native evidence.",
"passed_boundary, restricted, not_implemented and blocked dispositions remain unresolved for Full Parity until an explicit approved boundary policy exists.",
"Fixed Google Chrome evidence is not extended to other browsers or desktop Qt behavior.",
],
}
out_json = P6 / "full-parity-gate.json"
out_md = P6 / "full-parity-gate.md"
out_log = DOCS / "freecad-p6-full-parity-y.log"
out_json.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
lines = [
"# FreeCAD Web Full Parity Gate",
"",
f"- 生成时间:`{report['generatedAt']}`",
f"- 源码 revision:`{report['sourceRevision']}`",
f"- 结论:`{result}`;`parityReady={str(parity_ready).lower()}`。",
"- 判据:每条 inventory 记录必须有已实现的 native 结论,且未批准边界必须为 0;模块 `completed` 不能替代逐条证据。",
"",
"## 覆盖汇总",
"",
f"- 区域:{len(areas)};源码 gate 通过/带边界:{report['summary']['areasPassingSourceGate']};Full Parity 仍待处理:{report['summary']['areasPendingFullParity']}。",
f"- 未解决记录:{unresolved};未批准边界:{boundary_count};记录终态完整:`{str(all_terminal).lower()}`。",
"",
"| 区域 | Gate | 记录 | 未解决 | 未批准边界 | 结论 |",
"|---|---|---:|---:|---:|---|",
]
for item in areas:
records = f"{item['terminalRecords']}/{item['inventoryRecords']}" if item["inventoryRecords"] is not None else "n/a"
lines.append(f"| `{item['id']}` | `{item['gateResult']}` | {records} | {item['unresolvedRecords']} | {len(item['unapprovedBoundaries'])} | `{item['status']}` |")
lines.extend([
"",
"## 账本阻断",
"",
f"- active task:`{state.get('active_task')}`",
f"- blocked FPA tasks:{', '.join(f'`{item}`' for item in blocked_tasks) if blocked_tasks else 'none'}",
f"- planned recovery tasks:{', '.join(f'`{item}`' for item in planned_recovery_tasks) if planned_recovery_tasks else 'none'}",
"",
"## 证据",
"",
])
for item in evidence:
lines.append(f"- `{item['path']}` SHA-256 `{item['sha256']}`")
lines.extend([
"",
"## 边界",
"",
"- `passed_boundary`、`restricted`、`not_implemented` 和 `blocked` 均保留为未解决项,不升级为 Full Parity。",
"- 详细逐条记录和 native dispatcher 证据仍以各 area gate 为准;本聚合不生成几何、API 或浏览器结果。",
"",
"## 自动续接状态",
"",
"- `nextTask`: Y gate 保持 `pending`,先由恢复卡处理未解决记录,再重新运行本 gate。",
"- `nextAction`: 优先执行账本中的 D1/D2 blocked family recovery;不得声明 Full Parity。",
"- `contextRotation`: 结果已落盘;轮换后只读取新 manifest 的 `mode=read` 文件。",
"- `resumeCommand`: `python3 tools/check_continuation_state.py && python3 tools/validate_web_context.py && python3 tools/prepare_web_task_context.py`",
])
out_md.write_text("\n".join(lines) + "\n", encoding="utf-8")
out_log.write_text(
json.dumps(
{
"result": result,
"parityReady": parity_ready,
"unresolvedRecords": unresolved,
"unapprovedBoundaryCount": boundary_count,
"blockedTasks": blocked_tasks,
"evidenceCount": len(evidence),
},
ensure_ascii=False,
)
+ "\n",
encoding="utf-8",
)
print(json.dumps({"result": result, "summary": report["summary"], "blockedTasks": blocked_tasks}, ensure_ascii=False))
return 0
if __name__ == "__main__":
raise SystemExit(main())