prepare-workshop-report-parity-local.mjs 4.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243
  1. /** Freeze a reviewable local candidate. This script has no network or deployment actions. */
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import crypto from 'node:crypto';
  5. import assert from 'node:assert/strict';
  6. const root = path.resolve('.'), skill = 'skills/ksf-workshop-runtime';
  7. const out = 'docs/workshop-report-parity-20261111/candidate';
  8. assert.ok(!fs.existsSync(out), '候选目录已存在;不可覆盖已冻结快照');
  9. const digest = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
  10. const original = 'docs/ksf-workshop-skill-20261110-r13-r2/package/' + skill;
  11. const protectedFiles = ['runtime-lock.json', ...['ksf.mjs','ksf-secure.mjs','ksf-workshop.mjs','ksf-identity.mjs','ksf-identity-bridge.mjs','describe-business-schema.mjs'].map(file => 'scripts/' + file)];
  12. for (const file of protectedFiles) assert.ok(fs.readFileSync(skill + '/' + file).equals(fs.readFileSync(original + '/' + file)), '受保护文件发生变化:' + file);
  13. const declared = fs.readFileSync(skill + '/FILES.sha256', 'utf8').trim().split(/\r?\n/).map(line => line.match(/^[a-f0-9]{64}\s+\*?(.+)$/)?.[1]);
  14. assert.ok(declared.every(file => file && !file.includes('..') && !path.isAbsolute(file)));
  15. const entries = [...new Set([...declared, 'scripts/report-contract.mjs', 'scripts/save-report.mjs', 'references/workshop-report-parity.md'])].sort();
  16. fs.writeFileSync(skill + '/FILES.sha256', entries.map(file => digest(fs.readFileSync(skill + '/' + file)) + ' ' + file).join('\n') + '\n');
  17. const links = [];
  18. for (const file of entries.filter(file => file.endsWith('.md'))) {
  19. const text = fs.readFileSync(skill + '/' + file, 'utf8');
  20. for (const [, target] of text.matchAll(/\[[^\]]+\]\(([^)]+)\)/g)) {
  21. if (/^(https?:|#|<)/.test(target)) continue;
  22. assert.ok(fs.existsSync(path.resolve(skill, path.dirname(file), target.split('#')[0])), '技能相对链接不存在:' + file + ' → ' + target);links.push({ file, target });
  23. }
  24. }
  25. const frontend = ['src/core/kfs/build.js','src/core/library/reports.js','src/core/library/types.js','src/core/report-delivery.js',
  26. 'src/core/report-attachments.js','src/core/report.js','src/core/workshop-outputs.js','src/pages/report-card.js','src/pages/view-report.js',
  27. 'src/pages/view-report-type.js','src/pages/view-reports.js','src/pages/view-workshop-outputs.js','src/layout/kf-app.js',
  28. 'src/layout/kf-nav.js','src/utils/router.js',skill + '/scripts/report-contract.mjs'];
  29. const backend = ['ksfSaveWorkshopTask','ksfGetIngestContract','ksfGetPublicBoard','ksfReadFile','ksfPrepareFileUpload'];
  30. const files = [...new Set([...entries.map(file => skill + '/' + file),skill + '/FILES.sha256',...frontend,
  31. ...backend.map(name => 'server/dist/' + name + '.js'),'server/references/runtime-data.json',
  32. 'docs/workshop-quality-r13-20261110/final-康师傅工作坊操作手册.html'])];
  33. const manifest = files.map(file => {
  34. const bytes = fs.readFileSync(file), target = out + '/' + file;fs.mkdirSync(path.dirname(target), { recursive: true });fs.writeFileSync(target, bytes);
  35. return { path: file, bytes: bytes.length, sha256: digest(bytes) };
  36. });
  37. const result = { schema: 'ksf.workshop-report-parity-local.v1', createdAt: new Date().toISOString(), version: 'r14-local',
  38. localCandidateOnly: true, productionDeployed: false, runtimeChanges: 0, protectedFilesVerified: protectedFiles,
  39. skillPublicFiles: entries.length + 1, skillRelativeLinksVerified: links.length,
  40. frontend, backend, schemaMigrations: 0,
  41. deploymentNote: '快照是待审本地候选,不含生产基线、生产凭据或自动发布器;部署须按目标当前代码建立定向计划并保留并发与回滚保护。后端/前端/Skill需成套升级,不能仅复制静态页面。', files: manifest };
  42. fs.writeFileSync(out + '/manifest.json', JSON.stringify(result, null, 2) + '\n');
  43. console.log(JSON.stringify({ out, files: manifest.length, skillPublicFiles: result.skillPublicFiles, relativeLinksVerified: links.length, protectedFilesVerified: protectedFiles.length, productionDeployed: false }));