audit-workshop-report-coverage-r13.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. /** Read-only production inspection plus isolated in-memory save/read/projection check.
  2. * Never uses service credentials, production write APIs, or a PostgreSQL connection.
  3. */
  4. import fs from 'node:fs/promises';
  5. import path from 'node:path';
  6. import crypto from 'node:crypto';
  7. import assert from 'node:assert/strict';
  8. import { createApplication } from '../server/src/core.mjs';
  9. import { attachWorkshopHandlers } from '../server/src/workshop.mjs';
  10. import { runWorkshop } from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
  11. import { buildModel } from '../src/core/kfs/build.js';
  12. import { WORKSHOP_OUTPUTS, workshopReview } from '../src/core/workshop-outputs.js';
  13. import { REPORT_TYPES } from '../src/core/library/types.js';
  14. import { entriesForType } from '../src/core/library/reports.js';
  15. import { loadPublicBoard, publicBoardAction } from '../src/services/workshop-data.js';
  16. const root = path.resolve('docs/workshop-report-coverage-r13-20261110');
  17. const input = path.resolve('docs/workshop-dialogue-r13-20261110');
  18. await fs.mkdir(path.join(root, 'local-drafts'), { recursive: true });
  19. const sha = value => crypto.createHash('sha256').update(value).digest('hex');
  20. const json = async (file, value) => fs.writeFile(path.join(root, file), JSON.stringify(value, null, 2) + '\n');
  21. const data = JSON.parse(await fs.readFile('server/references/runtime-data.json', 'utf8'));
  22. const ledger = JSON.parse(await fs.readFile(path.join(input, '使用的证据与编码.json'), 'utf8'));
  23. const publicSkill = path.resolve('skills/ksf-workshop-runtime');
  24. async function fingerprint(dir, prefix = '') {
  25. const entries = await fs.readdir(dir, { withFileTypes: true });
  26. const result = {};
  27. for (const e of entries) {
  28. const rel = prefix + e.name;
  29. if (e.isDirectory()) Object.assign(result, await fingerprint(path.join(dir, e.name), rel + '/'));
  30. else result[rel] = sha(await fs.readFile(path.join(dir, e.name)));
  31. }
  32. return result;
  33. }
  34. const skillBefore = await fingerprint(publicSkill);
  35. // Actual current files, no invented new research or employee responses.
  36. const definitions = [
  37. ['03-category.txt', 'D1-01', 'S1', 'category_report', 'market-analysis'],
  38. ['04-rules.txt', 'D1-02', 'S1', 'category_report', 'verification-config'],
  39. ['05-competitors.txt', 'D1-03', 'S1', 'competitor_report', 'market-analysis'],
  40. ['06-tasks.txt', 'D1-04', 'S1', 'pain_list', 'research-task-package'],
  41. ['07-research.txt', 'D1-N', 'S2', 'voc_report', 'research-progress'],
  42. ['08-accept.txt', 'D2-01', 'S2', 'voc_report', 'research-acceptance'],
  43. ['09-voc.txt', 'D2-02', 'S2', 'pain_list', 'evidence-pool'],
  44. ['10-matrix.txt', 'D2-03', 'S3', 'assessment_report', 'supply-demand-matrix'],
  45. ['11-opportunities.txt', 'D2-04', 'S3', 'assessment_report', 'opportunity-validation'],
  46. ['12-priority.txt', 'D2-05', 'S4', 'assessment_report', 'opportunity-validation'],
  47. ['13-feasibility.txt', 'D2-06', 'S4', 'assessment_report', 'opportunity-validation'],
  48. ['14-value.txt', 'D2-07', 'S5', 'concept', 'buying-points'],
  49. ['15-proposal.txt', 'D2-08:proposal', 'S5', 'concept', 'product-proposal'],
  50. ];
  51. const chapter = (id, title, text, stage) => ({ id, title, stage,
  52. sections: [{ kind: 'longpara', title: '现有阶段正文', text }] });
  53. const drafts = [];
  54. for (const [file, module, stageCode, artifactType, anchor] of definitions) {
  55. const rawText = await fs.readFile(path.join(input, '阶段产出', file), 'utf8');
  56. const title = rawText.split(/\r?\n/)[0];
  57. const chapters = [chapter(anchor, title, rawText, stageCode)];
  58. if (module === 'D2-02') {
  59. const text = rawText.match(/N5 前置资源。[\s\S]*?(?=\r?\nKANO)/)?.[0];
  60. assert.ok(text);
  61. chapters.push(chapter('social-needs', '有限场景线索,尚非完整社媒需求报告', text, stageCode));
  62. }
  63. if (module === 'D2-08:proposal') {
  64. const text = rawText.match(/V3全池:[\s\S]*?(?=\r?\n五分钟汇报)/)?.[0];
  65. assert.ok(text);
  66. chapters.push(chapter('opportunity-validation', 'V3全池去向', text, stageCode));
  67. }
  68. const base = { sessionId: null, taskKey: 'local-r13-audit:' + module + ':report', version: 1,
  69. stageCode, title, artifactType, category: '拌面', rawText, sourceIds: [],
  70. pendingQuestions: ['仅用于本地结构验证,员工和选择为模拟,未上传企业数据库',
  71. '原件索引尚未通过正常业务接口桥接为KfSource/KfEvidence;不声明已保存真实证据'],
  72. report: { executive: ['来自r13模拟对话的现有正文;正文存在不代表研究质量或真实验证已通过。'], chapters } };
  73. drafts.push({ sourceFile: file, module, kind: 'stage', input: base });
  74. if (module === 'D2-04') {
  75. const matches = [...rawText.matchAll(/^(O[123]) ([^\r\n]+)[\r\n]+([\s\S]*?)(?=^O[123] |^五力对全池)/gm)];
  76. assert.equal(matches.length, 3);
  77. for (const match of matches) {
  78. const [text, key, title, body] = match;
  79. const demandSummary = body.match(/^U:(.+)$/m)?.[1];
  80. const usageScenario = body.match(/^C:(.+)$/m)?.[1];
  81. const fusionDefinition = body.split(/\r?\n/).filter(line => /^[SUC]:/.test(line)).join(' ');
  82. assert.ok(demandSummary && usageScenario);
  83. drafts.push({ sourceFile: file, module: module + ':' + key, kind: 'independent-card-draft', input: {
  84. ...structuredClone(base), taskKey: 'local-r13-audit:' + module + ':' + key, title: key + ' ' + title,
  85. artifactType: 'opportunity_card', rawText: text,
  86. report: { executive: ['从现有模拟正文拆出的独立卡草稿;无新增研究,尚未在生产保存。'],
  87. chapters: [chapter('opportunity-cards', key + ' ' + title, text, stageCode)] },
  88. opportunity: { key: 'local-r13-audit:' + key, title, demandSummary,
  89. targetAudience: key === 'O1' ? '想尝新又保留经典选择的人;目标学生尚未验证'
  90. : key === 'O2' ? '首次接触陌生速食的人;身份和地点尚未验证'
  91. : '购买常备速食并有补货任务的人;真实购物断点未验证',
  92. usageScenario, fusionDefinition },
  93. } });
  94. }
  95. }
  96. }
  97. for (let i = 0; i < drafts.length; i++) {
  98. await json('local-drafts/' + String(i + 1).padStart(2, '0') + '.json', {
  99. schema: 'ksf.local.unsent-workshop-draft.v1', productionReady: false, productionSaved: false,
  100. requiredDataMode: 'sample', notes: 'sessionId待真实授权上下文提供;不可将外层说明作为save字段。来源桥接和正文质量须另验。',
  101. ...drafts[i],
  102. });
  103. }
  104. // All mutations below target this disposable JavaScript object, not a DB server.
  105. function memory() {
  106. let db = {}, seq = 0;
  107. return {
  108. async transaction(fn) { const prior = structuredClone(db); try { return await fn(this); } catch (e) { db = prior; throw e; } },
  109. async lock() {},
  110. async get(n, id) { return structuredClone(db[n]?.find(r => r.objectId === id) || null); },
  111. async list(n, { where = {}, afterId, limit = 200 } = {}) {
  112. return structuredClone((db[n] || []).filter(r => Object.entries(where).every(([k, v]) => JSON.stringify(r[k] ?? null) === JSON.stringify(v)))
  113. .filter(r => !afterId || r.objectId > afterId).sort((a, b) => a.objectId.localeCompare(b.objectId)).slice(0, limit));
  114. },
  115. async create(n, value) {
  116. for (const key of Object.keys(value)) assert.ok(data.tables[n]?.[key], n + '.' + key);
  117. const row = { ...structuredClone(value), objectId: 'local' + String(++seq).padStart(7, '0'), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
  118. (db[n] ??= []).push(row); return structuredClone(row);
  119. },
  120. async update(n, id, value) {
  121. const row = db[n]?.find(r => r.objectId === id); assert.ok(row);
  122. for (const key of Object.keys(value)) assert.ok(data.tables[n]?.[key], n + '.' + key);
  123. Object.assign(row, structuredClone(value)); return structuredClone(row);
  124. },
  125. };
  126. }
  127. const store = memory(), actor = { id: 'local-fixture-service', kind: 'service', scopes: ['ingest:write', 'data:read'], projectIds: ['*'] };
  128. const app = createApplication({ store, crypto, Parse: {}, data, settings: { signingSecret: 'local-test-only' }, files: {} });
  129. attachWorkshopHandlers(app, { store, data });
  130. const call = async (name, params, identity = actor) => {
  131. const result = await app.dispatch(name, JSON.parse(JSON.stringify(params)), identity);
  132. assert.equal(result.ok, true, JSON.stringify(result.errors)); return result.data;
  133. };
  134. const agentProfile = { key: 'agent-registration:local-report-audit', name: '本地结构核验助手', role: 'workshop',
  135. identityBasis: 'operator_registration', account: { platform: 'local', accountId: 'local-report-audit' },
  136. runtimePlatform: 'local-test', channel: 'local-test', collectedAt: new Date().toISOString() };
  137. const participantProfile = { key: 'participant-registration:local-report-audit', name: '本地模拟学员',
  138. identityBasis: 'participant_registration', account: { platform: 'local', accountId: 'local-simulated-participant' } };
  139. const run = (command, task, requestId) => runWorkshop(command, task, { requestId, agentProfile, participantProfile, call });
  140. const session = await run('workshop-start', { workshopKey: 'local-r13-report-audit', participant: { profileKey: participantProfile.key },
  141. conversation: { key: 'local-simulation-only', channel: 'local-test' }, groupId: '本地模拟组', dataMode: 'sample' }, 'local-report-audit:start');
  142. const receipts = [];
  143. for (const [index, draft] of drafts.entries()) {
  144. const task = { ...draft.input, sessionId: session.sessionId }, requestId = 'local-report-audit:save:' + index;
  145. const result = await run('workshop-save', task, requestId);
  146. const read = await run('workshop-read', { className: 'KfArtifact', objectId: result.artifactId }, 'local-report-audit:read:' + index);
  147. assert.equal(read.record.rawText, task.rawText);
  148. assert.equal(read.record.reportSeriesKey, task.taskKey);
  149. assert.equal(read.record.meta.attribution.agentKey, agentProfile.key);
  150. assert.equal(read.record.dataMode, 'sample');
  151. const replay = await run('workshop-save', task, requestId);
  152. assert.equal(replay.artifactId, result.artifactId); assert.equal(replay.version, 1);
  153. assert.equal(result.boardVisible, true); assert.equal(result.formalReportPublished, false);
  154. receipts.push({ module: draft.module, sourceFile: draft.sourceFile, ...result, rawTextExact: true, idempotent: true });
  155. }
  156. const projected = await call('ksfGetPublicBoard', { limit: 500 }, null);
  157. assert.equal(projected.artifact.length, 16); assert.equal(projected.opportunity.length, 3);
  158. assert.equal(projected.project.length, 0);
  159. const model = buildModel(projected), groups = { schema: 'ksf.member-groups.v1', revision: 0,
  160. groups: [{ id: 'local-group', name: '本地模拟组' }], assignments: { [model.artifacts[0].memberId]: 'local-group' }, updatedAt: null };
  161. const review = workshopReview(model, groups, { mode: 'sample' });
  162. assert.equal(review.rows.length, 16);
  163. const outputCounts = Object.fromEntries(WORKSHOP_OUTPUTS.map(o => [o.key, review.rows.filter(r => r.refs.some(ref => ref.key === o.key)).length]));
  164. for (const output of WORKSHOP_OUTPUTS) assert.ok(outputCounts[output.key] > 0, output.name);
  165. assert.equal(workshopReview(model, groups).rows.length, 0, 'sample must not appear as live');
  166. assert.equal(workshopReview(model, null, { mode: 'sample' }).ungrouped, 16, 'do not guess group assignments');
  167. const reportCenterCounts = Object.fromEntries(REPORT_TYPES.map(t => [t.key, entriesForType(model, t.key).filter(r => r.kind === 'artifact').length]));
  168. for (const count of Object.values(reportCenterCounts)) assert.ok(count > 0);
  169. await json('local-public-projection.json', projected);
  170. await json('local-shared-groups.json', groups);
  171. await json('local-save-check.json', { checkedAt: new Date().toISOString(), scope: 'in-memory-only', productionWrites: 0,
  172. realPostgreSQLTest: false, encryptedEntryTested: false, fixtureIdentities: true,
  173. stages: 13, drafts: 16, independentOpportunities: 3, rawTextsExact: true, replaysIdempotent: true,
  174. outputCounts, reportCenterCounts, sampleExcludedFromLive: true, unassignedNotGuessed: true,
  175. semanticCompletenessNotProvenByRouting: true, sourceBridgeNotExecuted: true, receipts });
  176. const connection = { url: 'https://kangshifu.fmode.cn/api/functions', appId: 'FKieRbIoGt_main' };
  177. const online = { checkedAt: new Date().toISOString(), productionWrites: 0, readOnlyOperations: ['ksfGetPublicBoard', 'ksfGetPublicMemberGroups'] };
  178. const reads = await Promise.allSettled([
  179. loadPublicBoard(connection), publicBoardAction({ ...connection, name: 'ksfGetPublicMemberGroups', params: {} }),
  180. ...['src/core/workshop-outputs.js', 'src/core/library/types.js'].map(async file => {
  181. const response = await fetch('https://kangshifu.fmode.cn/' + file, { cache: 'no-store', signal: AbortSignal.timeout(30000) });
  182. assert.ok(response.ok); const body = await response.text();
  183. return { file, http: response.status, sha256: sha(body), exactLocalMatch: sha(body) === sha(await fs.readFile(file)) };
  184. }),
  185. ]);
  186. online.errors = reads.flatMap((r, i) => r.status === 'rejected' ? [{ index: i, message: r.reason.message }] : []);
  187. if (reads[0].status === 'fulfilled') {
  188. const raw = reads[0].value, groupState = reads[1].status === 'fulfilled' ? reads[1].value : null;
  189. const db = buildModel(raw), r = workshopReview(db, groupState, { mode: 'all' });
  190. Object.assign(online, { publicReadSucceeded: true, asOf: raw.loadedAt,
  191. counts: Object.fromEntries(['project', 'artifact', 'insight', 'source', 'session', 'opportunity'].map(k => [k, raw[k].length])),
  192. groups: groupState?.groups?.length ?? null, outputsAcrossAllModes: Object.fromEntries(WORKSHOP_OUTPUTS.map(o => [o.key, r.rows.filter(row => row.refs.some(ref => ref.key === o.key)).length])),
  193. unmappedAcrossAllModes: r.unmapped, ungroupedAcrossAllModes: r.ungrouped,
  194. note: 'Current production records are independent of the unsaved r13 simulation; routing counts are not report quality judgments.' });
  195. }
  196. online.staticModules = reads.slice(2).filter(r => r.status === 'fulfilled').map(r => r.value);
  197. await json('production-readonly-check.json', online);
  198. assert.deepEqual(await fingerprint(publicSkill), skillBefore);
  199. await json('scope.json', { checkedAt: new Date().toISOString(), skillVersion: 'r13-local-quality', skillFilesUnchanged: Object.keys(skillBefore).length,
  200. handbookUnmodified: true, originalDialogueUnmodified: true, productionWrites: 0, published: false,
  201. reusedLedgerRows: { oldReviews: ledger.oldReviews.length, addedReviews: ledger.addedReviews.length, social: ledger.social.length },
  202. note: '16 unsent JSON drafts were derived locally from existing texts for checking the contract. No production IDs or save receipts were created.' });
  203. console.log(JSON.stringify({ ok: true, scope: 'local-save-plus-readonly-production-check', drafts: drafts.length, outputCounts,
  204. reportCenterCounts, publicReadSucceeded: online.publicReadSucceeded ?? false, onlineErrors: online.errors, productionWrites: 0 }, null, 2));