Files
FreeCADWasmDev/tools/report_native_layer_gaps.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

88 lines
6.1 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
"""Describe every unresolved route from existing static source metadata; no runtime validation."""
import json
import hashlib
from collections import Counter, defaultdict
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "docs/current/feature-capability-registry.json"
OUTPUT = ROOT / "docs/current/native-layer-gap-inventory.json"
def main():
registry = json.loads(SOURCE.read_text(encoding="utf-8"))
baseline_path = ROOT / "docs/current/native-layer-gap-baseline-20260912.json"
if not baseline_path.exists() and OUTPUT.exists():
baseline_path.write_bytes(OUTPUT.read_bytes())
baseline = json.loads(baseline_path.read_text(encoding="utf-8")) if baseline_path.exists() else {}
decisions_path = ROOT / "docs/current/native-semantic-decisions.json"
decisions = {}
if decisions_path.exists():
for group in json.loads(decisions_path.read_text(encoding="utf-8")).get("groups", []):
sources = group.get("sourceDigests", {})
current = bool(sources) and all((ROOT / name).resolve().is_relative_to(ROOT)
and (ROOT / name).is_file() and hashlib.sha256((ROOT / name).read_bytes()).hexdigest() == expected
for name, expected in sources.items())
for record_id, finding in group.get("records", {}).items():
decisions[record_id] = {**finding, "sourceSnapshotCurrent": current}
by_id = {row["id"]: row for row in registry["records"]}
baseline_ids = {row["id"] for row in baseline.get("records", [])}
baseline_progress = {"total": len(baseline_ids),
"registeredNow": sum(by_id.get(key, {}).get("implementationStatus") == "routed" for key in baseline_ids),
"sourceDecisionsCurrent": sum(decisions.get(key, {}).get("sourceSnapshotCurrent") is True for key in baseline_ids),
"sourceReviewRemaining": sum(decisions.get(key, {}).get("sourceSnapshotCurrent") is not True for key in baseline_ids)}
rows = []
modules = defaultdict(Counter)
for row in registry.get("records", []):
if row.get("implementationStatus") == "routed":
continue
for gap in row.get("implementationGaps", []):
kind = gap.get("classification") or gap.get("stage") or "unclassified"
modules[row["module"]][kind] += 1
rows.append({"id": row["id"], "module": row["module"], "command": row.get("command"),
"desktopSource": row.get("sourcePath"), "desktopLine": row.get("sourceLine"),
"requirement": "retained", "implementationStatus": row.get("implementationStatus"),
"semanticDecision": decisions.get(row["id"]),
"gaps": row.get("implementationGaps", [])})
kinds = Counter(gap.get("classification") or gap.get("stage") or "unclassified"
for row in rows for gap in row["gaps"])
payload = {"schema": "freecad-web-native-gap-inventory/v1", "sourceGeneratedAt": registry.get("generatedAt"),
"scope": "All unresolved feature-capability records; static source anchors only, not a semantic completeness audit.",
"businessUnnecessaryDeterminations": 0, "automaticValidationRun": False,
"baselineProgress": baseline_progress,
"counts": {"featureTotal": len(registry["records"]), "unresolved": len(rows), "byKind": dict(kinds)},
"modules": dict(modules), "records": rows}
OUTPUT.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
lines = ["# 原生层缺口逐项清单", "", f"登记快照:{payload['sourceGeneratedAt']}。未接通 {len(rows)} 项。",
"", f"固定基线 {baseline_progress['total']} 项:当前已登记 {baseline_progress['registeredNow']} 项;有效源码结论 {baseline_progress['sourceDecisionsCurrent']} 项;待继续源码归因 {baseline_progress['sourceReviewRemaining']} 项。已登记不等于用户已验证。",
"", "本清单保留全部业务,没有任何一项被判为业务不需要。源码候选仅用于定位,不能证明业务已实现;",
"Probe 中未找到入口不代表本机没有源码。未运行测试或业务验证。", "",
"## 模块分布", "", "| 模块 | 标记拒绝 | 有源码候选未登记 | Probe 未定位入口 | 策略/其他 |",
"| --- | ---: | ---: | ---: | ---: |"]
for module, counts in sorted(modules.items()):
rejected = counts['registration-marker-rejected']
candidates = counts['native-source-candidate-unregistered']
missing = counts['native-entry-not-located-in-probe-directory']
lines.append(f"| {module} | {rejected} | {candidates} | {missing} | {sum(counts.values()) - rejected - candidates - missing} |")
lines.extend(["", "## 每项来源与下一步", ""])
for row in rows:
lines.extend([f"### {row['id']}", "", f"桌面来源:`{row['desktopSource']}:{row['desktopLine']}`;需求保留。"])
if row.get('semanticDecision'):
decision = row['semanticDecision']
lines.extend([f"- 源码结论:{decision['classification']};指纹仍匹配:{decision['sourceSnapshotCurrent']}",
f"- 发现:{decision['finding']}", f"- 实施下一步:{decision['nextAction']}"])
for gap in row['gaps']:
lines.append(f"- 分类:`{gap.get('classification') or gap.get('stage')}`;{gap.get('reason', '')}")
for candidate in gap.get('sourceCandidates', []):
source = ROOT / candidate['path']
lines.append(f"- 原生候选:[{source.name}:{candidate['line']}]({source}:{candidate['line']})")
lines.append(f"- 下一步:{gap.get('repair') or 'Inspect the existing route policy and the supplied dependency closure; do not remove the guard without connecting the real implementation.'}")
lines.append("")
OUTPUT.with_suffix('.md').write_text('\n'.join(lines), encoding='utf-8')
print(json.dumps({**payload['counts'], "baselineProgress": baseline_progress}, ensure_ascii=False))
if __name__ == '__main__':
main()