// Independent read-only acceptance of the completed S4 classroom chain. import fs from 'node:fs'; import crypto from 'node:crypto'; import assert from 'node:assert/strict'; import {invoke,classRows} from '../server/scripts/platform-client.mjs'; import {loadPublicBoard} from '../src/services/workshop-data.js'; const output='work/live-S4-readback-20261103.json'; assert.equal(fs.existsSync(output),false,'Preserve previous acceptance'); const base='KSF-真实资料-20261102-01',sessionId='btrFqkz0yw'; const ids={candidateArtifactId:'V2z4ox7Ehx',candidateSourceId:'CFjbZlMWp5',opportunityId:'QE9hjtCj8E',projectId:'S_CikxZeBs', baselineArtifactId:'XF4h-Z7l5o',baselineSourceId:'H_eFL1eT-d',artifactId:'U3_W7WDliP',outputSourceId:'y-KcU9Ijc0'}; const ptr=(className,objectId)=>({__type:'Pointer',className,objectId}); const sha=x=>crypto.createHash('sha256').update(x).digest('hex'); const checks=[]; let step='fixed template fingerprint'; const check=(name,fn)=>{step=name;fn();checks.push({name,passed:true});}; 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; }; const readGroup=async tasks=>{ const all=await Promise.allSettled(tasks);assert.deepEqual(all.flatMap((r,i)=>r.status==='rejected'?[i]:[]),[],'Read failure indexes'); return all.map(r=>r.value); }; const dir='work/S4-classroom-next-20261103/'; const templateNames=['01-candidate-save.template.json','03-project-S3-save.template.json','05-project-S4-save.template.json']; const templates=templateNames.map(n=>JSON.parse(fs.readFileSync(dir+n,'utf8'))); const template=templates[2]; const requestIds=[...fs.readFileSync('F:/文档/工作坊当前资料/47-发给王刚001-S4课堂项目与桌面评估.txt','utf8').matchAll(/requestId=(KSF-[^\s。;]+)/gu)].map(m=>m[1]); try{ check('fixed S4 template and scope',()=>{ assert.equal(sha(fs.readFileSync(dir+templateNames[2])),'fd4d5b19f9dc104ae7e31a0aebd56e578c24280ca1ba515bd583d8b070a57f7b'); assert.equal(requestIds.length,12);assert.equal(template.sessionId,null);assert.equal(template.projectId,null); assert.equal(template.version,1);assert.equal(template.stageCode,'S4');assert.equal(template.artifactType,'assessment_report'); assert.equal(template.taskKey,base+':S4');assert.equal([...template.rawText].length,1862);assert.equal(template.pendingQuestions.length,8); }); step='read chain, status, history and write audit'; const [session,candidate,candidateSource,opp,project,baseline,baselineSource,artifact,source,statusResponse,operations,history,opps,projects, s1,s2,s3v1,s3v2]=await readGroup([ read('KfSession',sessionId),read('KfArtifact',ids.candidateArtifactId),read('KfSource',ids.candidateSourceId),read('KfOpportunity',ids.opportunityId), read('KfProject',ids.projectId),read('KfArtifact',ids.baselineArtifactId),read('KfSource',ids.baselineSourceId), read('KfArtifact',ids.artifactId),read('KfSource',ids.outputSourceId),invoke('ksfGetWorkshopProject',{sessionId,projectId:ids.projectId}), classRows('KsfOperation',{requestId:{$in:requestIds}}),classRows('KfArtifact',{workshopKey:base}), classRows('KfOpportunity',{sessionRef:ptr('KfSession',sessionId)}),classRows('KfProject',{sessionRef:ptr('KfSession',sessionId)}), read('KfArtifact','G8IrkkDG7X'),read('KfArtifact','b7NOby5ys0'),read('KfArtifact','oiNxX7o27j'),read('KfArtifact','ZW1FB5zeHr') ]); assert.equal(statusResponse.http,200);assert.equal(statusResponse.body?.result?.ok,true);const status=statusResponse.body.result.data; for(const [i,row,body,pid]of [[0,candidate,candidateSource,null],[1,baseline,baselineSource,ids.projectId],[2,artifact,source,ids.projectId]]){ const t=templates[i]; check(templateNames[i]+' saved fields and body equal fixed template',()=>{ for(const key of ['title','rawText','version','stageCode','artifactType'])assert.deepEqual(row[key],t[key]); assert.equal(row.reportSeriesKey,t.taskKey);assert.equal(row.reportType,'workshop_auxiliary');assert.equal(row.artifactSubtype,'classroom_task'); assert.equal(row.schemaVersion,'ksf.v0.7');assert.equal(row.recordState,'active');assert.equal(row.dataMode,'live'); assert.equal(row.dashboardState,'workshop');assert.equal(row.ownerSession,sessionId);assert.deepEqual(row.sessionRef,ptr('KfSession',sessionId)); assert.equal(row.projectId??null,pid);assert.deepEqual(row.projectRef??null,pid?ptr('KfProject',pid):null); assert.deepEqual(row.sourceRef,ptr('KfSource',body.objectId));assert.equal(body.rawText,t.rawText);assert.equal(body.dataMode,'live'); assert.deepEqual(row.meta.workshop.sourceIds,t.sourceIds);assert.deepEqual(row.meta.workshop.pendingQuestions,t.pendingQuestions); for(const key of ['chapters','evidence','method','sources','subtitle'])assert.deepEqual(row.report[key],t.report[key]); assert.deepEqual(row.report.executive,['课堂过程成果,未经员工正式确认;待验证项不作为市场结论。',...t.report.executive]); assert.equal(row.report.title,t.title);assert.ok(Number.isFinite(Date.parse(row.report.generatedAt))); assert.equal(row.evidenceCount,4);assert.deepEqual(row.tags,['拌面','课堂过程成果']); assert.deepEqual(row.meta.attribution,session.meta.attribution);assert.equal(row.agentId,session.meta.attribution.agentKey);assert.equal(row.agentName,'王刚001'); assert.equal(row.dashboardPublishedAt??null,null);assert.equal(row.contentConfirmedAt??null,null);assert.equal(row.meta.workshop.formalConfirmation,false); }); } check('S4 three visible tables and eight pending rows',()=>{ const tables=artifact.report.chapters.flatMap(c=>c.sections).filter(s=>s.kind==='table'); assert.deepEqual(tables.map(t=>[t.title,t.data.cols.length,t.data.rows.length]),[['主引文',3,4],['桌面评估',4,5],['待补问题',2,8]]); assert.deepEqual(tables[2].data.rows,template.pendingQuestions.map((q,i)=>[String(i+1),q])); assert.deepEqual(artifact.report.chapters.map(c=>c.sections.length),[3,2,3,2]); }); const evidenceIds={'consumer-cq-03':'_9mUml0wmh','consumer-cq-04':'2bFGP7axMo','public-p1':'xHGV_mRWcG','public-p2':'CeAEIwymTa'}; step='read source and evidence bindings'; const entries=Object.entries(template.report.evidence); const evidenceRows=await readGroup(entries.flatMap(([key,e])=>[read('KfSource',e.sourceId),read('KfEvidence',evidenceIds[key])])); for(const [index,[key,e]]of entries.entries())check(key+' exact quote, source and evidence identity',()=>{ const origin=evidenceRows[index*2],evidence=evidenceRows[index*2+1]; assert.equal(origin.dataMode,'live');assert.equal(origin.sourceKind,key==='public-p1'?'ecommerce':'social');assert.equal(origin.platform,key==='public-p1'?'suning':'smzdm'); assert.deepEqual(evidence.sourceRef,ptr('KfSource',e.sourceId));assert.equal(evidence.productRef??null,null);assert.equal(evidence.quote,e.quotes[0].text); assert.equal(evidence.evidenceKind,key==='public-p1'?'retailer_listing_observation':'public_user_experience_self_report'); assert.equal(evidence.verificationState,'archived_text_and_screenshot_checked');assert.equal(evidence.quoteForm,'verbatim_excerpt'); assert.ok(origin.rawText.includes(e.quotes[0].text));assert.ok(template.rawText.includes(e.quotes[0].text));assert.equal(e.quotes.length,1); }); check('single classroom opportunity and project retain hypothesis scope',()=>{ assert.deepEqual(opps.map(x=>x.objectId),[ids.opportunityId]);assert.deepEqual(projects.map(x=>x.objectId),[ids.projectId]); assert.equal(opp.claimKind,'hypothesis');assert.equal(opp.status,'approved');assert.equal(opp.version,1);assert.equal(opp.opportunitySeriesKey,base+':portion-fit'); assert.equal(opp.linkedProject,ids.projectId);assert.deepEqual(opp.linkedArtifacts,[ids.candidateArtifactId]); for(const key of ['title','demandSummary','targetAudience','usageScenario','fusionDefinition'])assert.equal(opp[key],templates[0].opportunity[key]); assert.equal(project.stageCode,'S4');assert.equal(project.progressState,'in_progress');assert.equal(project.status,'workshop');assert.equal(project.dataMode,'live'); assert.equal(project.ownerSession,sessionId);assert.deepEqual(project.sessionRef,ptr('KfSession',sessionId));assert.deepEqual(project.opportunityRef,ptr('KfOpportunity',ids.opportunityId)); assert.equal(project.meta.projectKey,base+':portion-fit-project');assert.equal(project.artifactCount,2);assert.equal(session.stageCode,'S4'); }); check('project status contains two artifacts, four ordered events and no concept',()=>{ assert.equal(status.project.objectId,ids.projectId);assert.equal(status.project.stageCode,'S4');assert.equal(status.project.progressState,'in_progress'); assert.deepEqual(status.artifacts.map(x=>x.objectId).sort(),[ids.baselineArtifactId,ids.artifactId].sort()); assert.deepEqual(status.events.map(x=>x.eventType),['project_created','task_submitted','stage_changed','task_submitted']); 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'); assert.equal(status.events[1].artifactRef.objectId,ids.baselineArtifactId);assert.equal(status.events[3].artifactRef.objectId,ids.artifactId); assert.equal(status.concept,null);assert.deepEqual(status.formulaItems,[]); }); check('five committed writes, one per business key including recovered transition',()=>{ const writes=[requestIds[0],requestIds[3],requestIds[5],requestIds[7],requestIds[9]]; assert.equal(operations.length,5);assert.deepEqual(operations.map(x=>x.requestId).sort(),writes.sort()); for(const [key,id]of [[requestIds[0],ids.candidateArtifactId],[requestIds[5],ids.baselineArtifactId],[requestIds[9],ids.artifactId]]){ 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); } const created=operations.find(x=>x.requestId===requestIds[3]).receipt;assert.equal(created.projectId,ids.projectId);assert.equal(created.enterpriseApproval,false); 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); }); check('one S4 version, eight workshop reports and no S5 report',()=>{ assert.equal(history.length,8);assert.deepEqual(history.filter(x=>x.reportSeriesKey===template.taskKey).map(x=>[x.objectId,x.version]),[[ids.artifactId,1]]); assert.equal(history.some(x=>x.stageCode==='S5'),false); }); check('previous S1, S2 and both S3 reports still match their templates',()=>{ 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']]){ const t=JSON.parse(fs.readFileSync('work/'+file,'utf8')); for(const k of ['title','rawText','version','stageCode'])assert.deepEqual(row[k],t[k]); assert.deepEqual(row.report.chapters,t.report.chapters);assert.deepEqual(row.report.evidence,t.report.evidence);assert.deepEqual(row.meta.workshop.sourceIds,t.sourceIds); assert.deepEqual(row.meta.attribution,session.meta.attribution); } }); step='anonymous public board projection'; const board=await loadPublicBoard({url:'https://kangshifu.fmode.cn/api/functions',appId:'FKieRbIoGt_main'}); check('anonymous board includes three new reports, project and selected opportunity',()=>{ for(const [row,t]of [[candidate,templates[0]],[baseline,templates[1]],[artifact,template]]){ 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); } const p=board.project.find(x=>x.objectId===ids.projectId);assert.ok(p);assert.equal(p.stageCode,'S4');assert.equal(p.progressState,'in_progress'); assert.ok(board.opportunity.some(x=>x.objectId===ids.opportunityId)); 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); }); const result={ok:true,operation:'live-S4-independent-readback',at:new Date().toISOString(),readOnly:true,businessWrites:0,checksPassed:checks.length,checks, sessionId,...ids,reportUrl:'https://kangshifu.fmode.cn/report/'+ids.artifactId,version:1,stageCode:'S4',progressState:'in_progress',dataMode:'live',dashboardState:'workshop', reportSeriesKey:template.taskKey,sourceIds:template.sourceIds,evidenceIds,evidenceCount:4,pendingQuestionCount:8,rawTextCodepoints:[...template.rawText].length, 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})), 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})), committedWriteRequests:operations.map(o=>({requestId:o.requestId,operation:o.operation})),committedWriteCount:operations.length, opportunityCount:opps.length,projectCount:projects.length,workshopArtifactCount:history.length,formalReportPublished:false,employeeConfirmed:false,enterpriseApproval:false, conceptPresent:false,templateMatched:true,oldReportsPreserved:true,publicBoardProjectionVerified:true,browserVerified:false, invalidStep08PreservedBasis:'Operator report of old USAGE logs. One committed transition now independently verified; historical failure is not erased.', remoteFilesAndExitcodesBasis:'Operator report only; remote filesystem and CLI exit codes not independently read',templateSha256:sha(fs.readFileSync(dir+templateNames[2]))}; fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'}); 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)); }catch(error){ const result={ok:false,operation:'live-S4-independent-readback-stopped',at:new Date().toISOString(),step, checkLine:error.stack?.match(/verify-live-S4-readback\.mjs:(\d+):/)?.[1]??null,checksPassed:checks.length,readOnly:true,businessWrites:0}; fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result,null,2));process.exitCode=1; }