#!/usr/bin/env python3 """Verify P13-03 records only. Never writes receipts or accepts game content. Runner: python -B tools/check_p13_content_difference.py --verify-only Optional --ledger permits read-only verification of a candidate ledger. """ import argparse import copy import hashlib import json from pathlib import Path import re import subprocess import sys from unittest.mock import patch ROOT = Path(__file__).resolve().parents[1] OUT = 'docs/plans/original-completion-2026-10-02/execution/p13-03/' PLAN = 'docs/plans/original-completion-2026-10-02/' MAPPING = 'docs/plans/original-completion-2026-09-29/execution/original_facility_mapping.json' SLOTS = PLAN + 'asset_inventory_reaudit/facility_usage_slots.json' SPEC = 'docs/spec/acceptance/P13-03.json' BASE = 'e1a1651e15bbc9a0526ef31d0b2786feb20d8cd1' MAIN = '57131ec616ba06a71d3d97eaea738f209f965349' TARGET_PATHS = ('autoload/simulation_runtime.gd', 'scenes/main/main.gd') SPEC_RAW = '917cf1504bbfc700e0da42b2db496b42486fe3c36c680f301f58cf34c95267b6' SPEC_RUNNER = 'e1255835c1227e02bac42eb99b026b0fcbe69c272f9ed0b36199a2daf8d55c53' VIEWS = ('normal_entry', 'runtime', 'event_state', 'save', 'test') CATEGORIES = { 'event': ['B-1.6', 'B-2.5', 'B-8.2', 'B-8.3'], 'overlay': ['B-2.5'], 'history': ['B-1.6'], 'story': ['B-1.6'], 'voice': ['B-1.6'], 'scene': ['B-1.6'], } INTEGRATION_NOTES = [ 'The two target sources are immutable main Git blobs at ' + MAIN + '. Matches are re-searched from those bytes with main_git_blob provenance.', 'Read-only Git ancestry determines applicability: descendant HEAD requires working files equal to main blobs; non-descendant HEAD requires base blobs. Git errors reject. Report flags cannot select a mode.', 'Other current-checkout checks remain strict; work packages remain frozen at the contract base. All unknown/unverified states and 30/88 accounting remain unchanged.', 'Formal Runner validation and separate independent review remain external requirements; static observations confer no content completion credit.', ] def raw(path): p = Path(path) if p.is_absolute() or '..' in p.parts: raise ValueError('source must be checkout-relative') resolved = (ROOT / p).resolve() if not resolved.is_relative_to(ROOT): raise ValueError('source escapes checkout') return resolved.read_bytes() def sha(data): return hashlib.sha256(data).hexdigest() def load(path): def unique(pairs): result = {} for key, value in pairs: if key in result: raise ValueError('duplicate JSON key: ' + key) result[key] = value return result return json.loads(raw(path), object_pairs_hook=unique) def git_blob(commit, path): return subprocess.check_output(['git', 'show', commit + ':' + path], cwd=ROOT) def main_is_ancestor(): result = subprocess.run(['git', 'merge-base', '--is-ancestor', MAIN, 'HEAD'], cwd=ROOT, capture_output=True) if result.returncode not in (0, 1): raise ValueError('Git ancestry check failed: ' + result.stderr.decode(errors='replace')) return result.returncode == 0 def check_target_sources(): commit = MAIN if main_is_ancestor() else BASE for path in TARGET_PATHS: if raw(path) != git_blob(commit, path): raise ValueError('stale working source: ' + path) def source_bytes(path): return git_blob(MAIN, path) if path in TARGET_PATHS else raw(path) def provenance(path): record = {'path': path, 'sha256': sha(source_bytes(path)), 'applicability': 'main_git_blob' if path in TARGET_PATHS else 'current_checkout'} if path in TARGET_PATHS: record['commit'] = MAIN return record def observation(paths, tokens, note): """Exact token matches are search observations, never accepted links.""" evidence = [] for path in paths: lines = source_bytes(path).decode('utf-8').splitlines() matches = [{'line': i + 1, 'text': line} for i, line in enumerate(lines) if any(token in line for token in tokens)] evidence.append({**provenance(path), 'searched_tokens': tokens, 'matches': matches, 'scope': 'Static search of this file only; no execution or acceptance.'}) return {'status': 'unverified', 'accepted': False, 'note': note, 'evidence': evidence} def expected(): check_target_sources() if sha(raw(SPEC)) != SPEC_RAW or sha(raw(SPEC).decode().strip().encode()) != SPEC_RUNNER: raise ValueError('specification hash mismatch') spec = load(SPEC) sources = [] for path, pin in spec['source_pins'].items(): if path == PLAN + 'work_packages.json': frozen = git_blob(BASE, path) if sha(frozen) != pin: raise ValueError('frozen work package pin mismatch') sources.append({'path': path, 'sha256': pin, 'applicability': 'base_git_blob', 'commit': BASE}) sources[-1]['scope'] = 'Frozen contract input. Live plan state is maintained separately and is not content completion evidence.' else: if sha(raw(path)) != pin: raise ValueError('stale required source: ' + path) sources.append({'path': path, 'sha256': pin, 'applicability': 'current_checkout'}) mapping = load(MAPPING) slots = load(SLOTS) if len({s['slot_id'] for s in slots}) != len(slots): raise ValueError('duplicate source slot IDs') definitions = sorted(p.relative_to(ROOT).as_posix() for p in (ROOT / 'data/facilities').rglob('*.tres')) definitions_text = {p: raw(p).decode() for p in definitions} catalog = 'scripts/data/facility_catalog.gd' entry = 'scenes/main/main.gd' runtime = 'autoload/simulation_runtime.gd' event = 'scripts/events/event_service.gd' save = 'scripts/core/save_service.gd' test = 'tests/s6/test_runtime_snapshot_serialization.gd' search_paths = [catalog] + definitions facilities = [] for index, original in enumerate(mapping, 1): if original['section'] not in ('10.4', '10.6'): continue identity = 'original_facility_%02d' % index # Names and original stable definition IDs are the bounded search basis. tokens = [original['name']] if original['definition_id']: tokens.append(original['definition_id']) matching = [p for p, text in definitions_text.items() if any(t in text for t in tokens)] ids = [] for path in matching: found = re.search(r'^id = &"([^"]+)"', definitions_text[path], re.M) if found: ids.append(found.group(1)) # No mana_control_room or generic proxy may fill an original identity. if 'mana_control_room' in ids: raise ValueError('proxy mapping is prohibited') facility_slots = [s for s in slots if s['facility_name'] == original['name']] facilities.append({ 'original_id': identity, 'original': original, 'mapping_observation': observation(search_paths, tokens, 'Current catalog and all data/facilities definitions searched. No match means missing in this bounded set; historical null is not treated as proof.'), 'current_definition_ids': ids, 'current_definition_paths': matching, 'connections': { 'normal_entry': observation([entry, catalog], tokens, 'adult_private_room is in the UI list and baseline catalog; black_market is expansion-only. No other original identity match. Static eligibility only; actual construction is unverified.'), 'runtime': observation([runtime, catalog] + matching, tokens + ['FacilityCatalog.FACILITY_PATHS'], 'Runtime loads ORIGINAL_FACILITY_PATHS, including some files under the expansion directory. It does not load EXPANSION_FACILITY_PATHS or black_market. Generic services do not prove original special behavior.'), 'event_state': observation([runtime, event], tokens, 'No facility-specific transition established by this search. Generic EventService is not a dedicated content catalog.'), 'save': observation([runtime, save], tokens + ['event_service.create_snapshot()', 'save_snapshot'], 'Generic snapshot plumbing is a technical lead only. Facility event round-trip and original semantics remain unverified.'), 'test': observation([test, 'tests/s2/test_facility_service.gd', 'tests/s4/test_runtime_relationship_events.gd'], tokens, 'Selected generic suite sources searched; no formal suite run or dedicated original facility acceptance inferred.')}, 'media_usage_slots': [{'slot_id': s['slot_id'], 'source_path': SLOTS, 'source_sha256': sha(raw(SLOTS)), 'source_record': s, 'status': 'unverified_required_slot', 'approved_asset': None, 'content_complete': False} for s in facility_slots], 'actual_required_asset_count': None, 'actual_required_asset_count_status': 'unknown', 'completed_asset_acceptance': 'unverified', 'original_content_completion': 'unverified', 'technical_as_content_credit': False}) if len(facilities) != 7: raise ValueError('original facility coverage must be seven') nonfacility = [] for category, requirements in CATEGORIES.items(): nonfacility.append({'difference_id': 'P13-03.' + category, 'category': category, 'requirement_ids': requirements, 'authority_path': spec['authority'], 'authority_sha256': sha(raw(spec['authority'])), 'related_original_sections': ['1.6', '2.5', '8.2', '8.3', '10.4', '10.6', '13', '14'], 'content_ids': None, 'content_ids_status': 'unknown', 'quantity': None, 'quantity_status': 'unknown', 'completed_asset_acceptance': 'unverified', 'original_content_completion': 'unverified', 'technical_as_content_credit': False, 'decisions': ['D08', 'D09'], 'difference': 'Required original presentation/content is not accepted by generic state processing. Final catalog, variants, production quantities and ownership remain unknown.', 'technical_observation': observation([event, 'scripts/events/event_definition.gd', 'scripts/ui/event_log_dialog.gd'], [category], 'Neutral metadata search only; matches are not finished media, history semantics, story, voice or scenes.')}) additional = sorted(set(search_paths + [entry, runtime, event, save, test, 'tests/s2/test_facility_service.gd', 'tests/s4/test_runtime_relationship_events.gd', 'scripts/events/event_definition.gd', 'scripts/ui/event_log_dialog.gd'])) sources += [{**provenance(p), 'scope': 'Bounded static observation; no executed connection/content acceptance.'} for p in additional] return {'schema_version': 1, 'task_id': 'P13-03', 'acceptance_ids': ['P13-03'], 'requirement_ids': spec['package_requirement_ids'], 'validation_ids': ['p13-difference'], 'base_commit': BASE, 'spec_raw_sha256': SPEC_RAW, 'spec_runner_text_sha256': SPEC_RUNNER, 'scope': spec['scope'], 'sources': sources, 'facilities': facilities, 'nonfacility_differences': nonfacility, 'unknowns': {'D08': 'unknown', 'D09': 'unknown', 'final_content_ids': None, 'final_content_quantity': None, 'final_actual_asset_count': None}, 'accounting': {'packages': 30, 'delivery_units': 88, 'no_partial_credit': True, 'package_completion_inferred': False, 'original_views_edited': False, 'content_completion_inferred': False, 'technical_as_content_credit': False}, 'asset_policy': 'Usage slots are not image counts. Fallback, Kenney pack, mockup, candidate token, file existence and technical tests confer no content credit.', 'test_applicability': {name: {'status': 'not_applicable', 'reason': 'Records-only change; no product code, assets, save schema or values changed. No game/platform acceptance is claimed.'} for name in ['runtime', 'native', 'Windows', 'engine', 'full_game_regression']}, 'formal_validation': 'pending_runner', 'formal_claude_review': 'pending_runner', 'integration_gate': 'Required p13-difference validator and separate formal Claude review must pass before integration.'} def verify(candidate, canonical): # Compare every field, including pending/unknown fields: these must stay # explicit and are not skipped as if they were content PASS. if json.dumps(candidate, sort_keys=True, ensure_ascii=False) != json.dumps(canonical, sort_keys=True, ensure_ascii=False): raise ValueError('coverage, source, observation, unknown or accounting mismatch') def compact(document): """Lossless shared long-string table, with exact checked references.""" strings, identities = {}, {} evidence, evidence_ids = {}, {} contexts, context_ids = {}, {} def context(record): signature = json.dumps(record, sort_keys=True, ensure_ascii=False) if signature not in context_ids: identity = 'context_%04d' % (len(contexts) + 1) context_ids[signature] = identity contexts[identity] = visit(record) return context_ids[signature] def visit(value): if isinstance(value, str) and len(value) >= 64: if value not in identities: identity = 'text_%04d' % (len(strings) + 1) identities[value] = identity strings[identity] = value return {'text_ref': identities[value]} if isinstance(value, list): return [visit(item) for item in value] if isinstance(value, dict): result = {} for key, item in value.items(): if key == 'evidence': refs = [] for record in item: signature = json.dumps(record, sort_keys=True, ensure_ascii=False) if signature not in evidence_ids: identity = 'evidence_%04d' % (len(evidence) + 1) evidence_ids[signature] = identity provenance = {k: v for k, v in record.items() if k not in ('searched_tokens', 'matches')} evidence[identity] = { 'source_ref': context(provenance), 'search_ref': context({'searched_tokens': record['searched_tokens']}), 'matches': visit(record['matches'])} refs.append({'evidence_ref': evidence_ids[signature]}) result[key] = refs else: result[key] = visit(item) return result return value result = visit(document) result['schema_version'] = 2 result['shared_text'] = strings result['shared_evidence'] = evidence result['shared_context'] = contexts return result def expand(document): def visit(value): if isinstance(value, dict): if set(value) == {'text_ref'}: return document['shared_text'][value['text_ref']] if set(value) == {'evidence_ref'}: return visit(document['shared_evidence'][value['evidence_ref']]) if set(value) == {'source_ref', 'search_ref', 'matches'}: return {**visit(document['shared_context'][value['source_ref']]), **visit(document['shared_context'][value['search_ref']]), 'matches': visit(value['matches'])} return {key: visit(item) for key, item in value.items() if key not in ('shared_text', 'shared_evidence', 'shared_context')} if isinstance(value, list): return [visit(item) for item in value] return value return visit(document) def negative_checks(canonical, ledger_path=OUT + "difference-ledger.json"): cases = {} mutations = { 'tampered_ancestry_flag': lambda d: d.update(main_is_ancestor=False), 'tampered_report_flag': lambda d: d.update(source_mode='legacy_fallback'), 'facility_omission': lambda d: d['facilities'].pop(), 'slot_omission': lambda d: d['facilities'][0]['media_usage_slots'].pop(), 'stale_pin': lambda d: d['sources'][0].update(sha256='0' * 64), 'duplicate_facility': lambda d: d['facilities'].append(copy.deepcopy(d['facilities'][0])), 'duplicate_slot': lambda d: d['facilities'][0]['media_usage_slots'].append(copy.deepcopy(d['facilities'][0]['media_usage_slots'][0])), 'false_content_completion': lambda d: d['facilities'][0].update(original_content_completion='completed'), 'false_asset_acceptance': lambda d: d['facilities'][0]['media_usage_slots'][0].update(approved_asset='fallback.png', content_complete=True), 'technical_as_content': lambda d: d['accounting'].update(technical_as_content_credit=True), 'unknown_to_zero': lambda d: d['unknowns'].update(final_content_quantity=0), 'nonfacility_omission': lambda d: d['nonfacility_differences'].pop(), 'proxy_mapping': lambda d: d['facilities'][-1].update(current_definition_ids=['mana_control_room']), 'connection_promotion': lambda d: d['facilities'][0]['connections']['runtime'].update(accepted=True), 'false_runner_pass': lambda d: d.update(formal_validation='pass'), 'missing_shared_text': lambda d: d['shared_text'].pop(next(iter(d['shared_text']))), 'missing_shared_evidence': lambda d: d['shared_evidence'].pop(next(iter(d['shared_evidence']))), 'missing_shared_context': lambda d: d['shared_context'].pop(next(iter(d['shared_context']))), 'altered_shared_text': lambda d: d['shared_text'].update({next(iter(d['shared_text'])): 'omitted'}), 'dangling_reference': lambda d: d['facilities'][0]['connections']['runtime'].update(note={'text_ref': 'missing'}), } for name, mutate in mutations.items(): changed = copy.deepcopy(canonical) mutate(changed) try: verify(changed, canonical) except ValueError: cases[name] = 'rejected' else: raise ValueError('negative case was accepted: ' + name) # Inject source bytes and Git results only in memory; no product/Git writes. real_raw, real_blob = raw, git_blob snapshots = {(commit, path): real_blob(commit, path) for commit in (BASE, MAIN) for path in TARGET_PATHS} def rejected(name, operation): try: operation() except (ValueError, OSError, subprocess.CalledProcessError): cases[name] = 'rejected' else: raise ValueError('negative case was accepted: ' + name) for descendant in (False, True): commit = MAIN if descendant else BASE label = 'merged' if descendant else 'frozen' def working(path): return snapshots[commit, path] if path in TARGET_PATHS else real_raw(path) with patch(__name__ + '.main_is_ancestor', return_value=descendant): with patch(__name__ + '.raw', side_effect=working): verify(compact(expected()), canonical) for target in TARGET_PATHS: def corrupt(path): data = working(path) return data + b'\n# corrupted source' if path == target else data with patch(__name__ + '.raw', side_effect=corrupt): rejected(label + '_stale_' + target, expected) other = BASE if descendant else MAIN def wrong_version(path): return snapshots[other, path] if path in TARGET_PATHS else real_raw(path) with patch(__name__ + '.raw', side_effect=wrong_version): rejected(label + '_wrong_snapshot', expected) def corrupt_blob(version, path): data = real_blob(version, path) return data + b'\n# corrupted blob' if version == MAIN else data with patch(__name__ + '.raw', side_effect=working), patch(__name__ + '.git_blob', side_effect=corrupt_blob): rejected(label + '_corrupt_git_blob', lambda: verify(compact(expected()), canonical)) for result in (subprocess.CompletedProcess([], 2, b'', b'Git failure'), subprocess.CompletedProcess([], -1, b'', b'interrupted')): with patch.object(subprocess, 'run', return_value=result): rejected('ancestry_git_error_' + str(result.returncode), expected) with patch.object(subprocess, 'run', side_effect=OSError('Git unavailable')): rejected('ancestry_git_unavailable', expected) with patch(__name__ + '.git_blob', side_effect=subprocess.CalledProcessError(128, ['git', 'show'])): rejected('blob_git_error', expected) # Exercise the same read-only alternate input path used by the CLI. alternate = OUT + 'in_memory_candidate.json' ledger_bytes = real_raw(ledger_path) def alternate_raw(path): if path == alternate: return ledger_bytes if path == OUT + 'difference-ledger.json': raise ValueError('default ledger must not be read for alternate input') return real_raw(path) with patch(__name__ + '.raw', side_effect=alternate_raw): verify_records(alternate, canonical) cases['alternate_ledger_valid'] = 'verified_read_only' with patch(__name__ + '.raw', side_effect=lambda path: b'{}' if path == alternate else real_raw(path)): rejected('alternate_ledger_corrupt', lambda: verify_records(alternate, canonical)) return cases def verify_records(ledger_path, canonical): verify(load(ledger_path), canonical) if load(OUT + 'validation.json') != receipt_expected(ledger_path): raise ValueError('receipt mismatch or false formal acceptance') if raw(OUT + 'DIFFERENCE_LEDGER.md').decode() != readable(expand(canonical)): raise ValueError('readable ledger differs from machine ledger') def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--ledger', default=OUT + 'difference-ledger.json', help='Read-only checkout-relative alternate ledger; must match canonical records and the registered receipt hash. No output is written.') parser.add_argument('--negative-checks', action='store_true') parser.add_argument('--verify-only', action='store_true', help='Read-only verification; negative checks are always mandatory.') args = parser.parse_args() try: canonical = compact(expected()) verify_records(args.ledger, canonical) result = {'record_integrity': 'pass', 'content_acceptance': 'not_claimed', 'formal_review': 'external_runner_required'} result['negative_checks'] = negative_checks(canonical, args.ledger) print(json.dumps(result, ensure_ascii=False)) return 0 except (ValueError, OSError, KeyError, TypeError, subprocess.CalledProcessError) as error: print(json.dumps({'record_integrity': 'fail', 'error': str(error)})) return 1 def receipt_expected(ledger_path=OUT + 'difference-ledger.json'): ledger = expand(load(ledger_path)) return {'task_id': 'P13-03', 'acceptance_ids': ['P13-03'], 'validation_ids': ['p13-difference'], 'requirement_ids': ledger['requirement_ids'], 'facility_ids': [f['original_id'] for f in ledger['facilities']], 'slot_ids': [s['slot_id'] for f in ledger['facilities'] for s in f['media_usage_slots']], 'difference_ids': [n['difference_id'] for n in ledger['nonfacility_differences']], 'source_applicability_records': ledger['sources'], 'output_pins': {p: sha(raw(ledger_path if p == OUT + 'difference-ledger.json' else p)) for p in [OUT + 'difference-ledger.json', OUT + 'DIFFERENCE_LEDGER.md', 'tools/check_p13_content_difference.py']}, 'status': 'ready_for_external_validation', 'formal_tests': 'external_runner_required', 'formal_claude_review': 'external_independent_review_required', 'worker_history': ['Earlier invocation failed because a literal python alias was unavailable; historical failure is not current readiness or formal acceptance.'], 'command': '{python} -B tools/check_p13_content_difference.py --verify-only', 'purpose': 'Verify record integrity and negative rejection only; not content acceptance.', 'source_applicability': 'Exact IDs, source SHA256 and scopes are in difference-ledger.json. Work package base_git_blob, two main_git_blob sources and other current_checkout sources are separately identified.', 'required_before_integration': ['Runner p13-difference pass', 'separate formal Claude review pass'], 'record_readiness': 'Canonical records prepared; formal validation and review are external.', 'test_applicability': {name: 'Not applicable to records-only changes; no game or platform acceptance.' for name in ['runtime', 'native', 'Windows', 'engine', 'full_game_regression']}} def readable(d): lines = ['# P13-03 内容差異台帳', '', '記録の提出であり、ゲーム内容・P13パッケージの完成認定ではない。原30パッケージ/88成果、部分点なし。原子・状態台帳は変更しない。', '', '通常入口、Runtime、イベント状態、保存、試験はすべて静的観測・未検証。汎用処理は原施設固有の接続や完成素材を証明しない。', '', '機械台帳のshared_text/text_refとshared_evidence/evidence_refは重複証拠の共有参照。検索語、一致行番号・全文、相対出典パス、SHAを省略せず保持し、チェッカーは全参照と本文を照合する。作業計画は契約baseのGit blobを固定入力として検証する。別管理の現行進捗は内容完成の証拠にしない。', '', '施設の原典IDは旧対応表の1始まりの位置を保持する。現行の data/facilities 全定義とカタログを名前・既存IDで再検索した。マナ制御室をマナ炉に代用しない。', '', '|原典ID|章|原典名|現行定義候補|使用スロット数|完成素材・原内容|', '|---|---|---|---|---|---|'] for f in d['facilities']: lines.append('|%s|%s|%s|%s|%d|未検証|' % (f['original_id'], f['original']['section'], f['original']['name'], ', '.join(f['current_definition_ids']) or '対象検索範囲で欠落', len(f['media_usage_slots']))) lines += ['', '風俗個室は通常UIリストとRuntime読込元に候補がある。闇市場は拡張定義のみで通常読込元に含まれない。他の5施設は対象定義集合で対応が見つからない。実行・保存再開・施設固有イベントの受入は未検証。', '', '## 施設別の観測と使用スロット', ''] lines += ['共有観測範囲(各施設の具体的検索結果は機械台帳に保持):', ''] for view in VIEWS: lines += ['- ' + view + ': ' + d['facilities'][0]['connections'][view]['note']] lines += ['', '共有出典・検索語はshared_contextのsource_ref/search_refから復元する。各施設の各観測は個別に参照し、合格接続を意味しない。', ''] for f in d['facilities']: lines += ['### ' + f['original_id'] + ' / ' + f['original']['name'], ''] for view in VIEWS: o = f['connections'][view] lines += ['- ' + view + ': ' + o['status'] + '(検索語・一致行本文・出典SHAは機械台帳参照)'] lines += ['', '使用スロット(すべて unverified_required_slot、approved_asset=null):', ''] lines += ['- `' + s['slot_id'] + '`' for s in f['media_usage_slots']] lines += ['', '実制作資産数はunknown/null。スロット数を画像数に転用しない。', ''] lines += ['## 施設外の内容差異', ''] for n in d['nonfacility_differences']: lines += ['- `' + n['difference_id'] + '` (' + ', '.join(n['requirement_ids']) + '): ' + n['difference']] lines += ['', 'event/overlay/history/story/voice/scene の内容ID・数量・受入はunknownまたは未検証。D08(内容範囲)/D09(品質・量・担当・権利)は未決定。中立メタデータのみを記録し、場面は制作・描写しない。', '', '## 出典・適用範囲', '', '機械台帳に各観測の検索語・一致行・本文・実SHAを保持する。旧対応表は原典識別に利用し、現行対応は再検索で記録する。素材台帳は使用スロットと候補の証拠のみ。inventory_summaryの290 media files、0 audio files、360 slots、0 approved assetsは既存監査の限定集計であり、最終必要量を示さない。', ''] lines += ['- `' + s['path'] + '` / ' + s['applicability'] + ' / SHA256 `' + s['sha256'] + '`' for s in d['sources']] lines += ['', '## 統合監査注記', ''] lines += ['- ' + note for note in INTEGRATION_NOTES] lines += ['', '## 検証・統合条件', '', '`python -B tools/check_p13_content_difference.py --verify-only` は記録整合性と欠落・重複・古いpin・誤完成・技術から内容への昇格・unknownのゼロ化・共有参照改変等の拒否を必ず検証する。--negative-checksも互換引数として保持する。workerのローカル診断は正式受入ではない。Runner正式validatorと別セッションの正式Claudeレビュー合格後にのみ統合できる。', '', 'Runtime/native/Windows/engine/full game regressionは記録だけの変更に適用外(製品コード・資産・保存形式・値の変更なし)。この適用外をゲームの合格に転用しない。Kenney・fallback・mockup・コード候補・ファイル存在・状態試験から完成資産を推定しない。', ''] return '\n'.join(lines) if __name__ == '__main__': sys.exit(main())