| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208 |
- /** Read-only production inspection plus isolated in-memory save/read/projection check.
- * Never uses service credentials, production write APIs, or a PostgreSQL connection.
- */
- import fs from 'node:fs/promises';
- import path from 'node:path';
- import crypto from 'node:crypto';
- import assert from 'node:assert/strict';
- import { createApplication } from '../server/src/core.mjs';
- import { attachWorkshopHandlers } from '../server/src/workshop.mjs';
- import { runWorkshop } from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
- import { buildModel } from '../src/core/kfs/build.js';
- import { WORKSHOP_OUTPUTS, workshopReview } from '../src/core/workshop-outputs.js';
- import { REPORT_TYPES } from '../src/core/library/types.js';
- import { entriesForType } from '../src/core/library/reports.js';
- import { loadPublicBoard, publicBoardAction } from '../src/services/workshop-data.js';
- const root = path.resolve('docs/workshop-report-coverage-r13-20261110');
- const input = path.resolve('docs/workshop-dialogue-r13-20261110');
- await fs.mkdir(path.join(root, 'local-drafts'), { recursive: true });
- const sha = value => crypto.createHash('sha256').update(value).digest('hex');
- const json = async (file, value) => fs.writeFile(path.join(root, file), JSON.stringify(value, null, 2) + '\n');
- const data = JSON.parse(await fs.readFile('server/references/runtime-data.json', 'utf8'));
- const ledger = JSON.parse(await fs.readFile(path.join(input, '使用的证据与编码.json'), 'utf8'));
- const publicSkill = path.resolve('skills/ksf-workshop-runtime');
- async function fingerprint(dir, prefix = '') {
- const entries = await fs.readdir(dir, { withFileTypes: true });
- const result = {};
- for (const e of entries) {
- const rel = prefix + e.name;
- if (e.isDirectory()) Object.assign(result, await fingerprint(path.join(dir, e.name), rel + '/'));
- else result[rel] = sha(await fs.readFile(path.join(dir, e.name)));
- }
- return result;
- }
- const skillBefore = await fingerprint(publicSkill);
- // Actual current files, no invented new research or employee responses.
- const definitions = [
- ['03-category.txt', 'D1-01', 'S1', 'category_report', 'market-analysis'],
- ['04-rules.txt', 'D1-02', 'S1', 'category_report', 'verification-config'],
- ['05-competitors.txt', 'D1-03', 'S1', 'competitor_report', 'market-analysis'],
- ['06-tasks.txt', 'D1-04', 'S1', 'pain_list', 'research-task-package'],
- ['07-research.txt', 'D1-N', 'S2', 'voc_report', 'research-progress'],
- ['08-accept.txt', 'D2-01', 'S2', 'voc_report', 'research-acceptance'],
- ['09-voc.txt', 'D2-02', 'S2', 'pain_list', 'evidence-pool'],
- ['10-matrix.txt', 'D2-03', 'S3', 'assessment_report', 'supply-demand-matrix'],
- ['11-opportunities.txt', 'D2-04', 'S3', 'assessment_report', 'opportunity-validation'],
- ['12-priority.txt', 'D2-05', 'S4', 'assessment_report', 'opportunity-validation'],
- ['13-feasibility.txt', 'D2-06', 'S4', 'assessment_report', 'opportunity-validation'],
- ['14-value.txt', 'D2-07', 'S5', 'concept', 'buying-points'],
- ['15-proposal.txt', 'D2-08:proposal', 'S5', 'concept', 'product-proposal'],
- ];
- const chapter = (id, title, text, stage) => ({ id, title, stage,
- sections: [{ kind: 'longpara', title: '现有阶段正文', text }] });
- const drafts = [];
- for (const [file, module, stageCode, artifactType, anchor] of definitions) {
- const rawText = await fs.readFile(path.join(input, '阶段产出', file), 'utf8');
- const title = rawText.split(/\r?\n/)[0];
- const chapters = [chapter(anchor, title, rawText, stageCode)];
- if (module === 'D2-02') {
- const text = rawText.match(/N5 前置资源。[\s\S]*?(?=\r?\nKANO)/)?.[0];
- assert.ok(text);
- chapters.push(chapter('social-needs', '有限场景线索,尚非完整社媒需求报告', text, stageCode));
- }
- if (module === 'D2-08:proposal') {
- const text = rawText.match(/V3全池:[\s\S]*?(?=\r?\n五分钟汇报)/)?.[0];
- assert.ok(text);
- chapters.push(chapter('opportunity-validation', 'V3全池去向', text, stageCode));
- }
- const base = { sessionId: null, taskKey: 'local-r13-audit:' + module + ':report', version: 1,
- stageCode, title, artifactType, category: '拌面', rawText, sourceIds: [],
- pendingQuestions: ['仅用于本地结构验证,员工和选择为模拟,未上传企业数据库',
- '原件索引尚未通过正常业务接口桥接为KfSource/KfEvidence;不声明已保存真实证据'],
- report: { executive: ['来自r13模拟对话的现有正文;正文存在不代表研究质量或真实验证已通过。'], chapters } };
- drafts.push({ sourceFile: file, module, kind: 'stage', input: base });
- if (module === 'D2-04') {
- const matches = [...rawText.matchAll(/^(O[123]) ([^\r\n]+)[\r\n]+([\s\S]*?)(?=^O[123] |^五力对全池)/gm)];
- assert.equal(matches.length, 3);
- for (const match of matches) {
- const [text, key, title, body] = match;
- const demandSummary = body.match(/^U:(.+)$/m)?.[1];
- const usageScenario = body.match(/^C:(.+)$/m)?.[1];
- const fusionDefinition = body.split(/\r?\n/).filter(line => /^[SUC]:/.test(line)).join(' ');
- assert.ok(demandSummary && usageScenario);
- drafts.push({ sourceFile: file, module: module + ':' + key, kind: 'independent-card-draft', input: {
- ...structuredClone(base), taskKey: 'local-r13-audit:' + module + ':' + key, title: key + ' ' + title,
- artifactType: 'opportunity_card', rawText: text,
- report: { executive: ['从现有模拟正文拆出的独立卡草稿;无新增研究,尚未在生产保存。'],
- chapters: [chapter('opportunity-cards', key + ' ' + title, text, stageCode)] },
- opportunity: { key: 'local-r13-audit:' + key, title, demandSummary,
- targetAudience: key === 'O1' ? '想尝新又保留经典选择的人;目标学生尚未验证'
- : key === 'O2' ? '首次接触陌生速食的人;身份和地点尚未验证'
- : '购买常备速食并有补货任务的人;真实购物断点未验证',
- usageScenario, fusionDefinition },
- } });
- }
- }
- }
- for (let i = 0; i < drafts.length; i++) {
- await json('local-drafts/' + String(i + 1).padStart(2, '0') + '.json', {
- schema: 'ksf.local.unsent-workshop-draft.v1', productionReady: false, productionSaved: false,
- requiredDataMode: 'sample', notes: 'sessionId待真实授权上下文提供;不可将外层说明作为save字段。来源桥接和正文质量须另验。',
- ...drafts[i],
- });
- }
- // All mutations below target this disposable JavaScript object, not a DB server.
- function memory() {
- let db = {}, seq = 0;
- return {
- async transaction(fn) { const prior = structuredClone(db); try { return await fn(this); } catch (e) { db = prior; throw e; } },
- async lock() {},
- async get(n, id) { return structuredClone(db[n]?.find(r => r.objectId === id) || null); },
- async list(n, { where = {}, afterId, limit = 200 } = {}) {
- return structuredClone((db[n] || []).filter(r => Object.entries(where).every(([k, v]) => JSON.stringify(r[k] ?? null) === JSON.stringify(v)))
- .filter(r => !afterId || r.objectId > afterId).sort((a, b) => a.objectId.localeCompare(b.objectId)).slice(0, limit));
- },
- async create(n, value) {
- for (const key of Object.keys(value)) assert.ok(data.tables[n]?.[key], n + '.' + key);
- const row = { ...structuredClone(value), objectId: 'local' + String(++seq).padStart(7, '0'), createdAt: new Date().toISOString(), updatedAt: new Date().toISOString() };
- (db[n] ??= []).push(row); return structuredClone(row);
- },
- async update(n, id, value) {
- const row = db[n]?.find(r => r.objectId === id); assert.ok(row);
- for (const key of Object.keys(value)) assert.ok(data.tables[n]?.[key], n + '.' + key);
- Object.assign(row, structuredClone(value)); return structuredClone(row);
- },
- };
- }
- const store = memory(), actor = { id: 'local-fixture-service', kind: 'service', scopes: ['ingest:write', 'data:read'], projectIds: ['*'] };
- const app = createApplication({ store, crypto, Parse: {}, data, settings: { signingSecret: 'local-test-only' }, files: {} });
- attachWorkshopHandlers(app, { store, data });
- const call = async (name, params, identity = actor) => {
- const result = await app.dispatch(name, JSON.parse(JSON.stringify(params)), identity);
- assert.equal(result.ok, true, JSON.stringify(result.errors)); return result.data;
- };
- const agentProfile = { key: 'agent-registration:local-report-audit', name: '本地结构核验助手', role: 'workshop',
- identityBasis: 'operator_registration', account: { platform: 'local', accountId: 'local-report-audit' },
- runtimePlatform: 'local-test', channel: 'local-test', collectedAt: new Date().toISOString() };
- const participantProfile = { key: 'participant-registration:local-report-audit', name: '本地模拟学员',
- identityBasis: 'participant_registration', account: { platform: 'local', accountId: 'local-simulated-participant' } };
- const run = (command, task, requestId) => runWorkshop(command, task, { requestId, agentProfile, participantProfile, call });
- const session = await run('workshop-start', { workshopKey: 'local-r13-report-audit', participant: { profileKey: participantProfile.key },
- conversation: { key: 'local-simulation-only', channel: 'local-test' }, groupId: '本地模拟组', dataMode: 'sample' }, 'local-report-audit:start');
- const receipts = [];
- for (const [index, draft] of drafts.entries()) {
- const task = { ...draft.input, sessionId: session.sessionId }, requestId = 'local-report-audit:save:' + index;
- const result = await run('workshop-save', task, requestId);
- const read = await run('workshop-read', { className: 'KfArtifact', objectId: result.artifactId }, 'local-report-audit:read:' + index);
- assert.equal(read.record.rawText, task.rawText);
- assert.equal(read.record.reportSeriesKey, task.taskKey);
- assert.equal(read.record.meta.attribution.agentKey, agentProfile.key);
- assert.equal(read.record.dataMode, 'sample');
- const replay = await run('workshop-save', task, requestId);
- assert.equal(replay.artifactId, result.artifactId); assert.equal(replay.version, 1);
- assert.equal(result.boardVisible, true); assert.equal(result.formalReportPublished, false);
- receipts.push({ module: draft.module, sourceFile: draft.sourceFile, ...result, rawTextExact: true, idempotent: true });
- }
- const projected = await call('ksfGetPublicBoard', { limit: 500 }, null);
- assert.equal(projected.artifact.length, 16); assert.equal(projected.opportunity.length, 3);
- assert.equal(projected.project.length, 0);
- const model = buildModel(projected), groups = { schema: 'ksf.member-groups.v1', revision: 0,
- groups: [{ id: 'local-group', name: '本地模拟组' }], assignments: { [model.artifacts[0].memberId]: 'local-group' }, updatedAt: null };
- const review = workshopReview(model, groups, { mode: 'sample' });
- assert.equal(review.rows.length, 16);
- const outputCounts = Object.fromEntries(WORKSHOP_OUTPUTS.map(o => [o.key, review.rows.filter(r => r.refs.some(ref => ref.key === o.key)).length]));
- for (const output of WORKSHOP_OUTPUTS) assert.ok(outputCounts[output.key] > 0, output.name);
- assert.equal(workshopReview(model, groups).rows.length, 0, 'sample must not appear as live');
- assert.equal(workshopReview(model, null, { mode: 'sample' }).ungrouped, 16, 'do not guess group assignments');
- const reportCenterCounts = Object.fromEntries(REPORT_TYPES.map(t => [t.key, entriesForType(model, t.key).filter(r => r.kind === 'artifact').length]));
- for (const count of Object.values(reportCenterCounts)) assert.ok(count > 0);
- await json('local-public-projection.json', projected);
- await json('local-shared-groups.json', groups);
- await json('local-save-check.json', { checkedAt: new Date().toISOString(), scope: 'in-memory-only', productionWrites: 0,
- realPostgreSQLTest: false, encryptedEntryTested: false, fixtureIdentities: true,
- stages: 13, drafts: 16, independentOpportunities: 3, rawTextsExact: true, replaysIdempotent: true,
- outputCounts, reportCenterCounts, sampleExcludedFromLive: true, unassignedNotGuessed: true,
- semanticCompletenessNotProvenByRouting: true, sourceBridgeNotExecuted: true, receipts });
- const connection = { url: 'https://kangshifu.fmode.cn/api/functions', appId: 'FKieRbIoGt_main' };
- const online = { checkedAt: new Date().toISOString(), productionWrites: 0, readOnlyOperations: ['ksfGetPublicBoard', 'ksfGetPublicMemberGroups'] };
- const reads = await Promise.allSettled([
- loadPublicBoard(connection), publicBoardAction({ ...connection, name: 'ksfGetPublicMemberGroups', params: {} }),
- ...['src/core/workshop-outputs.js', 'src/core/library/types.js'].map(async file => {
- const response = await fetch('https://kangshifu.fmode.cn/' + file, { cache: 'no-store', signal: AbortSignal.timeout(30000) });
- assert.ok(response.ok); const body = await response.text();
- return { file, http: response.status, sha256: sha(body), exactLocalMatch: sha(body) === sha(await fs.readFile(file)) };
- }),
- ]);
- online.errors = reads.flatMap((r, i) => r.status === 'rejected' ? [{ index: i, message: r.reason.message }] : []);
- if (reads[0].status === 'fulfilled') {
- const raw = reads[0].value, groupState = reads[1].status === 'fulfilled' ? reads[1].value : null;
- const db = buildModel(raw), r = workshopReview(db, groupState, { mode: 'all' });
- Object.assign(online, { publicReadSucceeded: true, asOf: raw.loadedAt,
- counts: Object.fromEntries(['project', 'artifact', 'insight', 'source', 'session', 'opportunity'].map(k => [k, raw[k].length])),
- 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])),
- unmappedAcrossAllModes: r.unmapped, ungroupedAcrossAllModes: r.ungrouped,
- note: 'Current production records are independent of the unsaved r13 simulation; routing counts are not report quality judgments.' });
- }
- online.staticModules = reads.slice(2).filter(r => r.status === 'fulfilled').map(r => r.value);
- await json('production-readonly-check.json', online);
- assert.deepEqual(await fingerprint(publicSkill), skillBefore);
- await json('scope.json', { checkedAt: new Date().toISOString(), skillVersion: 'r13-local-quality', skillFilesUnchanged: Object.keys(skillBefore).length,
- handbookUnmodified: true, originalDialogueUnmodified: true, productionWrites: 0, published: false,
- reusedLedgerRows: { oldReviews: ledger.oldReviews.length, addedReviews: ledger.addedReviews.length, social: ledger.social.length },
- note: '16 unsent JSON drafts were derived locally from existing texts for checking the contract. No production IDs or save receipts were created.' });
- console.log(JSON.stringify({ ok: true, scope: 'local-save-plus-readonly-production-check', drafts: drafts.length, outputCounts,
- reportCenterCounts, publicReadSucceeded: online.publicReadSucceeded ?? false, onlineErrors: online.errors, productionWrites: 0 }, null, 2));
|