Files
TheTree_mock/tools/check_p13_content_difference.py
T

495 lines
30 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
"""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())