P13-03: 内容差異の記録

This commit is contained in:
Agent Control Runner
2026-10-15 19:39:35 +09:00
parent bc1299acf5
commit bd3a97ec74
5 changed files with 160 additions and 64 deletions
+130 -34
View File
@@ -12,6 +12,7 @@ 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/'
@@ -20,6 +21,8 @@ MAPPING = 'docs/plans/original-completion-2026-09-29/execution/original_facility
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')
@@ -33,12 +36,10 @@ CATEGORIES = {
}
INTEGRATION_NOTES = [
'Read-only integration audit: contract base e1a1651e15bbc9a0526ef31d0b2786feb20d8cd1 versus main 57131ec. This is a future refresh instruction, not a relabeling of any recorded source/game/asset hash.',
'autoload/simulation_runtime.gd adds preview_build_facility and extracts _build_facility_precheck shared by preview and execution. scenes/main/world_preview.gd routes previews through Runtime with a fallback and adds demolition mode. scenes/main/main.gd adds demolition confirmation/command handling, preview error messages and stricter interface-selection version checks.',
'At normal integration Root must refresh normal_entry observations for original_facility_13, original_facility_14, original_facility_15, original_facility_16, original_facility_24, original_facility_25 and original_facility_26 against the integrated main.gd and preview path; refresh runtime observations against the integrated Runtime. Refresh event_state and save search evidence too because their Runtime source hash changes, even though these changed hunks do not establish dedicated original facility events or save round-trips. Recheck mapping/catalog/definitions without promoting missing identities or black_market eligibility.',
'The selected test sources in this ledger, facility catalog, event service and save service are unchanged between these commits. Added tests/transport/test_runtime_build_preview.gd and tests/ui/test_p08_normal_placement_ops.gd concern generic preview/placement/demolition; tests/ui/test_selection_departure.gd and tests/ui/fixtures/selection_cross_process.gd concern selection restoration. Root should assess their bounded relevance when refreshing each facility test observation; none accepts original content. Guest observation selection changes and P08 migration fixtures likewise provide no original facility content acceptance.',
'Minimal handling: retain this base-version evidence, explicitly re-search changed integrated source paths and rebuild affected technical observations, source pins and output receipt as a reviewed integration refresh; retain all unknown/unverified content states and the 30/88 denominator. Do not merely replace old hashes or claim old tests ran against new code. No rebase, overwrite or Git write is performed here.',
'If P13 integrates first, P23 9a full tools map gains tools/check_p13_content_difference.py. P23 proof remains bound to its own version and requires future final source verification; this audit does not extend that proof.',
'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.',
]
@@ -67,27 +68,59 @@ def load(path):
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 = raw(path).decode('utf-8').splitlines()
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({'path': path, 'sha256': sha(raw(path)),
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 = subprocess.check_output(['git', 'show', BASE + ':' + path], cwd=ROOT)
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})
@@ -139,7 +172,7 @@ def expected():
'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 baseline/infrastructure/rescue/original paths, not expansion paths at this load site. Generic services do not prove original special behavior.'),
'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'],
@@ -171,7 +204,7 @@ def expected():
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 += [{'path': p, 'sha256': sha(raw(p)), 'applicability': 'current_checkout',
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'],
@@ -231,7 +264,7 @@ def compact(document):
if signature not in evidence_ids:
identity = 'evidence_%04d' % (len(evidence) + 1)
evidence_ids[signature] = identity
provenance = {k: record[k] for k in ('path', 'sha256', 'scope')}
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']}),
@@ -270,9 +303,11 @@ def expand(document):
return visit(document)
def negative_checks(canonical):
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),
@@ -301,27 +336,91 @@ def negative_checks(canonical):
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')
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(load(args.ledger), canonical)
receipt = load(OUT + 'validation.json')
if receipt != receipt_expected():
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')
verify_records(args.ledger, canonical)
result = {'record_integrity': 'pass', 'content_acceptance': 'not_claimed',
'formal_review': 'external_runner_required'}
result['negative_checks'] = negative_checks(canonical)
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:
@@ -329,28 +428,25 @@ def main():
return 1
def receipt_expected():
ledger = expand(load(OUT + 'difference-ledger.json'))
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(p)) for p in [OUT + 'difference-ledger.json',
'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': 'blocked', 'formal_tests': 'not_run', 'formal_claude_review': 'not_run',
'worker_local_checks': {
'python3_B_integrity_and_mandatory_negatives': 'pass',
'ast_parse': 'pass',
'registered_argv': 'blocked: python executable is unavailable in this worker environment',
'git_diff_check': 'pass',
'acceptance': 'not_claimed'},
'command': 'python -B tools/check_p13_content_difference.py --verify-only',
'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 frozen base Git blob and current checkout are separately identified.',
'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'],
'worker_tests_run': True,
'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']}}