verify-live-S5-readback.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. // Independent S5 acceptance. Production reads only; never invokes business writes.
  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. // The initial strict audit is retained separately. Receipt inspection established
  8. // a real save-request deviation, which must stay explicit in final acceptance.
  9. const output='work/live-S5-readback-20261103-02.json';assert.equal(fs.existsSync(output),false);
  10. const dir='work/S5-classroom-next-20261103/',base='KSF-真实资料-20261102-01';
  11. const runbook=JSON.parse(fs.readFileSync(dir+'runbook.json','utf8'));
  12. const template=JSON.parse(fs.readFileSync(dir+'03-project-S5-save.template.json','utf8'));
  13. const conceptTemplate=JSON.parse(fs.readFileSync(dir+'02-concept-save.template.json','utf8'));
  14. const ids={sessionId:runbook.sessionId,projectId:runbook.projectId,opportunityId:runbook.opportunityId,
  15. conceptId:'QO0uo8nisk',artifactId:'tLkz2MAoIw',outputSourceId:'5zsPtYFL1u',baselineArtifactId:'XF4h-Z7l5o',s4ArtifactId:'U3_W7WDliP'};
  16. const actualSaveRequestId=base+':portion-fit-project:S5:save:v1';
  17. const ptr=(className,objectId)=>({__type:'Pointer',className,objectId}),sha=x=>crypto.createHash('sha256').update(x).digest('hex');
  18. const checks=[];let step='fixed template';
  19. const check=(name,fn)=>{step=name;fn();checks.push({name,passed:true});};
  20. const read=async(className,objectId)=>{const r=await invoke('ksfGetRecord',{className,objectId});assert.equal(r.http,200);assert.equal(r.body?.result?.ok,true);const row=r.body.result.data.record;assert.equal(row?.objectId,objectId);return row;};
  21. const group=async tasks=>{const rs=await Promise.allSettled(tasks);assert.deepEqual(rs.flatMap((r,i)=>r.status==='rejected'?[i]:[]),[],'Read failure indexes');return rs.map(r=>r.value);};
  22. try{
  23. check('unchanged S5 templates, four sources and thirteen questions',()=>{
  24. assert.equal(sha(fs.readFileSync(dir+'03-project-S5-save.template.json')),'f8dc7d8a9f0b2a998f90453add9ca02f34a60cfc52be3bc592deab87d49ada8c');
  25. assert.equal(sha(fs.readFileSync(dir+'02-concept-save.template.json')),'5c182b5c5733218577bca330df90b85b91d69ad1b6b1e5494bab5b06e2238e4d');
  26. assert.equal([...template.rawText].length,3655);assert.equal(template.sourceIds.length,4);assert.equal(template.pendingQuestions.length,13);
  27. assert.deepEqual(conceptTemplate.concept.pendingQuestions,template.pendingQuestions);
  28. });
  29. step='read S5 records, current project and committed audits';
  30. const [artifact,source,concept,session,project,opp,statusResponse,operations,history,concepts,formulaItems,projects,opps,
  31. s4,baseline,s1,s2,s3v1,s3v2]=await group([
  32. read('KfArtifact',ids.artifactId),read('KfSource',ids.outputSourceId),read('KfConcept',ids.conceptId),read('KfSession',ids.sessionId),read('KfProject',ids.projectId),read('KfOpportunity',ids.opportunityId),
  33. invoke('ksfGetWorkshopProject',{sessionId:ids.sessionId,projectId:ids.projectId}),
  34. classRows('KsfOperation',{requestId:{$in:[...runbook.steps.map(s=>s.requestId),actualSaveRequestId]}}),classRows('KfArtifact',{workshopKey:base}),
  35. classRows('KfConcept',{projectRef:ptr('KfProject',ids.projectId)}),classRows('KfFormulaItem',{conceptRef:ptr('KfConcept',ids.conceptId)}),
  36. classRows('KfProject',{sessionRef:ptr('KfSession',ids.sessionId)}),classRows('KfOpportunity',{sessionRef:ptr('KfSession',ids.sessionId)}),
  37. read('KfArtifact',ids.s4ArtifactId),read('KfArtifact',ids.baselineArtifactId),read('KfArtifact','G8IrkkDG7X'),read('KfArtifact','b7NOby5ys0'),read('KfArtifact','oiNxX7o27j'),read('KfArtifact','ZW1FB5zeHr')
  38. ]);
  39. assert.equal(statusResponse.http,200);assert.equal(statusResponse.body?.result?.ok,true);const status=statusResponse.body.result.data;
  40. check('S5 artifact fields, complete report body and generated source match template',()=>{
  41. for(const k of ['rawText','title','version','stageCode','artifactType'])assert.deepEqual(artifact[k],template[k]);
  42. assert.equal(artifact.reportSeriesKey,template.taskKey);assert.equal(artifact.reportType,'workshop_auxiliary');assert.equal(artifact.artifactSubtype,'classroom_task');
  43. assert.equal(artifact.schemaVersion,'ksf.v0.7');assert.equal(artifact.recordState,'active');assert.equal(artifact.dataMode,'live');assert.equal(artifact.dashboardState,'workshop');
  44. assert.equal(artifact.ownerSession,ids.sessionId);assert.equal(artifact.projectId,ids.projectId);assert.deepEqual(artifact.sessionRef,ptr('KfSession',ids.sessionId));assert.deepEqual(artifact.projectRef,ptr('KfProject',ids.projectId));
  45. assert.deepEqual(artifact.sourceRef,ptr('KfSource',ids.outputSourceId));assert.equal(source.rawText,template.rawText);assert.equal(source.dataMode,'live');
  46. assert.deepEqual(artifact.tags,['拌面','课堂过程成果']);assert.equal(artifact.evidenceCount,4);assert.equal(artifact.evidenceLevel,'supported');
  47. assert.deepEqual(artifact.meta.workshop.sourceIds,template.sourceIds);assert.deepEqual(artifact.meta.workshop.pendingQuestions,template.pendingQuestions);
  48. for(const k of ['chapters','evidence','method','sources','subtitle'])assert.deepEqual(artifact.report[k],template.report[k]);
  49. assert.deepEqual(artifact.report.executive,['课堂过程成果,未经员工正式确认;待验证项不作为市场结论。',...template.report.executive]);
  50. assert.equal(artifact.report.title,template.title);assert.ok(Number.isFinite(Date.parse(artifact.report.generatedAt)));
  51. assert.equal(artifact.contentConfirmedAt??null,null);assert.equal(artifact.dashboardPublishedAt??null,null);assert.equal(artifact.meta.workshop.formalConfirmation,false);
  52. assert.equal(artifact.taskKey??null,null);assert.equal(artifact.conceptRef??null,null);assert.equal(artifact.previousVersionRef??null,null);
  53. });
  54. check('concept v1 fields and unknown specification match, with no formula or fake process',()=>{
  55. assert.equal(concept.version,1);assert.equal(concept.dataMode,'live');assert.equal(concept.schemaVersion,'ksf.v0.7');assert.equal(concept.recordState,'active');assert.equal(concept.reviewState,'pending');
  56. assert.deepEqual(concept.projectRef,ptr('KfProject',ids.projectId));assert.equal(concept.externalKey,'workshop:concept:'+ids.projectId+':1');assert.equal(concept.previousVersionRef??null,null);
  57. for(const k of ['name','targetAudience','usageScenario','specification','valueProposition','flavor','sellingPoints','productionSteps'])assert.deepEqual(concept[k],conceptTemplate.concept[k]);
  58. assert.deepEqual(concept.productionSteps,[]);assert.deepEqual(formulaItems,[]);assert.deepEqual(status.formulaItems,[]);
  59. assert.deepEqual(project.currentConceptRef,ptr('KfConcept',ids.conceptId));assert.equal(status.concept.objectId,ids.conceptId);assert.deepEqual(status.concept.specification,concept.specification);
  60. assert.deepEqual(project.meta.pendingQuestions,template.pendingQuestions);assert.deepEqual(status.project.meta.pendingQuestions,template.pendingQuestions);
  61. assert.equal(concept.pendingQuestions??null,null);assert.equal(concept.proposalArtifactRef??null,null);
  62. });
  63. const sections=artifact.report.chapters.flatMap(c=>c.sections),tables=sections.filter(s=>s.kind==='table');
  64. check('five chapters, fifteen sections, five exact tables and thirteen visible pending rows',()=>{
  65. assert.equal(artifact.report.chapters.length,5);assert.deepEqual(artifact.report.chapters.map(c=>c.sections.length),[4,4,2,2,3]);assert.equal(sections.length,15);
  66. assert.deepEqual(tables.map(t=>[t.title,t.data.cols.length,t.data.rows.length]),[['设计对照',4,4],['设计、观察与未知',3,8],['主引文',3,4],['继续、调整与暂停',4,4],['待补问题',3,13]]);
  67. const pending=tables.find(t=>t.title==='待补问题');assert.deepEqual(pending.data.rows.map(r=>r[1]),template.pendingQuestions);
  68. assert.deepEqual(pending.data.rows.map(r=>r[2]),[...Array(8).fill('原S4问题'),'承原1、4','承原4、6','承原7','承原8','承原3、6']);
  69. assert.deepEqual(template.pendingQuestions.slice(0,8),s4.meta.workshop.pendingQuestions);
  70. });
  71. const evidenceIds={'consumer-cq-03':'_9mUml0wmh','consumer-cq-04':'2bFGP7axMo','public-p1':'xHGV_mRWcG','public-p2':'CeAEIwymTa'};
  72. step='exact evidence and source binding reads';const entries=Object.entries(template.report.evidence);
  73. const evRows=await group(entries.flatMap(([k,e])=>[read('KfSource',e.sourceId),read('KfEvidence',evidenceIds[k])]));
  74. for(const [i,[k,e]]of entries.entries())check(k+' exact verbatim source quote and product remains unbound',()=>{
  75. const origin=evRows[i*2],ev=evRows[i*2+1];assert.equal(origin.dataMode,'live');assert.equal(ev.dataMode,'live');assert.equal(ev.quote,e.quotes[0].text);assert.ok(origin.rawText.includes(ev.quote));assert.ok(template.rawText.includes(ev.quote));
  76. assert.deepEqual(ev.sourceRef,ptr('KfSource',e.sourceId));assert.equal(ev.productRef??null,null);assert.equal(ev.quoteForm,'verbatim_excerpt');assert.equal(ev.verificationState,'archived_text_and_screenshot_checked');
  77. assert.equal(ev.evidenceKind,k==='public-p1'?'retailer_listing_observation':'public_user_experience_self_report');assert.equal(origin.platform,k==='public-p1'?'suning':'smzdm');
  78. });
  79. check('one classroom project and hypothesis opportunity; current session stage S5',()=>{
  80. assert.deepEqual(projects.map(p=>p.objectId),[ids.projectId]);assert.deepEqual(opps.map(p=>p.objectId),[ids.opportunityId]);assert.deepEqual(concepts.map(c=>c.objectId),[ids.conceptId]);
  81. assert.equal(project.stageCode,'S5');assert.equal(project.progressState,'in_progress');assert.equal(project.status,'workshop');assert.equal(project.dataMode,'live');assert.equal(project.artifactCount,3);
  82. assert.equal(project.ownerSession,ids.sessionId);assert.deepEqual(project.opportunityRef,ptr('KfOpportunity',ids.opportunityId));assert.equal(project.meta.projectKey,base+':portion-fit-project');
  83. assert.equal(opp.claimKind,'hypothesis');assert.equal(opp.status,'approved');assert.equal(opp.linkedProject,ids.projectId);assert.equal(opp.version,1);
  84. assert.equal(session.stageCode,'S5');assert.equal(session.dataMode,'live');assert.equal(session.workshopKey,base);assert.deepEqual(artifact.meta.attribution,session.meta.attribution);
  85. assert.equal(artifact.agentId,session.meta.attribution.agentKey);assert.equal(artifact.agentName,'王刚001');
  86. });
  87. check('exact three project reports and seven sequential events with no completed event',()=>{
  88. assert.equal(status.project.objectId,ids.projectId);assert.equal(status.project.stageCode,'S5');assert.equal(status.project.progressState,'in_progress');
  89. assert.deepEqual(status.artifacts.map(a=>a.objectId).sort(),[ids.baselineArtifactId,ids.s4ArtifactId,ids.artifactId].sort());
  90. assert.deepEqual(status.events.map(e=>e.sequence),[1,2,3,4,5,6,7]);assert.deepEqual(status.events.map(e=>e.eventType),['project_created','task_submitted','stage_changed','task_submitted','stage_changed','concept_saved','task_submitted']);
  91. assert.deepEqual(status.events.map(e=>e.objectId),['GGDTcg5YTy','RkkTTE_xeb','hOFghbcMsY','VlGFu_jv4b','0ya0a03U9G','qKnPnVkZA8','iUs3JwZ9Kf']);
  92. assert.equal(status.events[6].artifactRef.objectId,ids.artifactId);assert.equal(status.events[4].toStage,'S5');
  93. });
  94. check('three actual committed writes; S5 report save key deviates from runbook and original key remains unused',()=>{
  95. const expected=[runbook.steps[1].requestId,runbook.steps[3].requestId,actualSaveRequestId];assert.equal(operations.length,3);assert.deepEqual(operations.map(o=>o.requestId).sort(),expected.sort());
  96. assert.notEqual(actualSaveRequestId,runbook.steps[5].requestId);assert.equal(operations.some(o=>o.requestId===runbook.steps[5].requestId),false);
  97. const move=operations.find(o=>o.requestId===runbook.steps[1].requestId).receipt;assert.equal(move.projectId,ids.projectId);assert.equal(move.stageCode,'S5');assert.equal(move.eventId,'0ya0a03U9G');assert.equal(move.enterpriseApproval,false);
  98. const c=operations.find(o=>o.requestId===runbook.steps[3].requestId).receipt;assert.equal(c.conceptId,ids.conceptId);assert.equal(c.version,1);assert.deepEqual(c.pendingQuestions,template.pendingQuestions);assert.deepEqual(c.formulaItemIds,[]);assert.equal(c.enterpriseApproval,false);
  99. const a=operations.find(o=>o.requestId===actualSaveRequestId).receipt;assert.equal(a.artifactId,ids.artifactId);assert.equal(a.sourceId,ids.outputSourceId);assert.equal(a.opportunityId,null);assert.equal(a.projectId,ids.projectId);assert.equal(a.boardVisible,true);assert.equal(a.formalReportPublished,false);assert.equal(a.employeeConfirmed,false);
  100. });
  101. check('single S5 version and nine reports across the existing workshop',()=>{
  102. assert.equal(history.length,9);assert.deepEqual(history.filter(a=>a.reportSeriesKey===template.taskKey).map(a=>[a.objectId,a.version]),[[ids.artifactId,1]]);
  103. });
  104. check('all previous S1-S4 content and evidence retained with their original question sets',()=>{
  105. for(const [row,file]of [[baseline,'S4-classroom-next-20261103/03-project-S3-save.template.json'],[s4,'S4-classroom-next-20261103/05-project-S4-save.template.json'],[s1,'first-live-S1-v2-template.json'],[s2,'first-live-S2-template.json'],[s3v1,'first-live-S3-template.json'],[s3v2,'first-live-S3-v2-template.json']]){
  106. const t=JSON.parse(fs.readFileSync('work/'+file,'utf8'));for(const k of ['title','rawText','version','stageCode'])assert.deepEqual(row[k],t[k]);
  107. assert.deepEqual(row.report.chapters,t.report.chapters);assert.deepEqual(row.report.evidence,t.report.evidence);assert.deepEqual(row.meta.workshop.pendingQuestions,t.pendingQuestions);assert.deepEqual(row.meta.workshop.sourceIds,t.sourceIds);
  108. }
  109. });
  110. step='public anonymous board projection';const board=await loadPublicBoard({url:'https://kangshifu.fmode.cn/api/functions',appId:'FKieRbIoGt_main'});
  111. check('public S5 report and project stage/concept pointer are consistent without private identity',()=>{
  112. const a=board.artifact.find(a=>a.objectId===ids.artifactId),p=board.project.find(p=>p.objectId===ids.projectId);assert.ok(a&&p);
  113. assert.deepEqual(a.report.chapters,template.report.chapters);assert.deepEqual(a.report.evidence,template.report.evidence);assert.equal(a.dashboardState,'workshop');
  114. assert.equal(p.stageCode,'S5');assert.equal(p.progressState,'in_progress');assert.deepEqual(p.currentConceptRef,ptr('KfConcept',ids.conceptId));
  115. for(const v of [session.meta.attribution.participantAccount?.accountId,session.meta.attribution.agentAccount?.accountId,session.meta.attribution.conversationKey].filter(Boolean))assert.equal(JSON.stringify(board).includes(v),false);
  116. });
  117. const result={ok:true,at:new Date().toISOString(),operation:'live-S5-independent-readback',readOnly:true,businessWrites:0,checksPassed:checks.length,checks,...ids,
  118. reportUrl:'https://kangshifu.fmode.cn/report/'+ids.artifactId,stageCode:'S5',progressState:'in_progress',version:1,conceptVersion:1,reportSeriesKey:template.taskKey,
  119. templateMatched:true,conceptMatched:true,rawTextCodepoints:[...template.rawText].length,chapterCount:artifact.report.chapters.length,sectionCount:sections.length,tableCount:tables.length,
  120. reportTables:tables.map(t=>({title:t.title,columns:t.data.cols.length,rows:t.data.rows.length})),sourceIds:template.sourceIds,evidenceIds,evidenceCount:4,pendingQuestionCount:13,
  121. projectArtifactIds:status.artifacts.map(a=>a.objectId),projectEvents:status.events.map(e=>({eventId:e.objectId,eventType:e.eventType,sequence:e.sequence,artifactId:e.artifactRef?.objectId??null})),
  122. committedWriteCount:operations.length,committedWriteRequests:operations.map(o=>({requestId:o.requestId,operation:o.operation})),expectedReadCommandCount:5,
  123. conceptCount:concepts.length,formulaItemCount:formulaItems.length,workshopArtifactCount:history.length,formalReportPublished:false,employeeConfirmed:false,enterpriseApproval:false,
  124. oldReportsPreserved:true,publicBoardProjectionVerified:true,browserVerified:false,
  125. fieldLocations:{pendingQuestions:'KfProject.meta.pendingQuestions, KfArtifact.meta.workshop.pendingQuestions, concept save receipt',taskKey:'Stored as KfArtifact.reportSeriesKey',formulaItems:'Project-status top-level array; KfFormulaItem rows, not a KfProject property'},
  126. corrections:{businessWriteCommands:3,readCommands:5,chapters:5,sections:15,tables:5,pendingTableThirdColumn:'First eight 原S4问题; final five 承原... references'},
  127. executionConformsToFixedRequestIds:false,requestIdDeviation:{step:6,expected:runbook.steps[5].requestId,actual:actualSaveRequestId,expectedKeyCommitted:false,
  128. artifactId:ids.artifactId,sourceId:ids.outputSourceId,resolution:'Retain actual receipt and IDs; never resave under the unused expected key. No claim that every fixed request ID was followed.',cause:'Not established; remote input/CLI logs not obtained'},
  129. initialStrictAuditRetained:'work/live-S5-readback-20261103.json',
  130. remoteFilesExitcodesAndInvocationIdsBasis:'Operator report, remote filesystem not independently read. CLI command counts follow runbook and committed write audit; not a claim about total database row mutations or hidden reads.'};
  131. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});
  132. console.log(JSON.stringify({ok:true,checksPassed:checks.length,artifactId:ids.artifactId,conceptId:ids.conceptId,projectStage:'S5',progressState:'in_progress',projectReports:3,projectEvents:7,committedWriteCommands:3,sections:15,tables:5,businessWrites:0},null,2));
  133. }catch(error){
  134. const result={ok:false,at:new Date().toISOString(),operation:'S5-independent-readback-stopped',step,checksPassed:checks.length,
  135. checkLine:error.stack?.match(/verify-live-S5-readback\.mjs:(\d+):/)?.[1]??null,readOnly:true,businessWrites:0};
  136. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result,null,2));process.exitCode=1;
  137. }