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