P04-01: 同時発生の更新規約

This commit is contained in:
Agent Control Runner
2026-10-16 03:11:58 +09:00
parent 5475391647
commit 64617018ed
6 changed files with 910 additions and 15 deletions
+189 -15
View File
@@ -5,6 +5,7 @@ Required conditions and authority bindings are fixed here, never supplied by an
See execution/p04-01/CHECKER_PROTOCOL.md for the supported input subset.
"""
import argparse
from contextlib import contextmanager
import hashlib
import json
import os
@@ -12,6 +13,7 @@ from pathlib import Path
import re
import subprocess
import sys
import tempfile
ROOT = Path(__file__).resolve().parents[1]
SPEC = ROOT / 'docs/spec/acceptance/P04-01.json'
@@ -20,6 +22,11 @@ DOMAINS = {'閉店', '停電', '破壊', '退出', '課金', 'シフト交代',
OWNER_HASH = '4583ee9aad45c4db183d72df4420df48aa8e0897e3b5efda13016f0b5cdbd68e'
REGISTERED_COMMIT = 'deb1bcb2374eb378b840ca345db952bd8572cb6e'
EXECUTION_ORIGIN = '9767b0a28ca8d252520307dbbac60a1d8cfb87ab'
CHECKER_PATH = 'tools/check_p04_order_evidence.py'
DOC_SCOPE = 'docs/plans/original-completion-2026-10-02/execution/p04-01/'
MAIN_ANCHOR = '9694f50ee63750192376ca69bc7dda3f884784c5'
SELECTION_SHA = '320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4'
_GIT_ENV = None
# Fixed membership, never supplied by the evidence index.
ORDER_FUNCTIONS = {
'scripts/core/simulation_kernel.gd': ('advance_one_minute', '_run_phase', '_handle_time_boundaries'),
@@ -128,7 +135,173 @@ def literal(a, b):
def git_bytes(*args):
return subprocess.check_output(['git', *args], cwd=ROOT)
env = _GIT_ENV if _GIT_ENV is not None else dict(os.environ, GIT_OPTIONAL_LOCKS='0')
return subprocess.check_output(['git', '--no-replace-objects', *args], cwd=ROOT, env=env)
@contextmanager
def isolated_objects():
"""merge-tree writes only disposable objects; canonical refs/index stay untouched.
Keep this context alive while reading the composed tree. No stage switch can
bypass it. The alternate is only an object lookup path, never a write target.
"""
global _GIT_ENV
require(_GIT_ENV is None, 'nested object store')
objects = git_bytes('rev-parse', '--path-format=absolute', '--git-path', 'objects').decode().strip()
with tempfile.TemporaryDirectory(prefix='p04-object-audit-') as temporary:
env = dict(os.environ)
for key in ('GIT_OBJECT_DIRECTORY', 'GIT_ALTERNATE_OBJECT_DIRECTORIES',
'GIT_INDEX_FILE', 'GIT_QUARANTINE_PATH'):
env.pop(key, None)
env.update(GIT_OBJECT_DIRECTORY=temporary, GIT_ALTERNATE_OBJECT_DIRECTORIES=objects,
GIT_OPTIONAL_LOCKS='0', GIT_NO_REPLACE_OBJECTS='1')
_GIT_ENV = env
try:
yield
finally:
_GIT_ENV = None
def tree_entries(tree):
entries = {}
for row in git_bytes('ls-tree', '-r', '-z', tree).split(b'\0'):
if not row:
continue
metadata, encoded = row.split(b'\t', 1)
mode, kind, oid = metadata.decode().split()
path = encoded.decode('utf-8')
require(kind == 'blob' and mode in {'100644', '100755'}, 'nonordinary tree entry: ' + path)
require(path not in entries, 'duplicate tree path')
entries[path] = (mode, oid)
return entries
def selected_path(path):
parts = path.split('/')
return (parts[0] in {'autoload', 'scripts', 'scenes', 'data', 'tests', 'assets', 'tools'}
and len(parts) > 1 and parts[-1] != '.DS_Store' and '__pycache__' not in parts
or path in {'project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md'})
def allowed_path(path):
return path == CHECKER_PATH or path.startswith(DOC_SCOPE)
def product_manifest(entries):
result = {}
for path, (_, oid) in sorted(entries.items()):
if selected_path(path) and path != CHECKER_PATH:
data = git_bytes('cat-file', 'blob', oid)
result[path] = {'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
return result
def manifest_digest(manifest):
return hashlib.sha256(json.dumps(manifest, sort_keys=True, separators=(',', ':')).encode()).hexdigest()
def supplied_file(directory, descriptor):
"""Read supplied material in place, never copy it or follow provenance paths."""
name = descriptor['path']
path = Path(name)
if path.is_absolute():
support = directory / 'audit-support'
roots = [support / n for n in ('p04-whole-closure-r01', 'native-first-current',
'p04-current-native-series-r01')]
matches = [root for root in roots if path.is_relative_to(root)]
require(len(matches) == 1, 'unsupplied external material: ' + name)
path = safe(matches[0], path.relative_to(matches[0]).as_posix())
else:
path = safe(directory / 'p04-evidence', name)
require(type(descriptor.get('bytes')) is int and path.stat().st_size == descriptor['bytes']
and sha(path) == descriptor.get('sha256'), 'supplied material pin: ' + name)
return path
def verify_product_target(index, directory, checker_commit):
"""Four actual roles, complete source selection, and candidate equality.
This does not establish any native boundary condition. Runner input files
are checked against Git blobs, never against a Root-reported digest alone.
"""
binding = index['product_binding']
target = binding['product_target']
require(set(target) == {'main_anchor', 'submitted', 'composed_tree'}, 'product target shape')
require(target['main_anchor'] == MAIN_ANCHOR, 'main anchor changed')
for key in target:
require(re.fullmatch('[0-9a-f]{40}', target[key]) is not None, 'full product target OID')
for commit in (REGISTERED_COMMIT, EXECUTION_ORIGIN, target['main_anchor'], target['submitted'], checker_commit):
require(git_bytes('cat-file', '-t', commit).strip() == b'commit', 'role is not a commit')
require(_GIT_ENV is not None, 'merge-tree needs isolated object store')
composed = git_bytes('-c', 'rerere.enabled=false', 'merge-tree', '--write-tree',
target['main_anchor'], target['submitted']).decode().strip()
require(composed == target['composed_tree'], 'recomputed composed tree mismatch')
anchor, submission, tree = [tree_entries(t) for t in (MAIN_ANCHOR, target['submitted'], composed)]
selection = git_bytes('show', MAIN_ANCHOR + ':tools/run_all_tests.py')
require(hashlib.sha256(selection).hexdigest() == SELECTION_SHA, 'source selection authority')
changed = git_bytes('diff', '--no-ext-diff', '--no-renames', '--name-only', '-z',
REGISTERED_COMMIT, target['submitted']).split(b'\0')
require(all(allowed_path(p.decode()) for p in changed if p), 'submission outside registered scope')
for path in anchor.keys() | tree.keys():
if anchor.get(path) != tree.get(path):
require(allowed_path(path), 'new/changed path outside scope: ' + path)
baseline = product_manifest(anchor)
product = product_manifest(tree)
require(len(baseline) == 1338 and product == baseline, 'complete selected product mismatch')
metadata = sorted(p for p in anchor if not selected_path(p) and anchor[p] == tree.get(p))
require(len(metadata) == 3139, 'unchanged existing metadata membership changed')
require(set(p for p in tree if selected_path(p)) == set(product) | {CHECKER_PATH},
'unknown selected source cannot be metadata')
own = Path(__file__).read_bytes()
for role, entries in (('submitted', submission), ('composed', tree),
('checker', tree_entries(checker_commit))):
require(CHECKER_PATH in entries and git_bytes('cat-file', 'blob', entries[CHECKER_PATH][1]) == own,
'checker self blob not verified: ' + role)
tested = binding['tested_product']
require(git_bytes('cat-file', '-t', tested).strip() == b'commit'
and product_manifest(tree_entries(tested)) == product, 'tested product manifest mismatch')
candidate = tree_entries(index['candidate_tree'])
require(candidate == tree, 'candidate product/QA/collector/tree bytes differ from target')
runners = binding['runner_inputs']
require(isinstance(runners, list) and runners, 'actual runner inputs missing')
runner_paths = set()
for descriptor in runners:
runner_path = supplied_file(directory, descriptor)
require(runner_path not in runner_paths, 'duplicate runner input')
runner_paths.add(runner_path)
native = load(runner_path)
before = native['source_before']
require(set(before) == set(product) and all(
{'bytes': v['bytes'], 'sha256': v['sha256']} == product[p] for p, v in before.items()),
'runner source_before differs from complete product')
require(native['runner_sha256'] == product['tools/run_all_tests.py']['sha256'], 'runner source pin')
overrides = native['QA_overrides']
require(isinstance(overrides, list) and len(overrides) == 1
and overrides[0].startswith('tests/') and overrides[0] in product, 'one declared QA override')
selected = {p: v['sha256'] for p, v in product.items()}
selected[overrides[0]] = native['QA']['sha256']
require(native['selected_source_map'] == selected, 'selected source/QA override mismatch')
# A relocation is explicit and hash-bound; never open the original path.
materials = descriptor['materials']
fixed = native.get('fixed_input_pins', [])
require(isinstance(fixed, list), 'native fixed input pin list')
needed = fixed + [native['QA']]
require(set(materials) == {p['path'] for p in needed}, 'runner input/config material membership')
for pin in needed:
real = supplied_file(directory, materials[pin['path']])
require(real.stat().st_size == pin['bytes'] and sha(real) == pin['sha256'], 'native input/QA bytes')
wrapper = supplied_file(directory, descriptor['wrapper'])
require(sha(wrapper) == native['wrapper_sha256'], 'native wrapper bytes')
digest = manifest_digest(product)
require(index['versions'] == {
'registered': REGISTERED_COMMIT, 'execution_origin': EXECUTION_ORIGIN,
'checker': {'commit': checker_commit, 'sha256': hashlib.sha256(own).hexdigest()},
'tested': {'product_target': target, 'manifest_sha256': digest}}, 'four version roles')
require(index['source_map'] == {p: v['sha256'] for p, v in product.items()}, 'product source map')
return composed, {'manifest_sha256': digest, 'existing_metadata': metadata,
'runner_paths': runner_paths,
'tested_product': tested, 'product_target': target}
def function_bytes(data):
@@ -198,21 +371,19 @@ def applicability(index, spec, source, head):
for path, digest in registered.items():
require(hashlib.sha256(git_bytes('show', REGISTERED_COMMIT + ':' + path)).hexdigest() == digest,
'registered Git pin: ' + path)
require(index.get('versions') == {'execution_origin': EXECUTION_ORIGIN,
'registered': REGISTERED_COMMIT, 'tested': head, 'integration': None}, 'version roles')
# Refuse uncommitted game bytes; otherwise Git diff would not describe tested.
# head is the independently recomputed product tree, not the checker HEAD.
game_paths = git_bytes('ls-tree', '-r', '--name-only', '-z', head, '--', 'autoload/', 'scripts/').split(b'\0')
for encoded in game_paths:
if not encoded.endswith(b'.gd'):
continue
path = encoded.decode('utf-8')
require(path in source and sha(safe(ROOT, path)) == hashlib.sha256(git_bytes('show', head + ':' + path)).hexdigest(),
require(path in source and source[path] == hashlib.sha256(git_bytes('show', head + ':' + path)).hexdigest(),
'tested game differs from Git: ' + path)
delta = source_delta(REGISTERED_COMMIT, head)
require(index.get('applicability_delta') == delta, 'git diff/applicability_delta mismatch')
for path, names in ORDER_FUNCTIONS.items():
old = function_bytes(git_bytes('show', REGISTERED_COMMIT + ':' + path))
new = function_bytes(safe(ROOT, path).read_bytes())
new = function_bytes(git_bytes('show', head + ':' + path))
for name in names:
require(name in old and name in new and old[name] == new[name], 'stale ordering function: ' + path + ':' + name)
bindings = index.get('delta_cases')
@@ -227,7 +398,7 @@ def applicability(index, spec, source, head):
require(case_id == declaration.get('case_id'), 'boundary delta engine case: ' + key)
def coverage(index, raw, source):
def coverage(index, raw, source, product_tree):
authority = index['authority']
for ref in {r for _, refs, _ in REQUIRED.values() for r in refs}:
entry = authority.get(ref, {})
@@ -294,7 +465,7 @@ def coverage(index, raw, source):
require(ref.get('path') in source and ref.get('sha256') == source[ref['path']], 'reachability source pin')
identity = (ref['path'], ref.get('function'), ref.get('anchor_sha256'), ref.get('line_count'))
require(identity in REACH_REFS[name], 'unknown reachability anchor')
resolve_anchor(safe(ROOT, ref['path']).read_bytes(), *identity[1:])
resolve_anchor(git_bytes('show', product_tree + ':' + ref['path']), *identity[1:])
require(len(refs) == len(REACH_REFS[name]) and set(REACH_REFS[name]) == {
(r['path'], r['function'], r['anchor_sha256'], r['line_count']) for r in refs},
'missing static reachability chain: ' + name)
@@ -646,10 +817,10 @@ def verify(directory):
index = load(safe(evidence, 'index.json'))
require(type(index.get('schema_version')) is int and index['schema_version'] == 1, 'schema_version')
require(index.get('task_id') == 'P04-01' and index.get('formal_acceptance') is False, 'task/acceptance')
head = subprocess.check_output(['git', 'rev-parse', 'HEAD'], cwd=ROOT, text=True).strip()
require(index.get('candidate_commit') == head, 'candidate_commit mismatch')
checker_commit = git_bytes('rev-parse', 'HEAD').decode().strip()
require(index.get('candidate_commit') == checker_commit, 'checker invocation commit mismatch')
head, target_result = verify_product_target(index, directory, checker_commit)
source = index.get('source_map')
pins(source, ROOT)
applicability(index, spec, source, head)
oldmap = load(ROOT / 'docs/plans/original-completion-2026-10-02/execution/p04-01-input-r01/historical-source-map.json')
required = {p for p in oldmap if p.endswith('.gd') and p.startswith(('autoload/', 'scripts/'))}
@@ -719,15 +890,17 @@ def verify(directory):
raise ValueError('unsupported predicate: ' + str(op))
require(result, name + ': predicate failed')
require(index.get('review_status') in {'pending_review', 'approved'}, 'review status')
coverage(index, raw, source)
coverage(index, raw, source, head)
# Scalar conditions remain necessary declarations, never sufficient proof.
binding = index.get('native_bundle')
require(isinstance(binding, dict), 'native bundle required; self-reported scalar evidence is insufficient')
manifest = safe(evidence, binding['manifest'])
manifest = supplied_file(directory, binding)
require(manifest.name == 'OWNERSHIP_RAW_PROVENANCE.json' and sha(manifest) == binding['sha256'],
'native bundle provenance pin')
native = verify_native(manifest.parent)
require(native['production_commit'] == head, 'native production/tested mismatch; A/B reuse coverage required')
require({safe(manifest.parent, run + '/runner-input.json') for run in NATIVE_RUNS}
<= target_result['runner_paths'], 'C runner inputs absent from complete product binding')
require(native['production_commit'] == target_result['tested_product'], 'native production role mismatch')
# This adapter covers only the supplied dependency/formal series. Never
# promote it to the missing original scenarios through scalar booleans.
missing = (set(REQUIRED) | {'EV'}) - set(native['verified_conditions'])
@@ -741,7 +914,8 @@ def main():
parser.parse_args()
try:
require(bool(os.environ.get('BUILD_OUTPUT_DIR')), 'BUILD_OUTPUT_DIR required')
verify(Path(os.environ['BUILD_OUTPUT_DIR']))
with isolated_objects():
verify(Path(os.environ['BUILD_OUTPUT_DIR']).resolve())
except (ValueError, OSError, KeyError, TypeError, IndexError, AttributeError, subprocess.SubprocessError) as exc:
print(json.dumps({'task_id': 'P04-01', 'machine_verified': False, 'formal_acceptance': False, 'error': str(exc)}, ensure_ascii=False))
return 1