| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465 |
- /** Read-only audit of the public board against the local released Skill. */
- import fs from 'node:fs';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import { fileURLToPath } from 'node:url';
- import { loadPublicBoard, publicBoardAction } from '../src/services/workshop-data.js';
- import { buildModel } from '../src/core/kfs/build.js';
- import { reportCenter } from '../src/core/library/reports.js';
- import { workshopReview, workshopOutputRefs } from '../src/core/workshop-outputs.js';
- const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
- const args = process.argv.slice(2), outIndex = args.indexOf('--out');
- if (outIndex < 0 || !args[outIndex + 1]) throw Error('Usage: node scripts/audit-workshop-report-parity.mjs --out <local-json-path>');
- const output = path.resolve(args[outIndex + 1]);
- const base = 'https://kangshifu.fmode.cn', appId = 'FKieRbIoGt_main';
- const hash = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
- const raw = await loadPublicBoard({ url: base + '/api/functions', appId, fetchImpl: fetch });
- const groups = await publicBoardAction({ url: base + '/api/functions', appId, name: 'ksfGetPublicMemberGroups', params: {}, fetchImpl: fetch });
- const db = buildModel(raw), center = reportCenter(db);
- const view = workshopReview(db, groups, { mode: 'all', history: '1' });
- const productionAssets = await Promise.all(['src/core/library/types.js', 'src/core/library/reports.js', 'src/pages/view-workshop-outputs.js'].map(async asset => {
- const response = await fetch(base + '/' + asset, { signal: AbortSignal.timeout(15000) });
- if (!response.ok) throw Error(`Public asset failed: ${asset} (${response.status})`);
- const text = await response.text();
- if (!text.includes('export ')) throw Error(`Public asset is not JavaScript: ${asset}`);
- return { path: asset, sha256: hash(text), matchingLines: text.split(/\r?\n/)
- .filter(line => /assessment_report:|key: 'assessment'|import .*WORKSHOP|filter\('output'|filter\('reportType'/.test(line)) };
- }));
- const packageRoot = path.join(root, 'docs/ksf-workshop-skill-20261110-r13-r2/package/skills/ksf-workshop-runtime');
- const localRoot = path.join(root, 'skills/ksf-workshop-runtime');
- const manifest = fs.readFileSync(path.join(packageRoot, 'FILES.sha256'), 'utf8');
- const declared = manifest.trim().split(/\r?\n/).map(line => line.match(/^([a-f0-9]{64})\s+\*?(.+)$/)).filter(Boolean);
- const differingFiles = declared.filter(([, expected, file]) => !fs.existsSync(path.join(localRoot, file)) || hash(fs.readFileSync(path.join(localRoot, file))) !== expected).map(([, , file]) => file);
- const course = fs.readFileSync(path.join(localRoot, 'references/workshop-course-flow.md'), 'utf8');
- const curriculum = [...course.matchAll(/^\| (D[12]-(?:\d\d|N)) \| \d+ \| [^|]+ \| (S[1-5]) \| ([^\n]+)\|/gm)]
- .flatMap(([, module, stage, types]) => [...types.matchAll(/`(\w+)`/g)].map(([, type]) => ({ module, stage, type })));
- const tally = (rows, key) => rows.reduce((totals, row) => { const name = key(row); totals[name] = (totals[name] || 0) + 1; return totals; }, {});
- const result = {
- schema: 'ksf.workshop-report-parity-audit.v1', checkedAt: new Date().toISOString(), productionAsOf: raw.loadedAt,
- productionBusinessWrites: 0, productionConfigurationChanges: 0,
- scope: 'Public production data and assets; local released Skill and local board model. No production save or host execution.',
- skill: { release: 'ksf-workshop-skill-20261110-r13-r2', entrySha256: hash(fs.readFileSync(path.join(localRoot, 'SKILL.md'))),
- declaredFilesIncludingManifest: declared.length + 1, differingFiles,
- manifestMatchesReleased: hash(fs.readFileSync(path.join(localRoot, 'FILES.sha256'))) === hash(manifest), curriculum },
- productionAssets,
- publicCounts: Object.fromEntries(['project', 'artifact', 'insight', 'source', 'session', 'opportunity'].map(name => [name, raw[name].length])),
- reportCenter: { totalEntries: center.total, kinds: tally(center.entries, entry => entry.kind),
- allPublicArtifactsIncluded: db.artifacts.every(artifact => center.entries.some(entry => entry.kind === 'artifact' && entry.id === artifact.id)) },
- artifacts: { total: db.artifacts.length, withStructuredReport: db.artifacts.filter(artifact => artifact.report).length,
- withoutStructuredReport: db.artifacts.filter(artifact => !artifact.report).length,
- byType: tally(db.artifacts, artifact => artifact.type), byMode: tally(db.artifacts, artifact => artifact.dataMode || 'unknown'),
- missingSeries: db.artifacts.filter(artifact => !artifact.reportSeriesKey).length,
- sevenOutputLocatorCounts: Object.fromEntries(['market', 'social', 'evidence', 'opportunities', 'validation', 'value', 'proposal']
- .map(key => [key, db.artifacts.filter(artifact => workshopOutputRefs(artifact).refs.some(ref => ref.key === key)).length])) },
- localBoardModelAgainstProduction: { note: 'This is the local model, not evidence that the changed UI was deployed.',
- allModesAndHistoryCount: view.rows.length, nestedPersonalReportCount: view.hierarchy.flatMap(group => group.members.flatMap(member => member.reports)).length,
- groupCount: groups.groups.length, ungroupedReportCount: view.ungrouped, defaultLiveLatestCount: workshopReview(db, groups).rows.length },
- reportInventory: db.artifacts.map(artifact => ({ id: artifact.id, title: artifact.title, type: artifact.type, dataMode: artifact.dataMode || 'unknown',
- hasStructuredReport: Boolean(artifact.report), rawTextChars: artifact.rawText.length, reportSeriesKey: artifact.reportSeriesKey,
- version: artifact.version, chapterIds: (artifact.report?.chapters || []).map(chapter => chapter.id || '') })),
- };
- fs.mkdirSync(path.dirname(output), { recursive: true });
- fs.writeFileSync(output, JSON.stringify(result, null, 2) + '\n');
- console.log(JSON.stringify({ output, asOf: result.productionAsOf, reportCenter: result.reportCenter, artifacts: result.artifacts,
- localBoardModelAgainstProduction: result.localBoardModelAgainstProduction, skillDifferingFiles: differingFiles, productionBusinessWrites: 0 }, null, 2));
|