#!/usr/bin/env python3 """Capture current registration evidence; successful export is not acceptance.""" import argparse from collections import Counter import hashlib import json import os from pathlib import Path import re import subprocess ROOT = Path(__file__).resolve().parents[1] MAPPING = 'docs/plans/original-completion-2026-09-29/execution/original_facility_mapping.json' BASELINE = 'docs/plans/original-completion-2026-09-29/execution/baseline-fetched-2026-09-29.txt' def manifest(): paths = [ROOT / MAPPING, ROOT / BASELINE, ROOT / 'tools/capture_facility_audit.gd', Path(__file__)] for directory in ('autoload', 'scripts', 'scenes', 'data'): paths.extend(p for p in (ROOT / directory).rglob('*') if p.suffix in ('.gd', '.tres', '.tscn', '.json')) return {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(set(paths))} def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--godot', default='godot') parser.add_argument('--output', required=True) args = parser.parse_args() out = Path(args.output).resolve() out.mkdir(parents=True, exist_ok=True) def save(name, value): (out / name).write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n') before = manifest() save('source-start.json', before) env = dict(os.environ, THE_TREE_FACILITY_AUDIT_PATH=str(out / 'runtime-facilities.json')) with (out / 'export.log').open('w') as log: result = subprocess.run([args.godot, '--headless', '--path', str(ROOT), '--script', 'res://tools/capture_facility_audit.gd'], env=env, stdout=log, stderr=subprocess.STDOUT, timeout=120) after = manifest() changes = [p for p in sorted(before.keys() | after.keys()) if before.get(p) != after.get(p)] save('source-end.json', after) save('source-change.json', changes) if result.returncode or changes: raise SystemExit('Audit capture failed or source changed; inspect logs.') data = json.loads((out / 'runtime-facilities.json').read_text()) mapping = json.loads((ROOT / MAPPING).read_text()) rows = data['original_facilities'] if len(rows) != 30 or len({r['name'] for r in rows}) != 30 or [r['name'] for r in rows] != [r['name'] for r in mapping]: raise SystemExit('Original mapping coverage is invalid.') counts = Counter() mismatches = [] for row in rows: definition = row['definition'] row['registration_status'] = ('normal' if row['runtime_registered'] and row['palette_listed'] else 'definition_only' if definition else 'missing') counts[row['registration_status']] += 1 row['full_acceptance'] = 'unproven' if definition: expected = [int(x) for x in row['original_size'].split('x')] layer = re.match(r'FRONT|BACK', row['original_layer_zone']).group() if definition['footprint'] != expected or definition['layer'] != layer: mismatches.append(row['name']) extra = sorted(set(data['normal_registry_ids']) - {r['definition_id'] for r in rows}) unspecified = [r['name'] for r in rows if r['definition'] and r['definition']['customer_capacity'] < 0] summary = dict(original_count=30, registration_counts=dict(counts), normal_catalog_count=len(data['normal_registry_ids']), non_original_registry_ids=extra, dimension_or_layer_mismatches=mismatches, customer_limit_unset=unspecified, all_original_acceptance=False, scope='Registration and loaded definition values only. Not operation, save migration, assets, Windows or whole-spec acceptance.') save('summary.json', summary) save('classified-facilities.json', rows) labels = {'normal': '通常登録・パレット掲載', 'definition_only': '定義のみ', 'missing': '対応定義なし'} lines = ['# 原本30施設・現行登録監査', '', f"通常登録 {counts['normal']}、定義のみ {counts['definition_only']}、対応定義なし {counts['missing']}。通常登録全{len(data['normal_registry_ids'])}件のうち原本30施設以外は{len(extra)}件。", '', '登録状態と読込済み定義の監査。実営業・保存移行・完成資産・Windows・全要件の受入完了は証明しない。', '', f'対応定義の寸法・層の不一致: {len(mismatches)}件。人数専用上限が未設定: {len(unspecified)}件。capacityから人数値を転記していない。高層・背面・騒音・サービス条件は別途受入が必要。', '', '| 原本 | 施設 | ID | 登録 | 寸法 / 層 / ゾーン | 顧客人数上限 |', '|---|---|---|---|---|---|'] for r in rows: d = r['definition'] geometry = f"{d['footprint'][0]}×{d['footprint'][1]} / {d['layer']} / {d['zone']}" if d else '未定義' limit = str(d['customer_capacity']) if d and d['customer_capacity'] >= 0 else '未設定' lines.append(f"| {r['section']} | {r['name']} | {r['definition_id'] or '—'} | {labels[r['registration_status']]} | {geometry} | {limit} |") lines += ['', '## 未接続施設', '', '対応定義なし: ' + '、'.join(r['name'] for r in rows if r['registration_status']=='missing') + '。', '', '定義のみ: ' + '、'.join(r['name'] for r in rows if r['registration_status']=='definition_only') + '。', '', '## 次の受入', '', '未定義施設は寸法・層だけでなく経済値、人数、業務、通常操作、保存を接続する。登録済み施設は未設定人数とサービス属性、営業時間・停止・経路・客/職員行動を個別検証する。施設追加のたびにこの監査を再実行する。', '', '再現: `python3 tools/run_facility_audit.py --godot /opt/homebrew/bin/godot --output artifacts/facility-audit-current-2026-09-29`。原本SHA・入力ソースSHA・取得中の変更なしは同ディレクトリのsource-start/end/change.jsonを参照。', ''] (out / 'REPORT.md').write_text('\n'.join(lines)) print(json.dumps(summary, ensure_ascii=False)) if __name__ == '__main__': main()