P04-01: 同時発生の更新規約
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user