Files
digital-drosophila/tools/phase4ap-publication/publish_phase4ap.py
2026-09-30 18:36:01 +08:00

288 lines
18 KiB
Python

"""Exclusive small-evidence publication of an already sealed AP experiment.
Three external hashes are required. Publication verifies bytes and reporting
identities, not the numerical gate or learning. The imported AO trajectory and
other old bulk remain external; this tool does not reclassify AO's failure.
"""
from __future__ import annotations
import argparse
import hashlib
import importlib.util
import json
from pathlib import Path, PurePosixPath
import re
import sys
ROOT = Path('/data/digital-drosophila')
MODEL_REL = Path('models/male-cns-lif/conditioning-interface-v2')
EXP_REL = Path('experiments/phase4ap-conditioning-interface-v2')
REGISTRY_REL = Path('registry/phase4ap-conditioning-interface-v2')
AO_REL = Path('experiments/phase4ao-conditioning-interface')
HELPER_REL = Path('models/male-cns-lif/pn-recruitment-audit-v1/publication.py')
HELPER_SHA = '7a79c499098b74163e7bb16adabcbc54e7069dafa204d0b99c7f4c03d79f8016'
SOURCE_FILES = frozenset(('coordinator.py', 'test_coordinator.py', 'isolated_workers.py',
'test_isolated_workers.py', 'PROTOCOL.md'))
REGISTRATION, FINAL = 'protocol/preregistered.json', 'audit/final-status.json'
ANALYSES = ('audit/interface-analysis.json', 'audit/failure-analysis.json')
SNAPSHOT = 'protocol/source-snapshot/'
FIRST = 'D1--reference--epoch-000'
AO_STATUS = 'FAIL-POSTPROCESS-MODULE-IDENTITY'
MAX_BYTES = 4 * 1024 ** 2
SUFFIXES = frozenset(('.json', '.md', '.py', '.log', '.sha256'))
SHA = re.compile(r'^[0-9a-f]{64}$')
def require(ok, message):
if not ok:
raise ValueError(message)
def digest(path):
value = hashlib.sha256()
with Path(path).open('rb') as stream:
for chunk in iter(lambda: stream.read(8 << 20), b''):
value.update(chunk)
return value.hexdigest()
def load_helper(path):
raw = Path(path).read_bytes()
require(hashlib.sha256(raw).hexdigest() == HELPER_SHA, 'Frozen helper SHA mismatch')
spec = importlib.util.spec_from_file_location('phase4ap_pinned_seal_helper', path)
module = importlib.util.module_from_spec(spec)
exec(compile(raw, str(path), 'exec'), module.__dict__)
return module
def write_json(path, value):
raw = (json.dumps(value, indent=2, sort_keys=True, ensure_ascii=False, allow_nan=False) + '\n').encode('utf-8')
require(len(raw) <= MAX_BYTES, 'Generated evidence exceeds 4 MiB')
with Path(path).open('xb') as stream:
stream.write(raw)
def small_json(helper, experiment, rows, name):
require(name in rows and rows[name]['bytes'] <= MAX_BYTES, 'Missing/oversize required evidence: ' + name)
path = helper._regular_file(experiment, name)
require(digest(path) == rows[name]['sha256'], 'Evidence changed: ' + name)
value = helper._json_bytes(path.read_bytes())
require(type(value) is dict, 'Expected JSON object: ' + name)
return value
def source_identity(helper, root, experiment, rows, registration, reg_sha):
model = root / MODEL_REL
sources, prepared = registration.get('source_sha256'), registration.get('prepared_file_sha256')
require(type(sources) is dict and set(sources) == {str(model / name) for name in SOURCE_FILES} and
type(prepared) is dict, 'Registered AP source inventory must be exactly five fixed files')
proof = []
for name in sorted(SOURCE_FILES):
checksum = sources[str(model / name)]
require(type(checksum) is str and SHA.fullmatch(checksum), 'Malformed source SHA')
relative = SNAPSHOT + name
original = helper._regular_file(root, (MODEL_REL / name).as_posix())
snapshot = helper._regular_file(experiment, relative)
require(relative in rows and rows[relative]['sha256'] == checksum and
prepared.get(str(snapshot)) == checksum and rows[relative]['bytes'] <= MAX_BYTES and
original.stat().st_size == rows[relative]['bytes'] and digest(original) == digest(snapshot) == checksum,
'Registered source, prepared snapshot and current model differ: ' + name)
proof.append(dict(model_source=(MODEL_REL / name).as_posix(), sealed_source=relative,
copy='source-snapshot/' + name, bytes=rows[relative]['bytes'], sha256=checksum))
require({name for name in rows if name.startswith(SNAPSHOT)} == {SNAPSHOT + name for name in SOURCE_FILES},
'Sealed source snapshot inventory differs from registered five')
for text, checksum in prepared.items():
require(type(text) is str and type(checksum) is str and SHA.fullmatch(checksum), 'Invalid prepared identity')
path = Path(text)
require(path.is_absolute() and path.is_relative_to(experiment) and '..' not in path.parts,
'Prepared path escapes AP')
relative = path.relative_to(experiment).as_posix()
helper._relative_name(relative)
require(relative in rows and rows[relative]['sha256'] == checksum, 'Prepared file differs from seal: ' + relative)
commit = registration.get('source_code_commit')
require(type(commit) is str and re.fullmatch('[0-9a-f]{40}', commit), 'Missing registered source commit')
return dict(registration_sha256=reg_sha, source_count=5, source_code_commit=commit, files=proof,
model_snapshot_registration_exact=True, git_blobs_reverified_by_publisher=False,
entire_model_directory_sealed=False)
def declared_scope(root, reg, final, analysis, reg_sha):
require(reg.get('schema') == 'phase4ap-process-isolated-interface-v1', 'Wrong registration phase')
expected = [f'{arm}--{mode}--epoch-{epoch:03d}' for arm in ('D1', 'D0', 'frozen')
for mode in ('reference', 'light') for epoch in (0, 1)]
slots = reg.get('slots')
require(type(slots) is list and [row.get('row_id') for row in slots] == expected, 'Wrong registered slot coverage/order')
for row in slots:
name = row['row_id']
imported = name == FIRST
base = root / (AO_REL if imported else EXP_REL)
require(row.get('origin') == ('imported_sealed_AO_neural_trajectory' if imported else 'new_AP_neural_execution') and
row.get('neural_dir') == str(base / 'runs' / name) and row.get('case_dir') == str(base / 'cases' / name) and
row.get('original_case_dir') == str(root / AO_REL / 'cases' / name) and
type(row.get('steps')) is int and row['steps'] == 1200, 'Wrong imported/new slot provenance')
for key, number in dict(covered_slots=12, new_neural_calls=11, covered_ticks=14400,
new_neural_ticks=13200, new_sampler_calls=0).items():
require(type(reg.get(key)) is int and reg[key] == number, 'Wrong registered budget/coverage: ' + key)
require(reg.get('imported_slot') == FIRST and reg.get('original_failed_workflow_reclassified') is False and
reg.get('original_AO_process_complete') is False and reg.get('inherited_neural_checkpoint_complete') is True,
'Registration reclassifies or obscures imported AO origin')
pin = reg.get('imported_checkpoint_sha256')
require(type(pin) is str and SHA.fullmatch(pin) and type(reg.get('anchors')) is dict and
reg['anchors'].get(str(root / AO_REL / 'runs' / FIRST / 'checkpoint-manifest.json')) == pin,
'Imported checkpoint is not bound to its registered anchor')
require(reg.get('full_conditioning') is False and reg.get('learning_success_claimed') is False and
reg.get('within_trial_updates') is False and reg.get('retention_transition_supported') is False,
'Registered interface scope changed')
status = final.get('status')
require(type(status) is str and re.fullmatch('(PASS|FAIL|INCOMPLETE)(-[A-Z0-9]+)*', status), 'Unknown final status')
require(final.get('registration_sha256') == analysis.get('registration_sha256') == reg_sha and
analysis.get('status') == status, 'Final/analysis/registration identity mismatch')
for value in (final, analysis):
for flag in ('learned_candidate', 'learning_success_claimed', 'full_conditioning_executed'):
require(value.get(flag) is False, 'No-learning scope must be explicit: ' + flag)
require(final.get('AO_failure_preserved') is True and analysis.get('original_AO_status') == AO_STATUS,
'AP must not reclassify AO as successful')
# Report consistency only. Do not rerun the numerical gate or upgrade FAIL.
declared = {}
limits = dict(covered_slots=12, new_neural_calls=11, imported_neural_trajectories=1, new_sampler_calls=0)
for key, limit in limits.items():
a, b = final.get(key), analysis.get(key)
require(type(a) is int and type(b) is int and a == b and 0 <= a <= limit, 'Inconsistent declared count: ' + key)
require(status.startswith('FAIL') or a == limit, 'Nonfailed finalized interface lacks declared coverage')
declared[key] = a
return dict(original_AO_status=AO_STATUS, original_AO_process_complete=False,
planned_covered_slots=12, planned_imported_neural_trajectories=1, planned_new_neural_calls=11,
declared_final_counts=declared, imported_checkpoint_sha256=pin,
scope='Declared report/registration consistency only; imported AO bulk not reread by publisher',
scientific_gate_recomputed=False, imported_bulk_independently_reverified=False)
def selection(row):
name = row['path']; parts = PurePosixPath(name)
if parts.name == 'full-trial-plan.json':
return None, 'full_trial_plan_not_exported'
if any(part in ('extension-build', 'build', '__pycache__') for part in parts.parts):
return None, 'build_or_cache_artifact'
if row['bytes'] > MAX_BYTES:
return None, 'exceeds_4_MiB'
if name.startswith(SNAPSHOT):
return 'source-snapshot/' + parts.name, None
if parts.suffix.lower() in SUFFIXES:
return 'evidence/' + name, None
return None, 'binary_or_non_selected_extension'
def file_rows(directory):
return [dict(path=p.relative_to(directory).as_posix(), bytes=p.stat().st_size, sha256=digest(p))
for p in sorted(directory.rglob('*')) if p.is_file()]
def seal_registry(helper, registry):
files = file_rows(registry)
write_json(registry / 'manifest.json', dict(root=str(registry), files=files))
raw = ''.join(row['sha256'] + ' ' + row['path'] + '\n' for row in files)
raw += digest(registry / 'manifest.json') + ' manifest.json\n'
require(len(raw.encode()) <= MAX_BYTES, 'Small registry checksum list exceeds limit')
with (registry / 'checksums.sha256').open('xb') as stream:
stream.write(raw.encode('utf-8'))
return helper.verify_seal(registry)
def publish(registration_sha256, experiment_manifest_sha256, final_sha256, *, root=ROOT):
"""Verify a finalized AP seal and create only its absent small registry."""
require(sys.dont_write_bytecode, 'Use Python -B')
for value in (registration_sha256, experiment_manifest_sha256, final_sha256):
require(type(value) is str and SHA.fullmatch(value), 'Three external SHA-256 anchors are mandatory')
root = Path(root)
require(root.is_absolute() and root.is_dir() and not root.is_symlink(), 'Invalid project root')
root = root.resolve(strict=True)
experiment, registry, helper_path = (root / rel for rel in (EXP_REL, REGISTRY_REL, HELPER_REL))
for path in (experiment, registry, root / MODEL_REL, helper_path):
cursor = root
for part in path.relative_to(root).parts:
cursor /= part
require(not cursor.is_symlink(), 'Symlink in publication path')
require(not registry.exists(), 'Registry exists; never overwrite or resume')
helper = load_helper(helper_path); own_sha = digest(__file__)
for name, expected in ((REGISTRATION, registration_sha256), ('manifest.json', experiment_manifest_sha256), (FINAL, final_sha256)):
require(digest(helper._regular_file(experiment, name)) == expected, 'External anchor mismatch: ' + name)
verified = helper.verify_seal(experiment)
rows = {row['path']: row for row in verified['files']}
require(verified['manifest_sha256'] == experiment_manifest_sha256 and
rows.get(REGISTRATION, {}).get('sha256') == registration_sha256 and rows.get(FINAL, {}).get('sha256') == final_sha256,
'Verified seal differs from external anchors')
reg = small_json(helper, experiment, rows, REGISTRATION)
final = small_json(helper, experiment, rows, FINAL)
linked = [name for name in ANALYSES if name in rows and rows[name]['sha256'] == final.get('analysis_sha256')]
require(len(linked) == 1, 'Final must name one sealed interface/failure analysis SHA')
analysis_path = linked[0]
require('analysis_path' not in final or final['analysis_path'] == analysis_path, 'Analysis path/SHA conflict')
analysis = small_json(helper, experiment, rows, analysis_path)
scope = declared_scope(root, reg, final, analysis, registration_sha256)
sources = source_identity(helper, root, experiment, rows, reg, registration_sha256)
inventory, plans = [], []
for row in verified['files'] + helper._control_rows(experiment, verified):
target, reason = selection(row)
inventory.append(dict(**row, copy=target, disposition='copied' if target else 'omitted', reason=reason))
if target: plans.append((row, target))
targets = [target for _, target in plans]
required = {'evidence/' + name for name in (REGISTRATION, FINAL, analysis_path, 'manifest.json', 'checksums.sha256')}
required |= {'source-snapshot/' + name for name in SOURCE_FILES}
require(len(set(targets)) == len(targets) and required <= set(targets), 'Missing/oversize required evidence or destination collision')
registry.parent.mkdir(parents=True, exist_ok=True); registry.mkdir(exist_ok=False)
try:
records = []
for row, target in plans:
helper._copy_verified(helper._regular_file(experiment, row['path']), registry / target, row['bytes'], row['sha256'])
records.append(dict(source=(EXP_REL / row['path']).as_posix(), copy=target, bytes=row['bytes'], sha256=row['sha256']))
require(helper.verify_seal(experiment) == verified, 'AP experiment changed while publishing')
require(source_identity(helper, root, experiment, rows, reg, registration_sha256) == sources, 'Registered AP source changed')
require(digest(helper_path) == HELPER_SHA and digest(__file__) == own_sha, 'Publisher/helper changed')
write_json(registry / 'evidence-index.json', records)
write_json(registry / 'source-identity.json', sources)
write_json(registry / 'scope-audit.json', scope)
write_json(registry / 'seal-verification.json', {k: v for k, v in verified.items() if k != 'files'})
write_json(registry / 'artifact-inventory.json', dict(experiment_manifest_sha256=experiment_manifest_sha256,
files=inventory, omitted_files=sum(r['copy'] is None for r in inventory),
omitted_bytes=sum(r['bytes'] for r in inventory if r['copy'] is None),
covers_entire_AP_payload_and_controls=True, complete_bulk_backup=False,
imported_AO_bulk_is_external=True))
receipt = dict(schema='phase4ap-publication-v1', publication_complete=True, registry=REGISTRY_REL.as_posix(),
project_root=str(root), declared_audit_status=final['status'], registration_sha256=registration_sha256,
experiment_manifest_sha256=experiment_manifest_sha256, experiment_checksums_sha256=verified['checksums_sha256'],
final_sha256=final_sha256, analysis_path=analysis_path, analysis_sha256=final['analysis_sha256'],
source_count=5, publisher_sha256=own_sha, helper_sha256=HELPER_SHA, helper_globals_modified=False,
files=file_rows(registry), copied_evidence_files=len(records), copied_evidence_bytes=sum(r['bytes'] for r in records),
source_origin=scope, bulk_remains_at=EXP_REL.as_posix(), imported_bulk_remains_at=AO_REL.as_posix(),
complete_bulk_backup=False, model_directory_sealed=False, existing_files_modified=False,
scientific_gate_executed_by_publisher=False, scientific_gate_certified_by_publication=False,
learning_achieved=False, candidate_generated=False, new_brain_runs=0,
receipt_self_hash='Returned externally; also covered by the new registry seal.',
registry_controls='Created after this receipt, independently verified; no circular self hash.',
limits=dict(max_selected_file_bytes=MAX_BYTES, selected_suffixes=sorted(SUFFIXES)))
write_json(registry / 'publication-receipt.json', receipt)
sealed = seal_registry(helper, registry)
return dict(publication_complete=True, declared_audit_status=final['status'], registry=str(registry), source_count=5,
receipt_sha256=digest(registry / 'publication-receipt.json'), registry_manifest_sha256=sealed['manifest_sha256'],
registry_checksums_sha256=sealed['checksums_sha256'], copied_evidence_files=len(records),
complete_bulk_backup=False, scientific_gate_certified_by_publication=False)
except Exception as error:
if not (registry / 'publication-failure.json').exists():
write_json(registry / 'publication-failure.json', dict(publication_complete=False, error_type=type(error).__name__,
error=str(error), partial_outputs_preserved=True, existing_files_modified=False))
raise
def main():
parser = argparse.ArgumentParser(description=__doc__)
for name in ('registration-sha256', 'experiment-manifest-sha256', 'final-sha256'):
parser.add_argument('--' + name, required=True)
args = parser.parse_args()
print(json.dumps(publish(args.registration_sha256, args.experiment_manifest_sha256, args.final_sha256),
sort_keys=True, ensure_ascii=False, allow_nan=False), flush=True)
if __name__ == '__main__':
main()