| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- """Freeze r7 on immutable r6, applying only the product-detail evidence changes."""
- import argparse
- import ast
- from datetime import datetime, timezone
- import difflib
- import json
- from pathlib import Path
- import shutil
- import sqlite3
- import zipfile
- import hashlib
- ROOT = Path(__file__).resolve().parents[1]
- BASE = ROOT / 'artifacts/ksf-industry-monitor-20261107-r6/package'
- WORK = ROOT / 'work/industry-monitoring-20261107-r1'
- RELEASE = 'ksf-industry-monitor-20261107-r7'
- def sha(data):
- return hashlib.sha256(data).hexdigest()
- def once(text, old, new):
- if text.count(old) != 1:
- raise ValueError('FROZEN_INTEGRATION_ANCHOR_CHANGED:' + old[:100])
- return text.replace(old, new)
- def function(text, name):
- node = next(n for n in ast.walk(ast.parse(text)) if isinstance(n, ast.FunctionDef) and n.name == name)
- return ''.join(text.splitlines(True)[node.lineno - 1:node.end_lineno])
- def build(out):
- if out.exists():
- raise ValueError('RELEASE_DIRECTORY_MUST_BE_NEW')
- 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('R6_FROZEN_BASELINE_CHANGED')
- shutil.copytree(BASE, out)
- patch = []
- def save(relative, before, after):
- (out / relative).write_text(after, 'utf-8')
- patch.extend(difflib.unified_diff(before.splitlines(True), after.splitlines(True), fromfile='r6/' + relative, tofile='r7/' + relative))
- relative = 'server/voc-library/industry_monitor.py'
- before = (BASE / relative).read_text('utf-8')
- current = (ROOT / relative).read_text('utf-8')
- old_method, new_method = function(before, 'listings'), function(current, 'listings')
- # Prove the copied method contains only the explicit detail-evidence additions.
- stripped = once(new_method, ', page=1, detail_query=None):', ', page=1):')
- stripped = once(stripped, " from monitoring_details import projection as detail_projection\n detail_evidence = detail_projection(self.task_store, self.data['generation'], ids, detail_query, TITLE_RULES)\n", '')
- stripped = once(stripped, ", 'detailEvidence': detail_evidence", '')
- if stripped != old_method:
- raise ValueError('UNRELATED_LISTINGS_METHOD_CHANGES')
- after = once(before, old_method, new_method)
- after = once(after, "'listingPage', 'taskState'", "'listingPage', 'detailQuery', 'taskState'")
- after = once(after, "{'titleClaim', 'listingPage'}.intersection(request)", "{'titleClaim', 'listingPage', 'detailQuery'}.intersection(request)")
- after = once(after, "request.get('listingPage', 1))", "request.get('listingPage', 1), request.get('detailQuery'))")
- contract = " result['detailQuery'] = " + current.split(" result['detailQuery'] = ", 1)[1].split('\n elif', 1)[0] + '\n'
- after = once(after, " elif op == 'overview': result = monitor.overview()", contract + " elif op == 'overview': result = monitor.overview()")
- save(relative, before, after)
- relative = 'payload/src/pages/view-industry.js'
- before = (BASE / relative).read_text('utf-8')
- current_ui = (ROOT / 'src/pages/view-industry.js').read_text('utf-8')
- after = once(before, "import { rankingDiscoveryView } from './industry-ranking-view.js';", "import { rankingDiscoveryView } from './industry-ranking-view.js';\nimport { detailEvidenceView, DETAIL_CSS } from './industry-detail-view.js';")
- anchor = " if (claim) claim.value = this.routeQuery.titleClaim || '';"
- after = once(after, anchor, anchor + "\n const detailState = this.renderRoot.querySelector('select[data-detail-state]');\n if (detailState) detailState.value = this.routeQuery.detailState || '';")
- old = " if (Object.keys(patch).some(k => ['listingQ', 'titleClaim'].includes(k))) patch = { ...patch, listingPage: '' };"
- new = current_ui.split(" if (Object.keys(patch).some(k => ['listingQ', 'titleClaim']", 1)[1].split(" if (Object.keys(patch).some(k => ['reviewQ'", 1)[0]
- after = once(after, old + '\n', " if (Object.keys(patch).some(k => ['listingQ', 'titleClaim']" + new)
- anchor = ' if (this.routeQuery.listingPage) request.listingPage = Number(this.routeQuery.listingPage);'
- detail_request = current_ui.split(' request.detailQuery = {};', 1)[1].split('\n }', 1)[0]
- after = once(after, anchor, anchor + '\n request.detailQuery = {};' + detail_request)
- reset = next(line for line in current_ui.splitlines() if "if (this._tab === 'listings') return html`<button" in line)
- after = once(after, ' _researchResetView() {', ' _researchResetView() {\n' + reset)
- after = once(after, 'return html`${this._listingRecordsView(d)}<section', 'return html`${this._listingRecordsView(d)}${detailEvidenceView(this, d.detailEvidence)}<section')
- after = once(after, 'static styles = [VIEW_CSS, css`', 'static styles = [VIEW_CSS, DETAIL_CSS, css`')
- save(relative, before, after)
- relative = 'payload/src/services/industry-service.js'
- before = (BASE / relative).read_text('utf-8')
- additions = '\n'.join(line for line in (ROOT / 'src/services/industry-service.js').read_text('utf-8').splitlines() if line.startswith(' DETAIL_')) + '\n'
- save(relative, before, once(before, 'const MESSAGES = {\n', 'const MESSAGES = {\n' + additions))
- for name in ('server/voc-library/monitoring_details.py', 'server/voc-library/test_monitoring_details.py',
- 'scripts/register-industry-details.py'):
- shutil.copyfile(ROOT / name, out / name)
- shutil.copyfile(ROOT / 'src/pages/industry-detail-view.js', out / 'payload/src/pages/industry-detail-view.js')
- shutil.copyfile(ROOT / 'docs/industry-monitoring-20261107/detail-evidence.md', out / 'docs/detail-evidence.md')
- shutil.copyfile(WORK / 'detail-bootstrap.json', out / 'evidence/detail-bootstrap.json')
- (out / 'evidence/detail-evidence.patch').write_text(''.join(patch), 'utf-8')
- # Copy only the auxiliary registry; complete detail originals stay in the private local archive.
- with sqlite3.connect((WORK / 'monitoring-registry/monitoring.sqlite').resolve().as_uri() + '?mode=ro', uri=True) as source:
- with sqlite3.connect(out / 'data/monitoring-registry/monitoring.sqlite') 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():
- dest = out / 'data/monitoring-registry/assets' / file.name
- if not dest.exists():
- shutil.copyfile(file, dest)
- if (out / 'data/monitoring-registry/detail-originals').exists():
- raise ValueError('PRIVATE_DETAIL_ORIGINALS_MUST_NOT_BE_PACKAGED')
- manifest_path = out / 'deploy/industry-ui-files.json'
- manifest = json.loads(manifest_path.read_text('utf-8'))
- manifest['release'] = RELEASE
- manifest['files'].append({'path': 'src/pages/industry-detail-view.js', 'allowMissing': True,
- 'priorNormalizedSha256': [], 'observedProductionSha256': None})
- for entry in manifest['files']:
- previous = BASE / 'payload' / entry['path']
- if previous.is_file():
- prior = sha(previous.read_bytes().replace(b'\r\n', b'\n'))
- if prior not in entry['priorNormalizedSha256']:
- entry['priorNormalizedSha256'].append(prior)
- 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'
- script = publisher.read_text('utf-8').replace('ksf-industry-monitor-20261107-r6', RELEASE)
- old = next(line for line in script.splitlines() if line.startswith('export const FILES='))
- script = once(script, old, 'export const FILES=Object.freeze(' + json.dumps([e['path'] for e in manifest['files']]) + ');')
- publisher.write_text(script, '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-r6', RELEASE), 'utf-8')
- with (out / 'docs/README.md').open('a', encoding='utf-8') as stream:
- stream.write('\n\n## r7:商品详情证据\n\n新增详情原文、字段空值/缺失、图片顺序、同响应主题组合候选及Agent读取合同。操作与覆盖见[detail-evidence.md](detail-evidence.md)。4份历史原件均无详情卖点文字,不称全量覆盖。包内仅含字段投影,完整详情原件留在本机受限档案。\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-r6',
- detailEvidenceImplemented=True, detailObservations=4, productsWithDetailText=0,
- privateDetailOriginalsIncluded=False, overallComplete=False)
- release['backendFiles'].append('server/voc-library/monitoring_details.py')
- release['staticFiles'].append('src/pages/industry-detail-view.js')
- release_path.write_text(json.dumps(release, ensure_ascii=False, indent=2) + '\n', 'utf-8')
- print(json.dumps({'ok': True, 'candidate': str(out), 'sealed': False, 'paidCalls': 0}))
- def seal(out):
- receipt = json.loads((out / 'evidence/implementation-receipt-v7.json').read_text('utf-8'))
- if not receipt.get('browserPassed') or receipt.get('tests', {}).get('python') != 71 or not receipt.get('packageVerificationPassed'):
- 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(p for p in out.rglob('*') if p.is_file() and p != out / 'FILES.sha256')
- (out / 'FILES.sha256').write_text(''.join(sha(p.read_bytes()) + ' ' + p.relative_to(out).as_posix() + '\n' for p 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}))
- if __name__ == '__main__':
- p = argparse.ArgumentParser(description=__doc__)
- p.add_argument('--out', type=Path, required=True)
- p.add_argument('--seal', action='store_true')
- args = p.parse_args()
- (seal if args.seal else build)(args.out.resolve())
|