verify-live-S4-readback.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. // Independent read-only acceptance of the completed S4 classroom chain.
  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 output='work/live-S4-readback-20261103.json';
  8. assert.equal(fs.existsSync(output),false,'Preserve previous acceptance');
  9. const base='KSF-真实资料-20261102-01',sessionId='btrFqkz0yw';
  10. const ids={candidateArtifactId:'V2z4ox7Ehx',candidateSourceId:'CFjbZlMWp5',opportunityId:'QE9hjtCj8E',projectId:'S_CikxZeBs',
  11. baselineArtifactId:'XF4h-Z7l5o',baselineSourceId:'H_eFL1eT-d',artifactId:'U3_W7WDliP',outputSourceId:'y-KcU9Ijc0'};
  12. const ptr=(className,objectId)=>({__type:'Pointer',className,objectId});
  13. const sha=x=>crypto.createHash('sha256').update(x).digest('hex');
  14. const checks=[];
  15. let step='fixed template fingerprint';
  16. const check=(name,fn)=>{step=name;fn();checks.push({name,passed:true});};
  17. const read=async(className,objectId)=>{
  18. const r=await invoke('ksfGetRecord',{className,objectId});assert.equal(r.http,200);assert.equal(r.body?.result?.ok,true);
  19. const row=r.body.result.data.record;assert.equal(row?.objectId,objectId);return row;
  20. };
  21. const readGroup=async tasks=>{
  22. const all=await Promise.allSettled(tasks);assert.deepEqual(all.flatMap((r,i)=>r.status==='rejected'?[i]:[]),[],'Read failure indexes');
  23. return all.map(r=>r.value);
  24. };
  25. const dir='work/S4-classroom-next-20261103/';
  26. const templateNames=['01-candidate-save.template.json','03-project-S3-save.template.json','05-project-S4-save.template.json'];
  27. const templates=templateNames.map(n=>JSON.parse(fs.readFileSync(dir+n,'utf8')));
  28. const template=templates[2];
  29. const requestIds=[...fs.readFileSync('F:/文档/工作坊当前资料/47-发给王刚001-S4课堂项目与桌面评估.txt','utf8').matchAll(/requestId=(KSF-[^\s。;]+)/gu)].map(m=>m[1]);
  30. try{
  31. check('fixed S4 template and scope',()=>{
  32. assert.equal(sha(fs.readFileSync(dir+templateNames[2])),'fd4d5b19f9dc104ae7e31a0aebd56e578c24280ca1ba515bd583d8b070a57f7b');
  33. assert.equal(requestIds.length,12);assert.equal(template.sessionId,null);assert.equal(template.projectId,null);
  34. assert.equal(template.version,1);assert.equal(template.stageCode,'S4');assert.equal(template.artifactType,'assessment_report');
  35. assert.equal(template.taskKey,base+':S4');assert.equal([...template.rawText].length,1862);assert.equal(template.pendingQuestions.length,8);
  36. });
  37. step='read chain, status, history and write audit';
  38. const [session,candidate,candidateSource,opp,project,baseline,baselineSource,artifact,source,statusResponse,operations,history,opps,projects,
  39. s1,s2,s3v1,s3v2]=await readGroup([
  40. read('KfSession',sessionId),read('KfArtifact',ids.candidateArtifactId),read('KfSource',ids.candidateSourceId),read('KfOpportunity',ids.opportunityId),
  41. read('KfProject',ids.projectId),read('KfArtifact',ids.baselineArtifactId),read('KfSource',ids.baselineSourceId),
  42. read('KfArtifact',ids.artifactId),read('KfSource',ids.outputSourceId),invoke('ksfGetWorkshopProject',{sessionId,projectId:ids.projectId}),
  43. classRows('KsfOperation',{requestId:{$in:requestIds}}),classRows('KfArtifact',{workshopKey:base}),
  44. classRows('KfOpportunity',{sessionRef:ptr('KfSession',sessionId)}),classRows('KfProject',{sessionRef:ptr('KfSession',sessionId)}),
  45. read('KfArtifact','G8IrkkDG7X'),read('KfArtifact','b7NOby5ys0'),read('KfArtifact','oiNxX7o27j'),read('KfArtifact','ZW1FB5zeHr')
  46. ]);
  47. assert.equal(statusResponse.http,200);assert.equal(statusResponse.body?.result?.ok,true);const status=statusResponse.body.result.data;
  48. for(const [i,row,body,pid]of [[0,candidate,candidateSource,null],[1,baseline,baselineSource,ids.projectId],[2,artifact,source,ids.projectId]]){
  49. const t=templates[i];
  50. check(templateNames[i]+' saved fields and body equal fixed template',()=>{
  51. for(const key of ['title','rawText','version','stageCode','artifactType'])assert.deepEqual(row[key],t[key]);
  52. assert.equal(row.reportSeriesKey,t.taskKey);assert.equal(row.reportType,'workshop_auxiliary');assert.equal(row.artifactSubtype,'classroom_task');
  53. assert.equal(row.schemaVersion,'ksf.v0.7');assert.equal(row.recordState,'active');assert.equal(row.dataMode,'live');
  54. assert.equal(row.dashboardState,'workshop');assert.equal(row.ownerSession,sessionId);assert.deepEqual(row.sessionRef,ptr('KfSession',sessionId));
  55. assert.equal(row.projectId??null,pid);assert.deepEqual(row.projectRef??null,pid?ptr('KfProject',pid):null);
  56. assert.deepEqual(row.sourceRef,ptr('KfSource',body.objectId));assert.equal(body.rawText,t.rawText);assert.equal(body.dataMode,'live');
  57. assert.deepEqual(row.meta.workshop.sourceIds,t.sourceIds);assert.deepEqual(row.meta.workshop.pendingQuestions,t.pendingQuestions);
  58. for(const key of ['chapters','evidence','method','sources','subtitle'])assert.deepEqual(row.report[key],t.report[key]);
  59. assert.deepEqual(row.report.executive,['课堂过程成果,未经员工正式确认;待验证项不作为市场结论。',...t.report.executive]);
  60. assert.equal(row.report.title,t.title);assert.ok(Number.isFinite(Date.parse(row.report.generatedAt)));
  61. assert.equal(row.evidenceCount,4);assert.deepEqual(row.tags,['拌面','课堂过程成果']);
  62. assert.deepEqual(row.meta.attribution,session.meta.attribution);assert.equal(row.agentId,session.meta.attribution.agentKey);assert.equal(row.agentName,'王刚001');
  63. assert.equal(row.dashboardPublishedAt??null,null);assert.equal(row.contentConfirmedAt??null,null);assert.equal(row.meta.workshop.formalConfirmation,false);
  64. });
  65. }
  66. check('S4 three visible tables and eight pending rows',()=>{
  67. const tables=artifact.report.chapters.flatMap(c=>c.sections).filter(s=>s.kind==='table');
  68. assert.deepEqual(tables.map(t=>[t.title,t.data.cols.length,t.data.rows.length]),[['主引文',3,4],['桌面评估',4,5],['待补问题',2,8]]);
  69. assert.deepEqual(tables[2].data.rows,template.pendingQuestions.map((q,i)=>[String(i+1),q]));
  70. assert.deepEqual(artifact.report.chapters.map(c=>c.sections.length),[3,2,3,2]);
  71. });
  72. const evidenceIds={'consumer-cq-03':'_9mUml0wmh','consumer-cq-04':'2bFGP7axMo','public-p1':'xHGV_mRWcG','public-p2':'CeAEIwymTa'};
  73. step='read source and evidence bindings';
  74. const entries=Object.entries(template.report.evidence);
  75. const evidenceRows=await readGroup(entries.flatMap(([key,e])=>[read('KfSource',e.sourceId),read('KfEvidence',evidenceIds[key])]));
  76. for(const [index,[key,e]]of entries.entries())check(key+' exact quote, source and evidence identity',()=>{
  77. const origin=evidenceRows[index*2],evidence=evidenceRows[index*2+1];
  78. assert.equal(origin.dataMode,'live');assert.equal(origin.sourceKind,key==='public-p1'?'ecommerce':'social');assert.equal(origin.platform,key==='public-p1'?'suning':'smzdm');
  79. assert.deepEqual(evidence.sourceRef,ptr('KfSource',e.sourceId));assert.equal(evidence.productRef??null,null);assert.equal(evidence.quote,e.quotes[0].text);
  80. assert.equal(evidence.evidenceKind,key==='public-p1'?'retailer_listing_observation':'public_user_experience_self_report');
  81. assert.equal(evidence.verificationState,'archived_text_and_screenshot_checked');assert.equal(evidence.quoteForm,'verbatim_excerpt');
  82. assert.ok(origin.rawText.includes(e.quotes[0].text));assert.ok(template.rawText.includes(e.quotes[0].text));assert.equal(e.quotes.length,1);
  83. });
  84. check('single classroom opportunity and project retain hypothesis scope',()=>{
  85. assert.deepEqual(opps.map(x=>x.objectId),[ids.opportunityId]);assert.deepEqual(projects.map(x=>x.objectId),[ids.projectId]);
  86. assert.equal(opp.claimKind,'hypothesis');assert.equal(opp.status,'approved');assert.equal(opp.version,1);assert.equal(opp.opportunitySeriesKey,base+':portion-fit');
  87. assert.equal(opp.linkedProject,ids.projectId);assert.deepEqual(opp.linkedArtifacts,[ids.candidateArtifactId]);
  88. for(const key of ['title','demandSummary','targetAudience','usageScenario','fusionDefinition'])assert.equal(opp[key],templates[0].opportunity[key]);
  89. assert.equal(project.stageCode,'S4');assert.equal(project.progressState,'in_progress');assert.equal(project.status,'workshop');assert.equal(project.dataMode,'live');
  90. assert.equal(project.ownerSession,sessionId);assert.deepEqual(project.sessionRef,ptr('KfSession',sessionId));assert.deepEqual(project.opportunityRef,ptr('KfOpportunity',ids.opportunityId));
  91. assert.equal(project.meta.projectKey,base+':portion-fit-project');assert.equal(project.artifactCount,2);assert.equal(session.stageCode,'S4');
  92. });
  93. check('project status contains two artifacts, four ordered events and no concept',()=>{
  94. assert.equal(status.project.objectId,ids.projectId);assert.equal(status.project.stageCode,'S4');assert.equal(status.project.progressState,'in_progress');
  95. assert.deepEqual(status.artifacts.map(x=>x.objectId).sort(),[ids.baselineArtifactId,ids.artifactId].sort());
  96. assert.deepEqual(status.events.map(x=>x.eventType),['project_created','task_submitted','stage_changed','task_submitted']);
  97. assert.deepEqual(status.events.map(x=>x.sequence),[1,2,3,4]);assert.equal(status.events[0].objectId,'GGDTcg5YTy');assert.equal(status.events[2].objectId,'hOFghbcMsY');
  98. assert.equal(status.events[1].artifactRef.objectId,ids.baselineArtifactId);assert.equal(status.events[3].artifactRef.objectId,ids.artifactId);
  99. assert.equal(status.concept,null);assert.deepEqual(status.formulaItems,[]);
  100. });
  101. check('five committed writes, one per business key including recovered transition',()=>{
  102. const writes=[requestIds[0],requestIds[3],requestIds[5],requestIds[7],requestIds[9]];
  103. assert.equal(operations.length,5);assert.deepEqual(operations.map(x=>x.requestId).sort(),writes.sort());
  104. for(const [key,id]of [[requestIds[0],ids.candidateArtifactId],[requestIds[5],ids.baselineArtifactId],[requestIds[9],ids.artifactId]]){
  105. const receipt=operations.find(x=>x.requestId===key).receipt;assert.equal(receipt.artifactId,id);assert.equal(receipt.boardVisible,true);assert.equal(receipt.employeeConfirmed,false);assert.equal(receipt.formalReportPublished,false);
  106. }
  107. const created=operations.find(x=>x.requestId===requestIds[3]).receipt;assert.equal(created.projectId,ids.projectId);assert.equal(created.enterpriseApproval,false);
  108. const moved=operations.find(x=>x.requestId===requestIds[7]).receipt;assert.equal(moved.projectId,ids.projectId);assert.equal(moved.stageCode,'S4');assert.equal(moved.enterpriseApproval,false);
  109. });
  110. check('one S4 version, eight workshop reports and no S5 report',()=>{
  111. assert.equal(history.length,8);assert.deepEqual(history.filter(x=>x.reportSeriesKey===template.taskKey).map(x=>[x.objectId,x.version]),[[ids.artifactId,1]]);
  112. assert.equal(history.some(x=>x.stageCode==='S5'),false);
  113. });
  114. check('previous S1, S2 and both S3 reports still match their templates',()=>{
  115. for(const [row,file]of [[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']]){
  116. const t=JSON.parse(fs.readFileSync('work/'+file,'utf8'));
  117. for(const k of ['title','rawText','version','stageCode'])assert.deepEqual(row[k],t[k]);
  118. assert.deepEqual(row.report.chapters,t.report.chapters);assert.deepEqual(row.report.evidence,t.report.evidence);assert.deepEqual(row.meta.workshop.sourceIds,t.sourceIds);
  119. assert.deepEqual(row.meta.attribution,session.meta.attribution);
  120. }
  121. });
  122. step='anonymous public board projection';
  123. const board=await loadPublicBoard({url:'https://kangshifu.fmode.cn/api/functions',appId:'FKieRbIoGt_main'});
  124. check('anonymous board includes three new reports, project and selected opportunity',()=>{
  125. for(const [row,t]of [[candidate,templates[0]],[baseline,templates[1]],[artifact,template]]){
  126. const publicRow=board.artifact.find(x=>x.objectId===row.objectId);assert.ok(publicRow);assert.deepEqual(publicRow.report.chapters,t.report.chapters);assert.deepEqual(publicRow.report.evidence,t.report.evidence);
  127. }
  128. const p=board.project.find(x=>x.objectId===ids.projectId);assert.ok(p);assert.equal(p.stageCode,'S4');assert.equal(p.progressState,'in_progress');
  129. assert.ok(board.opportunity.some(x=>x.objectId===ids.opportunityId));
  130. for(const value of [session.meta.attribution.participantAccount?.accountId,session.meta.attribution.agentAccount?.accountId,session.meta.attribution.conversationKey].filter(Boolean))assert.equal(JSON.stringify(board).includes(value),false);
  131. });
  132. const result={ok:true,operation:'live-S4-independent-readback',at:new Date().toISOString(),readOnly:true,businessWrites:0,checksPassed:checks.length,checks,
  133. sessionId,...ids,reportUrl:'https://kangshifu.fmode.cn/report/'+ids.artifactId,version:1,stageCode:'S4',progressState:'in_progress',dataMode:'live',dashboardState:'workshop',
  134. reportSeriesKey:template.taskKey,sourceIds:template.sourceIds,evidenceIds,evidenceCount:4,pendingQuestionCount:8,rawTextCodepoints:[...template.rawText].length,
  135. reportTables:artifact.report.chapters.flatMap(c=>c.sections).filter(s=>s.kind==='table').map(s=>({title:s.title,columns:s.data.cols.length,rows:s.data.rows.length})),
  136. 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})),
  137. committedWriteRequests:operations.map(o=>({requestId:o.requestId,operation:o.operation})),committedWriteCount:operations.length,
  138. opportunityCount:opps.length,projectCount:projects.length,workshopArtifactCount:history.length,formalReportPublished:false,employeeConfirmed:false,enterpriseApproval:false,
  139. conceptPresent:false,templateMatched:true,oldReportsPreserved:true,publicBoardProjectionVerified:true,browserVerified:false,
  140. invalidStep08PreservedBasis:'Operator report of old USAGE logs. One committed transition now independently verified; historical failure is not erased.',
  141. remoteFilesAndExitcodesBasis:'Operator report only; remote filesystem and CLI exit codes not independently read',templateSha256:sha(fs.readFileSync(dir+templateNames[2]))};
  142. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});
  143. console.log(JSON.stringify({ok:true,checksPassed:checks.length,artifactId:ids.artifactId,projectId:ids.projectId,stageCode:'S4',progressState:'in_progress',projectArtifactCount:status.artifacts.length,projectEventCount:status.events.length,committedWriteCount:operations.length,businessWrites:0},null,2));
  144. }catch(error){
  145. const result={ok:false,operation:'live-S4-independent-readback-stopped',at:new Date().toISOString(),step,
  146. checkLine:error.stack?.match(/verify-live-S4-readback\.mjs:(\d+):/)?.[1]??null,checksPassed:checks.length,readOnly:true,businessWrites:0};
  147. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result,null,2));process.exitCode=1;
  148. }