#!/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()