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

This commit is contained in:
Agent Control Runner
2026-10-18 10:58:54 +09:00
parent 2dde7596a8
commit 20d0ae5990
32 changed files with 11890 additions and 314 deletions
+89 -115
View File
@@ -20,13 +20,31 @@ ROOT = Path(__file__).resolve().parents[1]
SPEC = ROOT / 'docs/spec/acceptance/P04-01.json'
SPEC_HASH = '9ec722e7dfb0bee178d291624a026420c08412574e238bba1fde012c7a6f7881'
DOMAINS = {'閉店', '停電', '破壊', '退出', '課金', 'シフト交代', '00:00決算'}
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 = '9b1a18c1eb25ca1300dcd59da78423c0dda2f498'
TESTED_PRODUCT = '5f6bc11d6bb6e92d22d787989b219fef3a435bc1'
OWNER_BINDINGS = {
ref: (DOC_SCOPE + leaf, digest)
for refs, leaf, digest in (
(('D05-P04-DEPENDENCY', 'D05-P04-SHIFT-1800'), 'D05_OWNER_APPROVAL.json',
'4583ee9aad45c4db183d72df4420df48aa8e0897e3b5efda13016f0b5cdbd68e'),
(('D05-P04-DESTRUCTION', 'D05-P04-MIDNIGHT-DAY', 'D05-P04-PAUSE-REASON',
'D05-P04-EXIT'), 'D05_REMAINING_APPROVAL.json',
'09b5092124574c3ccbf3b55169729a7529d9dde9b9a115d5e944fce4b960f5e4'),
) for ref in refs
}
MAIN_ANCHOR = 'c0ea009b28a7649aca6a433a1a24dc0be5e7ab81'
# Required evidence content, not an invented Runner API. Root must supply the
# independently authoritative receipt path/hash contract before C can qualify.
# Neither an index nor a commit with matching parents/tree can supply authority.
NORMAL_C_RECEIPT_CONTRACT = None
NORMAL_C_RECEIPT_FIELDS = frozenset({
'candidate_commit', 'candidate_tree', 'first_parent', 'second_parent',
'tested_tree', 'prepare_integration_receipt', 'independent_review_receipt',
'validation_receipt', 'source_manifest_sha256', 'settings_sha256',
'markers_sha256', 'policy_sha256', 'engine_sha256', 'inputs_sha256',
})
SELECTION_SHA = '320ca668732b4ff72fc4449998af2327d4e5c527b06896740806aaf0487282b4'
_GIT_ENV = None
# Fixed membership, never supplied by the evidence index.
@@ -43,19 +61,20 @@ ORDER_FUNCTIONS = {
'scripts/operations/operations_coordinator.gd': ('_process_minute_boundaries',),
}
# Condition -> (domain, authority refs, required engine measurements).
# Measurements describe safety/counts, not the still-pending attribution policy.
# Existing safety/count thresholds are retained. Repair30's additional approved
# order measurements still need the S5 raw adapter (see REPAIR30_STATUS.md).
REQUIRED = {
'power_stop': ('停電', ('BASELINE:183',), {'charges': 0}),
'destruction_stop': ('破壊', ('BASELINE:183',), {'charges': 0}),
'power_stop': ('停電', ('BASELINE:183', 'D05-P04-PAUSE-REASON'), {'charges': 0}),
'destruction_stop': ('破壊', ('D05-P04-DESTRUCTION',), {'charges': 0}),
'dependency_stop': ('閉店', ('D05-P04-DEPENDENCY',), {'charges': 0, 'progress_delta': 0, 'reservation_ids_preserved': True, 'service_id_preserved': True}),
'shift_completion': ('シフト交代', ('BASELINE:64', 'BASELINE:67', 'D05-P04-SHIFT-1800'), {'minute': 1080, 'completions': 1, 'charges': 1, 'completion_before_shift': True}),
'recovery_completion': ('課金', ('D05-P04-DEPENDENCY',), {'completions': 1, 'charges': 1}),
'same_minute_replay': ('課金', ('D05-P04-DEPENDENCY', 'D05-P04-SHIFT-1800'), {'completions': 1, 'charges': 1, 'duplicate_generations': 0, 'same_minute_replayed': True}),
'closure_release': ('閉店', ('D-034',), {'orphan_reservations': 0, 'released_once': True, 'started_service_continues': True}),
'exit_release': ('退出', ('D-034',), {'orphan_reservations': 0, 'released_once': True}),
'patience_release': ('退出', ('D-034',), {'orphan_reservations': 0, 'released_once': True}),
'paused_midnight': ('00:00決算', ('D05-P04-DEPENDENCY', 'BASELINE:71', 'BASELINE:72'), {'orphan_reservations': 0, 'crossed_midnight_while_paused': True, 'stopped_charges': 0, 'completions': 1, 'charges': 1}),
'midnight_settlement': ('00:00決算', ('BASELINE:71', 'BASELINE:72', 'A01'), {'settlements': 1, 'sales_payments': 1, 'payroll_payments': 1, 'upkeep_payments': 1, 'duty_payments': 1, 'nonzero_payroll': True}),
'exit_release': ('退出', ('D-034', 'D05-P04-EXIT'), {'orphan_reservations': 0, 'released_once': True}),
'patience_release': ('退出', ('D-034', 'D05-P04-EXIT'), {'orphan_reservations': 0, 'released_once': True}),
'paused_midnight': ('00:00決算', ('D05-P04-DEPENDENCY', 'BASELINE:71', 'BASELINE:72', 'D05-P04-MIDNIGHT-DAY'), {'orphan_reservations': 0, 'crossed_midnight_while_paused': True, 'stopped_charges': 0, 'completions': 1, 'charges': 1}),
'midnight_settlement': ('00:00決算', ('BASELINE:71', 'BASELINE:72', 'A01', 'D05-P04-MIDNIGHT-DAY'), {'settlements': 1, 'sales_payments': 1, 'payroll_payments': 1, 'upkeep_payments': 1, 'duty_payments': 1, 'nonzero_payroll': True}),
'midnight_replay': ('00:00決算', ('BASELINE:71', 'BASELINE:72', 'A01'), {'settlements': 1, 'same_minute_replayed': True, 'duplicate_payments': 0}),
'save_restore': ('00:00決算', ('BASELINE:71', 'BASELINE:72', 'A01'), {'settlements': 1, 'duplicate_payments': 0, 'formal_save_restored': True}),
'night_staff_absent': ('シフト交代', ('BASELINE:67', 'D05-P04-SHIFT-1800'), {'minute': 1081, 'state': 'WAITING_FOR_STAFF', 'night_staff_count': 0}),
@@ -244,6 +263,16 @@ def product_manifest(entries):
return result
def selected_manifest(entries, require_checker=True):
result = product_manifest(entries)
if require_checker:
require(CHECKER_PATH in entries, 'selected source missing checker')
if CHECKER_PATH in entries:
data = git_bytes('cat-file', 'blob', entries[CHECKER_PATH][1])
result[CHECKER_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()
@@ -266,91 +295,6 @@ def supplied_file(directory, descriptor):
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 ancestor(older, newer):
# merge-base's nonzero status is data only for the ordinary non-ancestor case.
try:
@@ -407,7 +351,7 @@ def verify_roles(index, directory):
parents = git_bytes('show', '-s', '--format=%P', commit).decode().split()
require(len(parents) == 2, 'integration/final requires a two-parent no-ff merge')
main, submitted = parents
require(ancestor(MAIN_ANCHOR, main), 'merge main ancestry')
require(main == MAIN_ANCHOR, 'integration main is not frozen M')
submission_role(submitted)
role = 'integration/final'
composed = git_bytes('-c', 'rerere.enabled=false', 'merge-tree', '--write-tree',
@@ -425,6 +369,7 @@ def verify_roles(index, directory):
'unknown source hidden as metadata: ' + path)
if role == 'integration/final':
require(actual_tree == composed and actual == target, 'actual merge differs from composition')
qualify_normal_candidate(commit, actual_tree, main, submitted, composed)
require(target[CHECKER_PATH] == patch[CHECKER_PATH] == actual[CHECKER_PATH],
'checker blob differs from submission')
for path in patch:
@@ -432,16 +377,22 @@ def verify_roles(index, directory):
require(target.get(path) == patch[path], 'reviewed patch differs in merge: ' + path)
binding = index['product_binding']
require(set(binding) == {'tested_product', 'runner_inputs'}, 'unknown product role/metadata declaration')
require(binding['tested_product'] == TESTED_PRODUCT, 'wrong tested revision')
tested = binding['tested_product']
require(tested == composed and git_bytes('cat-file', '-t', tested).strip() == b'tree',
'tested Q is not the genuine recomputed merge-tree')
selection = git_bytes('show', MAIN_ANCHOR + ':tools/run_all_tests.py')
require(hashlib.sha256(selection).hexdigest() == SELECTION_SHA, 'source selection authority')
product = product_manifest(target)
tested_entries = tree_entries(TESTED_PRODUCT)
tested_entries = tree_entries(tested)
expected = product_manifest(tested_entries)
require(len(product) == 1338 and product == expected and
require(product == expected and
product_manifest(main_entries) == expected and product_manifest(anchor_entries) == expected,
'complete 1338-file native product mismatch')
'complete selected native product mismatch')
require(all(target[p][0] == main_entries[p][0] == tested_entries[p][0] for p in product),
'native product mode mismatch')
require(index['source_map'] == {p: v['sha256'] for p, v in product.items()}, 'stale pin: source map')
selected_product = selected_manifest(target)
require(set(selected_product) == set(product) | {CHECKER_PATH}, 'checker must be explicitly selected')
require(index['source_map'] == {p: v['sha256'] for p, v in selected_product.items()}, 'stale pin: source map')
runner_paths = set()
for descriptor in binding['runner_inputs']:
path = supplied_file(directory, descriptor)
@@ -449,14 +400,14 @@ def verify_roles(index, directory):
runner_paths.add(path)
native = load(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()),
require(set(before) == set(selected_product) and all(
{'bytes': v['bytes'], 'sha256': v['sha256']} == selected_product[p] for p, v in before.items()),
'runner complete source map mismatch')
require(native['runner_sha256'] == product['tools/run_all_tests.py']['sha256'], 'runner source pin')
overrides = native['QA_overrides']
require(len(overrides) == 1 and overrides[0].startswith('tests/') and overrides[0] in product,
'QA override membership')
selected = {p: v['sha256'] for p, v in product.items()}
selected = {p: v['sha256'] for p, v in selected_product.items()}
selected[overrides[0]] = native['QA']['sha256']
require(native['selected_source_map'] == selected, 'selected QA source map')
needed = native.get('fixed_input_pins', []) + [native['QA']]
@@ -467,11 +418,26 @@ def verify_roles(index, directory):
require(sha(supplied_file(directory, descriptor['wrapper'])) == native['wrapper_sha256'], 'wrapper bytes')
require(runner_paths, 'runner inputs missing')
require(index['versions'] == {'registered': REGISTERED_COMMIT, 'execution_origin': EXECUTION_ORIGIN,
'tested': TESTED_PRODUCT}, 'evidence version roles')
'tested': tested}, 'evidence version roles')
return composed, {'role': role, 'actual_head': commit, 'actual_tree': actual_tree,
'submission': submitted, 'main': main, 'existing_metadata': metadata,
'manifest_sha256': manifest_digest(product), 'runner_paths': runner_paths,
'tested_product': TESTED_PRODUCT}
'tested_product': tested, 'product_file_count': len(product), 'selected_file_count': len(selected_product)}
def qualify_normal_candidate(commit, tree, main, submitted, composed):
"""Fail closed until the actual Root/Runner authority contract is supplied.
The field roster above records the required joins. It intentionally is not
a self-issued attestation format that a synthetic commit can satisfy.
"""
require(tree == composed and main == MAIN_ANCHOR and commit != submitted,
'normal candidate identity mismatch')
require(NORMAL_C_RECEIPT_CONTRACT is not None,
'normal C qualification context gap: authoritative Root/Runner receipt '
'path, SHA256 and provenance verification contract are not supplied')
# No authority implementation is inferred from a caller-supplied object.
raise ValueError('normal C receipt authority verifier is not implemented')
def function_bytes(data):
@@ -572,6 +538,19 @@ def applicability(index, spec, source, head):
require(case_id == declaration.get('case_id'), 'boundary delta engine case: ' + key)
def owner_authority(ref, entry, path):
"""Each decision is bound to its own immutable, path-qualified record."""
require(ref in OWNER_BINDINGS, 'unknown owner authority: ' + ref)
relative, digest = OWNER_BINDINGS[ref]
require(entry.get('status') == 'approved', 'pending/missing authority: ' + ref)
require(entry.get('source') == relative, 'owner approval path mismatch: ' + ref)
require(sha(path) == digest, 'owner approval hash mismatch: ' + ref)
owner = load(path)
require(owner.get('origin') == 'direct_user_reply' and owner.get('formal_acceptance') is False,
'owner approval scope')
require(ref in {v['id'] for v in owner['approved_choices']}, 'owner approval condition')
def coverage(index, raw, source, product_tree):
authority = index['authority']
for ref in {r for _, refs, _ in REQUIRED.values() for r in refs}:
@@ -580,11 +559,7 @@ def coverage(index, raw, source, product_tree):
aid = entry.get('artifact_id')
require(aid in raw and index['artifacts'][aid]['type'] == 'authority', 'raw authority: ' + ref)
if ref.startswith('D05-P04-'):
require(sha(raw[aid]) == OWNER_HASH, 'owner approval hash mismatch')
owner = load(raw[aid])
require(owner.get('origin') == 'direct_user_reply' and owner.get('formal_acceptance') is False,
'owner approval scope')
require(ref in {v['id'] for v in owner['approved_choices']}, 'owner approval condition')
owner_authority(ref, entry, raw[aid])
else:
path = ('docs/traceability/original/BASELINE.txt' if ref.startswith('BASELINE:') else
'docs/DESIGN_DECISIONS.md' if ref == 'D-034' else
@@ -983,14 +958,13 @@ def verify_shift_completion(base):
"""C's first 18:00 transaction, never the paid-midnight 1442 sale."""
for row in load(safe(base, 'SOURCE_MANIFEST.json'))['files']:
native_file(base, row['copy'], row)
require(native_applicability(base) == TESTED_PRODUCT, 'shift tested revision')
tested = native_applicability(base)
pids = native_ownership(base)
shift_pids = [pids['actual-native/' + phase] for phase in NATIVE_RUNS['actual-native']]
require(len(shift_pids) == len(set(shift_pids)) == 3, 'shift ended PID identity')
inputs = load(safe(base, 'actual-native/runner-input.json'))
expected = product_manifest(tree_entries(TESTED_PRODUCT))
require(len(inputs['source_before']) == 1338 and
{p: {'bytes': v['bytes'], 'sha256': v['sha256']} for p, v in inputs['source_before'].items()}
expected = selected_manifest(tree_entries(tested), require_checker=False)
require({p: {'bytes': v['bytes'], 'sha256': v['sha256']} for p, v in inputs['source_before'].items()}
== expected, 'shift complete native source')
prefix = 'actual-native/order-evidence/'
observations = load(safe(base, prefix + 'observations.json'))