verify-S5-classroom-preflight.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. // Production access is exclusively read-only. No writes, transition or concept save.
  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 {parseArgs} from '../skills/ksf-workshop-runtime/scripts/ksf-secure.mjs';
  7. const output='work/S5-classroom-live-preflight-20261103.json';
  8. assert.equal(fs.existsSync(output),false);
  9. const folder='work/S5-classroom-next-20261103/';
  10. const runbook=JSON.parse(fs.readFileSync(folder+'runbook.json','utf8'));
  11. const report=JSON.parse(fs.readFileSync(folder+'03-project-S5-save.template.json','utf8'));
  12. const concept=JSON.parse(fs.readFileSync(folder+'02-concept-save.template.json','utf8'));
  13. const s4=JSON.parse(fs.readFileSync('work/S4-classroom-next-20261103/05-project-S4-save.template.json','utf8'));
  14. const ptr=(className,objectId)=>({__type:'Pointer',className,objectId});
  15. const sha=x=>crypto.createHash('sha256').update(x).digest('hex');
  16. const checks=[],snapshots=[];let step='local fields and CLI';
  17. const check=(name,fn)=>{step=name;fn();checks.push({name,passed:true});};
  18. const read=async(className,objectId)=>{
  19. const r=await invoke('ksfGetRecord',{className,objectId});assert.equal(r.http,200);assert.equal(r.body?.result?.ok,true);
  20. assert.equal(r.body.result.data.record?.objectId,objectId);return r.body.result.data.record;
  21. };
  22. const group=async tasks=>{
  23. const r=await Promise.allSettled(tasks);assert.deepEqual(r.flatMap((x,i)=>x.status==='rejected'?[i]:[]),[],'Read failure indexes');return r.map(x=>x.value);
  24. };
  25. const block=(text,start,end)=>{const at=text.indexOf(start),finish=text.indexOf(end,at+start.length);assert.ok(at>=0&&finish>at);return text.slice(at,finish).trim();};
  26. const normalize=s=>s.replace(/\r\n/g,'\n');
  27. try{
  28. check('fixed source evidence, original eight questions and all thirteen align',()=>{
  29. assert.deepEqual(report.sourceIds,s4.sourceIds);assert.deepEqual(report.report.evidence,s4.report.evidence);
  30. assert.deepEqual(report.pendingQuestions.slice(0,8),s4.pendingQuestions);assert.equal(report.pendingQuestions.length,13);
  31. assert.deepEqual(report.pendingQuestions,concept.concept.pendingQuestions);
  32. const tables=report.report.chapters.flatMap(c=>c.sections).filter(s=>s.kind==='table');
  33. const pending=tables.find(s=>s.title==='待补问题');assert.equal(pending.data.rows.length,13);
  34. assert.deepEqual(pending.data.rows.map(x=>x[1]),report.pendingQuestions);
  35. assert.equal(concept.sessionId,null);assert.equal(concept.projectId,null);assert.equal(report.sessionId,null);assert.equal(report.projectId,null);
  36. assert.deepEqual(concept.formulaItems,[]);assert.deepEqual(concept.concept.productionSteps,[]);
  37. for(const k of ['netWeightG','noodleWeightG','packCount','currentSku'])assert.equal(concept.concept.specification[k],null);
  38. for(const k of ['name','targetAudience','usageScenario','valueProposition'])assert.ok(concept.concept[k].length<=200);
  39. assert.equal('opportunity' in report,false);assert.equal(report.artifactType,'concept');assert.equal(report.stageCode,'S5');
  40. });
  41. check('eight stable keys and legal CLI command spelling',()=>{
  42. assert.equal(runbook.steps.length,8);assert.equal(new Set(runbook.steps.map(s=>s.requestId)).size,8);
  43. for(const s of runbook.steps){
  44. const args=[s.command,'--input','F:/isolated-s5-check/'+s.inputFile,'--request-id',s.requestId];
  45. if(['project-progress','concept-save','workshop-save'].includes(s.command))args.push('--agent-profile','agent-registration:11111111-1111-4111-8111-111111111111');
  46. assert.equal(parseArgs(args).command,s.command);
  47. }
  48. });
  49. const names=['ksfTransitionProject','ksfSaveWorkshopConcept','ksfSaveWorkshopTask','ksfGetWorkshopProject'];
  50. step='live state, source, contract, audit and handler read';
  51. const [statusResponse,savedS4,session,contractResponse,functions,ops,concepts,s5Rows]=await group([
  52. invoke('ksfGetWorkshopProject',{sessionId:runbook.sessionId,projectId:runbook.projectId}),
  53. read('KfArtifact',runbook.priorS4ArtifactId),read('KfSession',runbook.sessionId),invoke('ksfGetIngestContract',{}),
  54. classRows('Function',{name:{$in:names}}),classRows('KsfOperation',{requestId:{$in:runbook.steps.map(s=>s.requestId)}}),
  55. classRows('KfConcept',{projectRef:ptr('KfProject',runbook.projectId)}),
  56. classRows('KfArtifact',{reportSeriesKey:runbook.taskKey})
  57. ]);
  58. assert.equal(statusResponse.http,200);assert.equal(statusResponse.body?.result?.ok,true);
  59. assert.equal(contractResponse.http,200);assert.equal(contractResponse.body?.result?.ok,true);
  60. const status=statusResponse.body.result.data,contract=contractResponse.body.result.data;
  61. check('current project remains S4, two reports, four events, no concept or pending S5 writes',()=>{
  62. assert.equal(status.project.objectId,runbook.projectId);assert.equal(status.project.stageCode,'S4');assert.equal(status.project.progressState,'in_progress');
  63. assert.equal(status.project.ownerSession,runbook.sessionId);assert.equal(status.project.dataMode,'live');assert.equal(status.project.status,'workshop');
  64. assert.deepEqual(status.project.opportunityRef,ptr('KfOpportunity',runbook.opportunityId));
  65. assert.deepEqual(status.artifacts.map(x=>x.objectId).sort(),[runbook.priorS3ArtifactId,runbook.priorS4ArtifactId].sort());
  66. assert.equal(status.events.length,4);assert.deepEqual(status.events.map(x=>x.eventType),['project_created','task_submitted','stage_changed','task_submitted']);
  67. assert.equal(status.concept,null);assert.equal(status.project.currentConceptRef??null,null);assert.deepEqual(status.formulaItems,[]);
  68. assert.equal(ops.length,0);assert.equal(concepts.length,0);assert.equal(s5Rows.length,0);assert.equal(session.stageCode,'S4');
  69. });
  70. check('independently read S4 still matches original template and ownership',()=>{
  71. assert.equal(savedS4.rawText,s4.rawText);assert.deepEqual(savedS4.report.chapters,s4.report.chapters);assert.deepEqual(savedS4.report.evidence,s4.report.evidence);
  72. assert.deepEqual(savedS4.meta.workshop.pendingQuestions,s4.pendingQuestions);assert.deepEqual(savedS4.meta.workshop.sourceIds,s4.sourceIds);
  73. assert.equal(savedS4.reportSeriesKey,s4.taskKey);assert.equal(savedS4.dataMode,'live');assert.equal(savedS4.dashboardState,'workshop');
  74. assert.deepEqual(savedS4.projectRef,ptr('KfProject',runbook.projectId));assert.deepEqual(savedS4.meta.attribution,session.meta.attribution);
  75. });
  76. const evidenceIds={'consumer-cq-03':'_9mUml0wmh','consumer-cq-04':'2bFGP7axMo','public-p1':'xHGV_mRWcG','public-p2':'CeAEIwymTa'};
  77. step='exact source and evidence read';
  78. const entries=Object.entries(report.report.evidence);
  79. const sources=await group(entries.flatMap(([key,e])=>[read('KfSource',e.sourceId),read('KfEvidence',evidenceIds[key])]));
  80. for(const [i,[key,e]]of entries.entries())check(key+' preserved quotation and binding',()=>{
  81. const source=sources[i*2],evidence=sources[i*2+1];assert.equal(source.dataMode,'live');assert.equal(evidence.dataMode,'live');
  82. assert.equal(evidence.quote,e.quotes[0].text);assert.ok(source.rawText.includes(evidence.quote));assert.ok(report.rawText.includes(evidence.quote));
  83. assert.deepEqual(evidence.sourceRef,ptr('KfSource',source.objectId));assert.equal(evidence.productRef??null,null);
  84. });
  85. const workshop=normalize(fs.readFileSync('server/src/workshop.mjs','utf8').replace(/^export /gm,''));
  86. const core=normalize(fs.readFileSync('server/src/core.mjs','utf8').replace(/^export /gm,''));
  87. const blocks=[
  88. ...names.map(n=>({name:n,code:block(workshop,'app.handlers.'+n+'=',n==='ksfGetWorkshopProject'?'\n const fields=':'\n app.handlers.')})),
  89. {name:'reportOf',code:block(workshop,'function reportOf(','\n app.handlers.ksfSaveWorkshopTask=')},
  90. {name:'session/project/event',code:block(workshop,'const own=','\n const contract=app.handlers.ksfGetIngestContract;')},
  91. {name:'idempotency',code:block(core,'async function operation(','\n function validateValue(')}
  92. ];
  93. for(const name of names)check('current deployed '+name+' matches relevant command and helpers',()=>{
  94. const matches=functions.filter(f=>f.name===name);assert.equal(matches.length,1);const f=matches[0];assert.equal(typeof f.code,'string');
  95. const code=normalize(f.code);const relevantBlocks=blocks.map(b=>({name:b.name,matched:code.includes(b.code)}));assert.ok(relevantBlocks.every(b=>b.matched));
  96. const filename='work/S5-deployed-'+name+'-20261103.js';fs.writeFileSync(filename,f.code,{flag:'wx'});
  97. snapshots.push({name,functionObjectId:f.objectId,sha256:sha(f.code),file:filename,relevantBlocks});
  98. });
  99. check('workshop capability and table field mapping',()=>{
  100. assert.equal(contract.schemaVersion,'ksf.v0.7');assert.equal(contract.capabilities.workshopProjectTransition,true);
  101. assert.ok(contract.reportTypes.some(t=>t.code==='workshop_auxiliary'));
  102. const conceptBlock=blocks.find(b=>b.name==='ksfSaveWorkshopConcept').code;
  103. assert.ok(conceptBlock.includes('pendingQuestions:p.concept.pendingQuestions'));assert.ok(conceptBlock.includes('currentConceptRef:pointer'));
  104. });
  105. const result={ok:true,at:new Date().toISOString(),readOnly:true,productionBusinessWrites:0,
  106. operation:'S5-current-state-and-command-preflight',checksPassed:checks.length,checks,sessionId:runbook.sessionId,projectId:runbook.projectId,
  107. currentStage:'S4',progressState:'in_progress',projectArtifacts:status.artifacts.map(a=>a.objectId),projectEventCount:4,conceptCount:concepts.length,
  108. s5ArtifactCount:s5Rows.length,matchedFutureWriteAuditRows:ops.length,checkedRequestIds:runbook.steps.map(s=>s.requestId),
  109. contract:{schemaVersion:contract.schemaVersion,contractRevision:contract.contractRevision,release:contract.release},deployedCodeChecks:snapshots,
  110. pendingQuestionCount:13,originalS4QuestionsUnchanged:true,sourceEvidencePairs:entries.map(([k,e])=>({key:k,sourceId:e.sourceId,evidenceId:evidenceIds[k]})),
  111. remoteFilesystemAndProcessesInspected:false,currentMessageIdentityVerifiedByCodex:false,browserVerified:false,
  112. limits:['Remote fixed input files, current-message registration, process state and CLI logs remain operator checks.',
  113. 'No future read-request invocation count inferred from committed operation audit.','A successful preparation is not a production save.']};
  114. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});
  115. console.log(JSON.stringify({ok:true,checksPassed:checks.length,currentStage:'S4',conceptCount:0,s5ArtifactCount:0,matchedFutureWriteAuditRows:0,
  116. deployedCommandsChecked:snapshots.length,productionBusinessWrites:0},null,2));
  117. }catch(error){
  118. const result={ok:false,at:new Date().toISOString(),step,checksPassed:checks.length,readOnly:true,productionBusinessWrites:0,
  119. errorCode:'S5_PREFLIGHT_STOPPED',checkLine:error.stack?.match(/verify-S5-classroom-preflight\.mjs:(\d+):/)?.[1]??null};
  120. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result,null,2));process.exitCode=1;
  121. }