verify-live-S2-readback.mjs 8.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. // Independent, read-only acceptance of the saved S2. Never calls save/start/import.
  2. import fs from 'node:fs';
  3. import crypto from 'node:crypto';
  4. import assert from 'node:assert/strict';
  5. import {invoke,classRows} from '../server/scripts/platform-client.mjs';
  6. import {loadPublicBoard} from '../src/services/workshop-data.js';
  7. const sessionId='btrFqkz0yw',artifactId='b7NOby5ys0',outputSourceId='wPbc3huKIM',s1Id='G8IrkkDG7X';
  8. const outputFile='work/live-S2-readback-20261102.json';
  9. const templateBytes=fs.readFileSync('work/first-live-S2-template.json'),template=JSON.parse(templateBytes);
  10. const s1Template=JSON.parse(fs.readFileSync('work/first-live-S1-v2-template.json','utf8'));
  11. const imported=JSON.parse(fs.readFileSync('work/consumer-quotes-readback-20261102-complete.json','utf8')).imports;
  12. const requestId=template.taskKey+':save:v1';
  13. const sha=x=>crypto.createHash('sha256').update(x).digest('hex');
  14. const canonical=x=>x&&typeof x==='object'?Array.isArray(x)?'['+x.map(canonical).join(',')+']':'{'+Object.keys(x).sort().map(k=>JSON.stringify(k)+':'+canonical(x[k])).join(',')+'}':JSON.stringify(x);
  15. const pointer=(className,objectId)=>({__type:'Pointer',className,objectId});
  16. const read=async(className,objectId)=>{
  17. const r=await invoke('ksfGetRecord',{className,objectId});
  18. assert.equal(r.http,200);assert.equal(r.body?.result?.ok,true);const record=r.body.result.data.record;
  19. assert.equal(record?.objectId,objectId);return record;
  20. };
  21. assert.equal(fs.existsSync(outputFile),false,'Preserve earlier acceptance');
  22. let check='local template fingerprint';
  23. try{
  24. const packaged=JSON.parse(fs.readFileSync('work/first-live-S2-package-receipt.json','utf8'));
  25. assert.equal(sha(templateBytes),packaged.templateSha256);assert.equal(template.sessionId,null);
  26. check='read saved records and history';
  27. const results=await Promise.allSettled([
  28. read('KfSession',sessionId),read('KfArtifact',artifactId),read('KfArtifact',s1Id),read('KfSource',outputSourceId),
  29. Promise.all(imported.map(r=>read('KfSource',r.sourceId))),
  30. Promise.all(imported.map(r=>read('KfEvidence',r.evidenceId))),
  31. classRows('KfArtifact',{workshopKey:'KSF-真实资料-20261102-01'}),
  32. classRows('KsfOperation',{operation:'workshop-task',requestId}),
  33. classRows('KfOpportunity',{sessionRef:pointer('KfSession',sessionId)}),
  34. classRows('KfProject',{sessionRef:pointer('KfSession',sessionId)}),
  35. ]);
  36. assert.deepEqual(results.flatMap((r,i)=>r.status==='rejected'?[i]:[]),[],'Read group failure, indexes only');
  37. const [session,artifact,s1,generated,origins,evidence,history,operations,opportunities,projects]=results.map(r=>r.value);
  38. check='saved S2 matches template';
  39. for(const field of ['title','rawText','version','stageCode','artifactType'])assert.deepEqual(artifact[field],template[field],field);
  40. assert.equal(artifact.reportSeriesKey,template.taskKey);assert.equal(artifact.reportType,'workshop_auxiliary');
  41. assert.equal(artifact.dashboardState,'workshop');assert.equal(artifact.dataMode,'live');assert.equal(artifact.recordState,'active');
  42. assert.equal(artifact.ownerSession,sessionId);assert.deepEqual(artifact.sessionRef,pointer('KfSession',sessionId));
  43. assert.deepEqual(artifact.sourceRef,pointer('KfSource',outputSourceId));assert.equal(artifact.projectRef??null,null);
  44. assert.equal(artifact.previousVersionRef??null,null);assert.equal(artifact.reportContentHash,sha(canonical(template.report)));
  45. assert.equal(artifact.report.title,template.title);assert.ok(Number.isFinite(Date.parse(artifact.report.generatedAt)));
  46. assert.equal(artifact.report.executive[0],'课堂过程成果,未经员工正式确认;待验证项不作为市场结论。');
  47. assert.deepEqual(artifact.report.executive.slice(1),template.report.executive);
  48. for(const field of ['chapters','evidence','subtitle','method','sources'])assert.deepEqual(artifact.report[field],template.report[field],field);
  49. assert.deepEqual(artifact.meta.workshop.sourceIds,template.sourceIds);assert.deepEqual(artifact.meta.workshop.pendingQuestions,template.pendingQuestions);
  50. assert.equal(artifact.meta.workshop.formalConfirmation,false);assert.equal(artifact.contentConfirmedAt??null,null);assert.equal(artifact.dashboardPublishedAt??null,null);
  51. assert.equal(generated.rawText,template.rawText);assert.equal(generated.contentHash,sha(template.rawText));assert.equal(generated.dataMode,'live');
  52. check='live original sources and exact quotes';
  53. let quoteCount=0;
  54. for(const [key,entry]of Object.entries(template.report.evidence)){
  55. const ids=imported.find(x=>key==='consumer-'+x.key);assert.ok(ids);assert.equal(entry.sourceId,ids.sourceId);
  56. const origin=origins.find(x=>x.objectId===entry.sourceId),quoteRecord=evidence.find(x=>x.objectId===ids.evidenceId);
  57. assert.ok(origin);assert.ok(quoteRecord);assert.equal(origin.dataMode,'live');assert.equal(origin.sourceKind,'social');assert.equal(origin.platform,'smzdm');
  58. assert.equal(sha(origin.rawText),origin.contentHash);assert.equal(quoteRecord.sourceRef?.objectId,origin.objectId);
  59. assert.equal(quoteRecord.evidenceKind,'public_user_experience_self_report');assert.equal(quoteRecord.productRef??null,null);
  60. assert.equal(quoteRecord.publishedAt??null,null);assert.equal(quoteRecord.locator.publicationTimezone,'unknown');
  61. assert.equal(entry.quotes.length,1);assert.equal(entry.quotes[0].text,quoteRecord.quote);
  62. for(const quote of entry.quotes){assert.ok(origin.rawText.includes(quote.text));quoteCount++;}
  63. }
  64. assert.equal(quoteCount,4);assert.equal(artifact.sources.length,4);assert.equal(artifact.evidenceCount,4);
  65. check='session, identity, original S1 and operation';
  66. assert.equal(session.dataMode,'live');assert.equal(session.stageCode,'S2');assert.equal(session.step,'submitted');
  67. assert.deepEqual(session.meta.attribution,artifact.meta.attribution);assert.deepEqual(s1.meta.attribution,artifact.meta.attribution);
  68. assert.equal(artifact.agentName,'王刚001');assert.equal(artifact.agentId,session.meta.attribution.agentKey);
  69. for(const field of ['title','rawText','version','stageCode'])assert.deepEqual(s1[field],s1Template[field]);
  70. assert.deepEqual(s1.report.chapters,s1Template.report.chapters);assert.deepEqual(s1.report.evidence,s1Template.report.evidence);
  71. assert.deepEqual(s1.meta.workshop.sourceIds,s1Template.sourceIds);
  72. const versions=history.filter(x=>x.reportSeriesKey===template.taskKey);
  73. assert.equal(versions.length,1);assert.equal(versions[0].objectId,artifactId);assert.equal(versions[0].version,1);
  74. assert.equal(history.length,3);assert.equal(operations.length,1);assert.equal(opportunities.length,0);assert.equal(projects.length,0);
  75. assert.equal(operations[0].payloadHash,sha(canonical({...template,sessionId,requestId})));
  76. assert.equal(operations[0].receipt.artifactId,artifactId);assert.equal(operations[0].receipt.sourceId,outputSourceId);
  77. assert.equal(operations[0].receipt.opportunityId,null);assert.equal(operations[0].receipt.projectId,null);
  78. check='public board projection';
  79. const publicData=await loadPublicBoard({url:'https://kangshifu.fmode.cn/api/functions',appId:'FKieRbIoGt_main'});
  80. const projected=publicData.artifact.find(x=>x.objectId===artifactId);assert.ok(projected);
  81. assert.equal(projected.stageCode,'S2');assert.equal(projected.dataMode,'live');assert.equal(projected.dashboardState,'workshop');
  82. assert.deepEqual(projected.report.evidence,template.report.evidence);assert.deepEqual(projected.report.chapters,template.report.chapters);
  83. assert.ok(publicData.artifact.some(x=>x.objectId===s1Id));
  84. for(const value of [session.meta.attribution.participantAccount?.accountId,session.meta.attribution.agentAccount?.accountId,session.meta.attribution.conversationKey].filter(Boolean))assert.equal(JSON.stringify(publicData).includes(value),false);
  85. const result={ok:true,operation:'live-S2-independently-verified',at:new Date().toISOString(),readOnly:true,businessWrites:0,
  86. sessionId,artifactId,outputSourceId,reportUrl:'https://kangshifu.fmode.cn/report/'+artifactId,version:1,stageCode:'S2',dataMode:'live',
  87. dashboardState:'workshop',reportSeriesKey:template.taskKey,sourceIds:template.sourceIds,evidenceIds:imported.map(x=>x.evidenceId),
  88. reportQuoteCount:quoteCount,pendingQuestionCount:6,templateMatched:true,originalS1Preserved:true,originalS1ArtifactId:s1Id,
  89. privateAttributionMatched:true,sessionStage:'S2',operationCount:1,s2VersionCount:1,opportunityCount:0,projectCount:0,
  90. publicBoardDataVerified:true,formalReportPublished:false,employeeConfirmed:false,browserVerified:false};
  91. fs.writeFileSync(outputFile,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.log(JSON.stringify(result,null,2));
  92. }catch(error){
  93. const result={ok:false,operation:'live-S2-independent-readback-stopped',check,
  94. checkLine:error.stack?.match(/verify-live-S2-readback\.mjs:(\d+):/)?.[1]??null,readOnly:true,businessWrites:0};
  95. fs.writeFileSync(outputFile,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result,null,2));process.exitCode=1;
  96. }