Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
161 lines
14 KiB
Python
161 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
"""Preview by default; --apply creates only this turn's new evidence archive.
|
|
Run after successful bound suite and before separate documentation edits.
|
|
No native launch, test execution, package rewrite or third-party payload copy.
|
|
"""
|
|
from pathlib import Path
|
|
import argparse,hashlib,json,subprocess,sys,shutil,tempfile
|
|
ROOT=Path('/home/jakub/devel/escapistsrereintroduced')
|
|
WORK=Path('/tmp/erri-gameplay-root-1509')
|
|
DEST=ROOT/'docs/evidence/config-supply-recovery-2026-10-03'
|
|
PROPOSALS={'config':Path('/tmp/erri-config-review-1509'),'supply':Path('/tmp/erri-ai-review-1509'),'optional-identity':Path('/tmp/erri-optional-identity-1509')}
|
|
ORIGINAL=Path('/home/jakub/devel/escapists')
|
|
REFERENCE=Path('/tmp/erri-heartbeat-20260930-1542-reference.json')
|
|
def sha(p):
|
|
return hashlib.file_digest(p.open('rb'),'sha256').hexdigest()
|
|
def load(p):return json.loads(p.read_text())
|
|
def require(ok,why):
|
|
if not ok:raise RuntimeError(why)
|
|
def capture_inputs():
|
|
sys.path.insert(0,str(ROOT/'tools'))
|
|
from source_manifest import addon_manifest
|
|
result=addon_manifest(ROOT)
|
|
for folder in ['tests','tools','terrain-profiles','terrain-adapters','terrain-integrations']:
|
|
result.update({str(p.relative_to(ROOT)):sha(p) for p in (ROOT/folder).rglob('*') if p.is_file() and '__pycache__' not in p.parts and p.suffix!='.pyc'})
|
|
result.update({str(p.relative_to(ROOT)):sha(p) for p in (ROOT/'docs').iterdir() if p.is_file()})
|
|
for name in ['README.md','.gitignore']:result[name]=sha(ROOT/name)
|
|
return result
|
|
|
|
def seal():
|
|
inventory={str(p.relative_to(DEST)):sha(p) for p in DEST.rglob('*') if p.is_file() and p.name!='artifact-sha256.json'}
|
|
(DEST/'artifact-sha256.json').write_text(json.dumps(inventory,indent=2)+'\n')
|
|
def finalize():
|
|
require(DEST.is_dir(),'Archive must exist before documentation finalization')
|
|
seal_before=load(DEST/'artifact-sha256.json')
|
|
require({str(p.relative_to(DEST)):sha(p) for p in DEST.rglob('*') if p.is_file() and p.name!='artifact-sha256.json'}==seal_before,'Archive changed before final seal')
|
|
changes=load(WORK/'post-suite-docs.json');inputs=load(WORK/'suite-inputs.json')
|
|
require(set(changes)=={'docs/current-status.md','docs/configuration.md','docs/infantry-resupply.md'},'Unexpected post-suite document scope')
|
|
expected=dict(inputs)
|
|
for name,values in changes.items():
|
|
require(inputs[name]==values['before'] and sha(ROOT/name)==values['after'],'Post-suite document binding mismatch '+name)
|
|
expected[name]=values['after']
|
|
require(capture_inputs()==expected,'Unreviewed post-suite input changes')
|
|
baseline=load(WORK/'baseline.json');integrated=load(WORK/'integration.json')
|
|
for name,digest in baseline['files'].items():
|
|
expected_hash=changes.get(name,{}).get('after',integrated.get(name,{}).get('after',digest))
|
|
require((ROOT/name).is_file() and sha(ROOT/name)==expected_hash,'Inherited path changed after docs '+name)
|
|
index=hashlib.sha256(subprocess.check_output(['git','diff','--cached','--binary'],cwd=ROOT)).hexdigest()
|
|
require(index==baseline['index_sha256'],'Index changed after docs')
|
|
original=load(REFERENCE);require(len(original)==533 and all(sha(ORIGINAL/n)==v for n,v in original.items()),'Original changed after docs')
|
|
for label in ['mounted-pair','mine-safety','ground-supply','depot-mines']:
|
|
candidate=ROOT/f'.build/playtest-2026-10-03-{label}'
|
|
require({str(p.relative_to(candidate)):sha(p) for p in candidate.rglob('*') if p.is_file()}==load(DEST/f'preservation/{label}-candidate-sha256.json'),'Candidate changed after docs '+label)
|
|
for name in ['post-suite-docs.json','update-docs.py']:
|
|
require(not (DEST/'root'/name).exists(),'Already finalized '+name)
|
|
shutil.copyfile(WORK/name,DEST/'root'/name)
|
|
(DEST/'finalization.json').write_text(json.dumps({'complete':True,'post_suite_document_changes':changes,'source_tests_tools_unchanged':True,'fresh_suite_after_prose_changes_claimed':False,'index_unchanged':True,'original533_unchanged':True,'four_sealed_candidates_unchanged':True},indent=2)+'\n')
|
|
seal();print('Final document delta archived and evidence resealed')
|
|
def main():
|
|
parser=argparse.ArgumentParser(description=__doc__);group=parser.add_mutually_exclusive_group();group.add_argument('--apply',action='store_true');group.add_argument('--finalize',action='store_true');args=parser.parse_args()
|
|
if args.finalize:return finalize()
|
|
require(not DEST.exists(),'Refusing existing evidence archive')
|
|
baseline=load(WORK/'baseline.json');integrated=load(WORK/'integration.json');suite=load(WORK/'suite-result.json');inputs=load(WORK/'suite-inputs.json')
|
|
require(suite['exit']==0 and suite['inputs_unchanged'] is True,'Full unchanged-input suite required')
|
|
programs=suite['programs'];require(programs and len(programs)==suite['regression_programs'] and all(p['exit']==0 for p in programs),'Incomplete/failed registered test list')
|
|
require(capture_inputs()==inputs,'Current suite inputs differ; do not archive as current pass')
|
|
for name,item in integrated.items():
|
|
require(sha(ROOT/name)==item['after']==inputs[name],'Integrated source/test binding differs: '+name)
|
|
for name,digest in baseline['files'].items():
|
|
expected=integrated.get(name,{}).get('after',digest)
|
|
require((ROOT/name).is_file() and sha(ROOT/name)==expected,'Unreviewed inherited change: '+name)
|
|
index=hashlib.sha256(subprocess.check_output(['git','diff','--cached','--binary'],cwd=ROOT)).hexdigest()
|
|
require(index==baseline['index_sha256'],'Index changed')
|
|
original=load(REFERENCE);require(len(original)==533,'Unexpected original reference scope')
|
|
require(all((ORIGINAL/name).is_file() and sha(ORIGINAL/name)==digest for name,digest in original.items()),'Original reference changed')
|
|
# Only manifests are archived; sealed source/ZIP/PAK/runtime payloads stay in place.
|
|
candidates=[];copies={}
|
|
for label in ['mounted-pair','mine-safety','ground-supply','depot-mines']:
|
|
sealpath=ROOT/f'docs/evidence/playtest-refresh-2026-10-03-{label}/candidate-sha256.json'
|
|
seal=load(sealpath);candidate=ROOT/f'.build/playtest-2026-10-03-{label}'
|
|
actual={str(p.relative_to(candidate)):sha(p) for p in candidate.rglob('*') if p.is_file()}
|
|
require(actual==seal,'Sealed candidate changed: '+label)
|
|
copies[f'preservation/{label}-candidate-sha256.json']=sealpath
|
|
candidates.append({'candidate':str(candidate),'files':len(seal),'manifest_sha256':sha(sealpath),'unchanged':True})
|
|
copies['preservation/original-reference.json']=REFERENCE
|
|
base_review=Path('/tmp/erri-base-review-1509/finding.json');require(base_review.is_file(),'Missing bounded base review');copies['base-gameplay-review.json']=base_review
|
|
root_required=['baseline.json','integration.json','suite-inputs.json','suite-result.json','full-suite.log','run-suite.py','native-deferred.json','optional-identity-audit.json','vanilla-character-audit.json']
|
|
for name in root_required:
|
|
require((WORK/name).is_file(),'Missing root evidence '+name);copies['root/'+name]=WORK/name
|
|
for name in ['integrate.py','host-idle-inspection.json','preflight.json','review.json','final-review.json']:
|
|
if (WORK/name).is_file():copies['root/'+name]=WORK/name
|
|
failed=load(WORK/'first-suite-failure/suite-result.json')
|
|
require(failed['exit']!=0,'Retained initial suite failure must remain a failure')
|
|
for name in ['full-suite.log','suite-inputs.json','suite-result.json']:
|
|
path=WORK/'first-suite-failure'/name
|
|
require(path.is_file() and not path.is_symlink(),'Missing original suite failure '+name)
|
|
copies['root/first-suite-failure/'+name]=path
|
|
proposal_bindings={}
|
|
# Explicit owned artifacts only. No recursive scratch copy or installed resources.
|
|
optional=['baseline.json','finding.json','report.json','result.json','test-results.json','independent-review.json','review.json','apply-check.log','baseline-red.log','proposal-green.log','previous-validation-red.log','red.log','green.log','mutation-red.log','baseline-test.log','proposed-test.log','prepare.py','build-test.py','prepare-test.py','optional-identity-audit.json','vanilla-character-audit.json','vanilla-identity-proof.json','test-source-inputs.json']
|
|
for label,folder in PROPOSALS.items():
|
|
manifest_path=folder/'source-manifest.json';manifest=load(manifest_path)
|
|
copies[label+'/source-manifest.json']=manifest_path
|
|
patch=folder/'fix.patch';require(patch.is_file(),'Missing patch '+label);copies[label+'/fix.patch']=patch
|
|
if 'patch_sha256' in manifest:require(sha(patch)==manifest['patch_sha256'],'Patch changed '+label)
|
|
files=manifest.get('files',manifest);selected={}
|
|
for name,item in files.items():
|
|
if not isinstance(item,dict):continue
|
|
digest=item.get('proposal_sha256',item.get('proposed'))
|
|
if not digest:continue
|
|
allowed=name.startswith(('addons/core/Scripts/Game/ERR/','tests/'))
|
|
if label=='optional-identity':allowed=name in {'tools/resource_references.py','tools/check.py','tools/source_bundle.py','tests/test_external_addons.py','docs/vanilla-resources.json'}
|
|
require(allowed and '..' not in Path(name).parts,'Unexpected proposal scope '+name)
|
|
require(name in integrated and integrated[name]['after']==digest and sha(folder/name)==digest,'Unbound proposal '+name)
|
|
copies[label+'/proposed/'+name]=folder/name;selected[name]=digest
|
|
base=folder/'baseline'/name
|
|
if base.is_file():
|
|
expected=item.get('baseline_sha256',item.get('before_working',item.get('before')))
|
|
require(expected and sha(base)==expected,'Proposal baseline mismatch '+name);copies[label+'/baseline/'+name]=base
|
|
require(selected,'Empty proposal '+label)
|
|
require((folder/'independent-review.json').is_file(),'Independent review required '+label)
|
|
independent=load(folder/'independent-review.json')
|
|
require(independent.get('approved') is True and independent['patch_sha256']==sha(patch) and independent['source_manifest_sha256']==sha(manifest_path),'Independent review binding differs '+label)
|
|
for name,digest in manifest.get('artifacts',{}).items():
|
|
require(Path(name).name==name and sha(folder/name)==digest,'Manifest artifact differs '+label+'/'+name)
|
|
if label=='optional-identity':
|
|
for name in ['optional-identity-audit.json','vanilla-character-audit.json']:
|
|
require(sha(folder/name)==manifest[name+'_sha256']==sha(WORK/name),'Audit evidence differs '+name)
|
|
audited=manifest['audited_source']
|
|
require(audited['modified'] is False and sha(ROOT/audited['path'])==audited['sha256'],'Optional audited source changed')
|
|
if 'test_results_sha256' in manifest:require(sha(folder/'test-results.json')==manifest['test_results_sha256'],'Test result binding differs '+label)
|
|
for name in optional:
|
|
if (folder/name).is_file():copies[label+'/'+name]=folder/name
|
|
for log in folder.glob('*.log'):
|
|
require(log.is_file() and not log.is_symlink(),'Unexpected log link '+str(log))
|
|
copies[label+'/'+log.name]=log
|
|
proposal_bindings[label]={'manifest_sha256':sha(manifest_path),'patch_sha256':sha(patch),'files':selected}
|
|
deferred=load(WORK/'native-deferred.json');require(deferred['native_check_started'] is False,'Unexpected native scope')
|
|
report={'complete':True,'scope':'Configuration and finite supply recovery plus bounded resource audit correction; no new native qualification or package change','suite':{'exit':0,'inputs_unchanged':True,'regression_programs':len(programs),'suite_inputs_sha256':sha(WORK/'suite-inputs.json'),'suite_result_sha256':sha(WORK/'suite-result.json')},'integration_sha256':sha(WORK/'integration.json'),'proposals':proposal_bindings,'preservation':{'baseline_paths':len(baseline['files']),'inherited_unchanged_except_integrated':True,'index_sha256':index,'original_files':533,'original_manifest_sha256':sha(REFERENCE),'original_unchanged':True,'sealed_candidates':candidates},'native':deferred,'retained_initial_suite_failure':{'exit':failed['exit'],'regression_programs':failed['regression_programs'],'suite_result_sha256':sha(WORK/'first-suite-failure/suite-result.json')},'archive_files':{name:sha(path) for name,path in sorted(copies.items())}}
|
|
print(json.dumps({'ready':True,'destination':str(DEST),'files':len(copies),'programs':len(programs),'apply':args.apply},indent=2))
|
|
if not args.apply:return
|
|
# All binding/preservation checks finish before the first destination mutation.
|
|
DEST.parent.mkdir(parents=True,exist_ok=True)
|
|
staging=Path(tempfile.mkdtemp(prefix='.config-supply-recovery-',dir=DEST.parent))
|
|
try:
|
|
for name,path in copies.items():
|
|
out=staging/name;out.parent.mkdir(parents=True,exist_ok=True);shutil.copyfile(path,out)
|
|
require(sha(out)==report['archive_files'][name],'Copy verification failed '+name)
|
|
(staging/'review.json').write_text(json.dumps(report,indent=2)+'\n')
|
|
shutil.copyfile(Path(__file__),staging/'archive.py')
|
|
(staging/'README.md').write_text('# Configuration and supply recovery — 3 October 2026\n\n'
|
|
'Malformed recognized configuration sections now prevent unrelated saves from replacing the only valid backup. Ordinary rifle resupply retains exact claims while its existing native transfer remains unresolved.\n\n'
|
|
+str(len(programs))+' registered regression programs passed with unchanged captured inputs. Focused baseline failures and corrected-method controls are retained under config/ and supply/. Read review.json for exact source, input and preservation bindings.\n\n'
|
|
'The initial suite failure is retained under root/first-suite-failure/. Shared scanners now distinguish one unchanged, hash-pinned optional prefab identity comparison from hard resource references; the actual vanilla Character_Base load has an ordinary audited resource entry. Optional runtime dependency closure is unchanged. The proposal and scanner baseline failures are retained under optional-identity/.\n\n'
|
|
'Native checks were deferred because an existing game was running. No new native compilation, physical transfer, manual escape, multiplayer or FPS qualification is claimed. Original missions and four previous sealed packages remain unchanged.\n')
|
|
require(not DEST.exists(),'Destination appeared during finalization')
|
|
staging.rename(DEST)
|
|
seal()
|
|
except BaseException:
|
|
shutil.rmtree(staging);raise
|
|
if __name__=='__main__':main()
|