"""Prepare only the workshop board UI against the current public site, excluding Skill/manual/runtime.""" from concurrent.futures import ThreadPoolExecutor import difflib import hashlib import json from pathlib import Path import posixpath import re from urllib.request import Request, urlopen from urllib.error import HTTPError ROOT = Path(__file__).resolve().parents[1] RELEASE = 'ksf-workshop-board-20261110-r1' OUT = ROOT / 'docs' / RELEASE PACK = OUT / 'package' WORK = ROOT / 'work' / RELEASE FILES = [ 'src/core/kfs/build.js', 'src/core/library/reports.js', 'src/core/library/types.js', 'src/core/workshop-outputs.js', 'src/layout/kf-app.js', 'src/layout/kf-nav.js', 'src/pages/view-reports.js', 'src/pages/view-workshop-outputs.js', 'src/utils/router.js', ] def sha(raw): return hashlib.sha256(raw).hexdigest() def normal(raw): return raw.replace(b'\r\n', b'\n') def write_json(path, value): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') def capture(name): try: req = Request('https://kangshifu.fmode.cn/' + name, headers={'Cache-Control': 'no-cache'}) with urlopen(req, timeout=40) as response: raw = response.read(3_000_001) assert len(raw) < 3_000_001 content_type = response.headers.get('Content-Type', '') exists = name == 'index.html' or 'javascript' in content_type or 'text/css' in content_type if exists: target = WORK / 'baseline' / name target.parent.mkdir(parents=True, exist_ok=True) target.write_bytes(raw) return {'path': name, 'exists': exists, 'status': response.status, 'contentType': content_type, 'sha256': sha(raw) if exists else None, 'normalizedSha256': sha(normal(raw)) if exists else None} except HTTPError as e: assert e.code == 404, (name, e.code) return {'path': name, 'exists': False, 'status': 404, 'sha256': None, 'normalizedSha256': None} def dependencies(observed): visited, queue = set(), list(FILES) while queue: batch = sorted(set(queue) - visited) queue = [] absent = [name for name in batch if name not in observed] with ThreadPoolExecutor(max_workers=8) as pool: for row in pool.map(capture, absent): observed[row['path']] = row for name in batch: visited.add(name) source = ROOT / name if name in FILES else WORK / 'baseline' / name assert source.exists(), 'MISSING_PRODUCTION_DEPENDENCY:' + name for relative in re.findall(r'(?:from\s*|import\s*)[\'\"]([.][^\'\"]+)[\'\"]', source.read_text('utf-8')): queue.append(posixpath.normpath(posixpath.join(posixpath.dirname(name), relative))) return visited - set(FILES) def main(): assert not PACK.exists(), 'NEW_RELEASE_REQUIRED' with ThreadPoolExecutor(max_workers=8) as pool: rows = list(pool.map(capture, sorted(set(FILES) | {'index.html'}))) observed = {r['path']: r for r in rows} dependencies_set = dependencies(observed) rows = [observed[name] for name in sorted(observed)] differences = [name for name in dependencies_set if not (ROOT / name).exists() or observed[name]['normalizedSha256'] != sha(normal((ROOT / name).read_bytes()))] write_json(WORK / 'baseline.json', {'files': rows, 'localDifferencesExcludedFromRelease': differences, 'dependencySource': 'existing-production-modules; not uncommitted unrelated changes'}) patches, files = [], [] for name in sorted(FILES): prior = observed[name] raw = normal((ROOT / name).read_bytes()) old = normal((WORK / 'baseline' / name).read_bytes()) if prior['exists'] else b'' dest = PACK / 'payload' / name dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes(raw) files.append({'path': name, 'sha256': sha(raw), 'allowMissing': not prior['exists'], 'priorNormalizedSha256': [prior['normalizedSha256']] if prior['exists'] else [], 'observedProductionSha256': prior['sha256'], 'changed': old != raw}) patches.extend(difflib.unified_diff(old.decode().splitlines(True), raw.decode().splitlines(True), fromfile='production/' + name, tofile=RELEASE + '/' + name)) # Reuse the previously tested publisher, including 0644 writes and concurrent-change checks. publisher = (ROOT / 'artifacts/ksf-board-mobile-filters-20261108-r14/package/scripts/publish-industry-ui.mjs').read_text('utf-8') line = next(line for line in publisher.splitlines() if line.startswith('export const FILES=')) publisher = publisher.replace(line, 'export const FILES=Object.freeze(' + json.dumps(sorted(FILES)) + ');') publisher = publisher.replace('ksf-board-mobile-filters-20261108-r14', RELEASE) target = PACK / 'scripts/publish-industry-ui.mjs' target.parent.mkdir(parents=True, exist_ok=True) target.write_text(publisher, encoding='utf-8', newline='\n') for name in ['rollback-ui.mjs', 'check-site-modes.mjs']: text = (ROOT / 'scripts/release-templates' / name).read_text('utf-8').replace('ksf-board-mobile-filters-20261108-r14', RELEASE) (PACK / 'scripts' / name).write_text(text, encoding='utf-8', newline='\n') write_json(PACK / 'deploy/industry-ui-files.json', {'version': 'ksf.industry-ui.publish.v1', 'release': RELEASE, 'homepageSha256': observed['index.html']['sha256'], 'files': files}) write_json(PACK / 'evidence/production-baseline.json', {'files': rows}) (PACK / 'evidence/ui.patch').write_text(''.join(patches), encoding='utf-8') for name in dependencies_set: dest = PACK / 'test-support/site' / name dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes((WORK / 'baseline' / name).read_bytes()) for name in FILES: dest = PACK / 'test-support/site' / name dest.parent.mkdir(parents=True, exist_ok=True) dest.write_bytes((PACK / 'payload' / name).read_bytes()) test = (ROOT / 'test/workshop-outputs.test.js').read_text('utf-8').replace('../src/', '../test-support/site/src/') dest = PACK / 'test/workshop-outputs.test.js' dest.parent.mkdir(parents=True, exist_ok=True) dest.write_text(test, encoding='utf-8') write_json(PACK / 'package.json', {'type': 'module', 'private': True}) write_json(OUT / 'preparation.json', {'release': RELEASE, 'staticFiles': len(files), 'dependencyFiles': len(dependencies_set), 'newFiles': sum(f['allowMissing'] for f in files), 'changedFiles': sum(f['changed'] for f in files), 'homepageSha256': observed['index.html']['sha256'], 'mainSkillIncluded': False, 'manualIncluded': False, 'productionDeployed': False}) print(json.dumps({'ok': True, 'files': len(files), 'dependenciesVerified': len(dependencies_set), 'changedFiles': sum(f['changed'] for f in files), 'package': str(PACK)}, ensure_ascii=False)) if __name__ == '__main__': main()