"""Build r6 from the immutable r5 release plus explicit request-guard changes only.""" import argparse import difflib from datetime import datetime, timezone import hashlib import json from pathlib import Path import shutil import sqlite3 import zipfile ROOT = Path(__file__).resolve().parents[1] BASE = ROOT / 'artifacts/ksf-industry-monitor-20261107-r5/package' WORK = ROOT / 'work/industry-monitoring-20261107-r1' RELEASE = 'ksf-industry-monitor-20261107-r6' INTEGRATION = """ from monitoring_dispatch import projection as dispatch_projection pipeline['requestGuard'] = dispatch_projection(self.task_store, self.data['generation']) """ def sha(raw): return hashlib.sha256(raw).hexdigest() def main(): p = argparse.ArgumentParser(description=__doc__) p.add_argument('--out', required=True, type=Path) p.add_argument('--seal', action='store_true', help='Seal an accepted candidate after receipts are written') args = p.parse_args() out = args.out.resolve() if args.seal: release = json.loads((out / 'RELEASE.json').read_text('utf-8')) receipt = json.loads((out / 'evidence/implementation-receipt-v6.json').read_text('utf-8')) if release['release'] != RELEASE or not receipt.get('browserPassed') or receipt.get('tests', {}).get('python') != 61: raise ValueError('CANDIDATE_ACCEPTANCE_REQUIRED') for name, digest in receipt['testedFilesSha256'].items(): if sha((out / name).read_bytes()) != digest: raise ValueError('TESTED_SOURCE_CHANGED') archive = out.parent / (RELEASE + '.zip') if archive.exists(): raise ValueError('SEALED_ARCHIVE_ALREADY_EXISTS') files = sorted(path for path in out.rglob('*') if path.is_file() and path != out / 'FILES.sha256') (out / 'FILES.sha256').write_text(''.join(sha(path.read_bytes()) + ' ' + path.relative_to(out).as_posix() + '\n' for path in files), 'utf-8') with zipfile.ZipFile(archive, 'x', zipfile.ZIP_DEFLATED) as z: for path in sorted(out.rglob('*')): if path.is_file(): z.write(path, path.relative_to(out).as_posix()) print(json.dumps({'ok': True, 'archive': str(archive), 'bytes': archive.stat().st_size, 'sha256': sha(archive.read_bytes()), 'productionDeployed': False, 'paidCalls': 0}, ensure_ascii=False)) return if out.exists(): raise ValueError('RELEASE_DIRECTORY_MUST_BE_NEW') # Do not copy unrelated work-in-progress innovation views or shared module changes. for line in (BASE / 'FILES.sha256').read_text('utf-8').splitlines(): digest, name = line.split(' ', 1) if sha((BASE / name).read_bytes()) != digest: raise ValueError('R5_FROZEN_BASELINE_CHANGED') shutil.copytree(BASE, out) before = (BASE / 'server/voc-library/industry_monitor.py').read_text('utf-8') anchor = " self.task_query.get('pipelinePage', 1), self.task_query.get('pipelineRevision'))\n" if before.count(anchor) != 1: raise ValueError('R5_INTEGRATION_ANCHOR_CHANGED') updated = before.replace(anchor, anchor + INTEGRATION) (out / 'server/voc-library/industry_monitor.py').write_text(updated, 'utf-8') names = ['server/voc-library/monitoring_dispatch.py', 'server/voc-library/test_monitoring_dispatch.py', 'scripts/manage-industry-dispatch.py'] for name in names: shutil.copyfile(ROOT / name, out / name) shutil.copyfile(ROOT / 'src/pages/industry-pipeline-view.js', out / 'payload/src/pages/industry-pipeline-view.js') # Verify the only UI change to the frozen module is the guard section. old_ui = (BASE / 'payload/src/pages/industry-pipeline-view.js').read_text('utf-8') new_ui = (out / 'payload/src/pages/industry-pipeline-view.js').read_text('utf-8') lines = [line for line in new_ui.splitlines(True) if 'class="request-guard"' not in line] if ''.join(lines) != old_ui: raise ValueError('UNRELATED_PIPELINE_UI_CHANGES') with (out / 'evidence/ui.patch').open('a', encoding='utf-8') as stream: stream.write(''.join(difflib.unified_diff(old_ui.splitlines(True), new_ui.splitlines(True), fromfile='r5/src/pages/industry-pipeline-view.js', tofile='r6/src/pages/industry-pipeline-view.js'))) shutil.copyfile(ROOT / 'docs/industry-monitoring-20261107/request-guard.md', out / 'docs/request-guard.md') for name in ('dispatch-closeout-import.json', 'dispatch-paid-replay-verification.json'): shutil.copyfile(WORK / name, out / 'evidence' / name) # Auxiliary working copy only. The active research snapshot is never packaged or altered. dest = out / 'data/monitoring-registry/monitoring.sqlite' with sqlite3.connect((WORK / 'monitoring-registry/monitoring.sqlite').resolve().as_uri() + '?mode=ro', uri=True) as source: with sqlite3.connect(dest) as c: source.backup(c) c.execute("UPDATE pipeline_workers SET state='ARCHIVED_COPY'") c.execute("UPDATE dispatch_audits SET state='ARCHIVED_COPY'") c.commit() for file in (WORK / 'monitoring-registry/assets').iterdir(): target = out / 'data/monitoring-registry/assets' / file.name if not target.exists(): shutil.copyfile(file, target) # Refresh release identity and the publisher's source-byte manifest; preserve all public baselines. manifest_path = out / 'deploy/industry-ui-files.json' manifest = json.loads(manifest_path.read_text('utf-8')) manifest['release'] = RELEASE for entry in manifest['files']: entry['sha256'] = sha((out / 'payload' / entry['path']).read_bytes()) manifest_path.write_text(json.dumps(manifest, ensure_ascii=False, indent=2) + '\n', 'utf-8') publisher = out / 'scripts/publish-industry-ui.mjs' publisher.write_text(publisher.read_text('utf-8').replace('ksf-industry-monitor-20261107-r5', RELEASE), 'utf-8') for directory in ('docs', 'deploy'): for path in (out / directory).iterdir(): if path.is_file() and path.suffix in ('.md', '.example'): path.write_text(path.read_text('utf-8-sig').replace('ksf-industry-monitor-20261107-r5', RELEASE), 'utf-8') with (out / 'docs/README.md').open('a', encoding='utf-8') as stream: stream.write('\n\n## r6:请求前核对与认领\n\n新增已结算76条请求保护、批次查重、并发核对认领和停费状态投影;操作见 [request-guard.md](request-guard.md)。上游执行端尚未接入,收费保持停止。本包仅在已冻结r5上追加本轮变更。\n') release_path = out / 'RELEASE.json' release = json.loads(release_path.read_text('utf-8')) release.update(release=RELEASE, createdAt=datetime.now(timezone.utc).isoformat(), builtFromFrozenRelease='ksf-industry-monitor-20261107-r5', requestGuardImplemented=True, externalCollectorsIntegrated=False, newPaidCalls=0, settledCloseoutIdentifiedRows=76, overallComplete=False) release['backendFiles'].append('server/voc-library/monitoring_dispatch.py') release_path.write_text(json.dumps(release, ensure_ascii=False, indent=2) + '\n', 'utf-8') # Browser acceptance and final test receipts are added before sealing, never after ZIP creation. print(json.dumps({'ok': True, 'candidate': str(out), 'sealed': False, 'paidCalls': 0}, ensure_ascii=False)) if __name__ == '__main__': main()