simulate-S5-classroom-next.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. // Captured deployed code, synthetic actor/session and RAM only. No network.
  2. import fs from 'node:fs';
  3. import vm from 'node:vm';
  4. import crypto from 'node:crypto';
  5. import assert from 'node:assert/strict';
  6. import {runWorkshop} from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
  7. const output='work/S5-classroom-isolated-simulation-20261103.json';assert.equal(fs.existsSync(output),false);
  8. const folder='work/S5-classroom-next-20261103/';
  9. const audit=JSON.parse(fs.readFileSync('work/S5-classroom-live-preflight-20261103.json','utf8'));assert.equal(audit.ok,true);
  10. const runbook=JSON.parse(fs.readFileSync(folder+'runbook.json','utf8'));
  11. const tasks=Object.fromEntries(['01-project-S5-progress.template.json','02-concept-save.template.json','03-project-S5-save.template.json'].map(n=>[n,JSON.parse(fs.readFileSync(folder+n,'utf8'))]));
  12. const sha=x=>crypto.createHash('sha256').update(x).digest('hex');
  13. const copy=structuredClone;
  14. const checks=[],calls=[],proof=[];let step='load captured definitions';
  15. const check=(name,fn)=>{step=name;fn();checks.push({name,passed:true});};
  16. function embedded(code,name){
  17. const marker='runDatabaseFunction('+JSON.stringify(name)+',request,response,',at=code.lastIndexOf(marker)+marker.length;assert.ok(at>=marker.length);
  18. let depth=0,quoted=false,escaped=false;
  19. for(let i=at;i<code.length;i++){
  20. const c=code[i];if(quoted){if(escaped)escaped=false;else if(c==='\\')escaped=true;else if(c==='"')quoted=false;continue;}
  21. if(c==='"')quoted=true;else if(c==='{')depth++;else if(c==='}'&&--depth===0)return JSON.parse(code.slice(at,i+1));
  22. }
  23. throw Error('Embedded schema missing');
  24. }
  25. const modules=new Map();let data;
  26. for(const snapshot of audit.deployedCodeChecks){
  27. const code=fs.readFileSync(snapshot.file,'utf8');assert.equal(sha(code),snapshot.sha256);
  28. const runtime=embedded(code,snapshot.name);if(data)assert.deepEqual(runtime,data);else data=runtime;
  29. const impl=vm.runInNewContext(code+'\n;({createApplication,attachWorkshopHandlers});',{Buffer,structuredClone,URL,URLSearchParams,TextEncoder,TextDecoder},{timeout:5000});
  30. modules.set(snapshot.name,impl);proof.push({name:snapshot.name,sha256:snapshot.sha256});
  31. }
  32. const actor={id:'isolated-s5-service',kind:'service',scopes:['ingest:write','data:read','source:write'],projectIds:['*']};
  33. const attribution={agentKey:'agent-registration:11111111-1111-4111-8111-111111111111',agentName:'隔离模拟助手',agentKeyOrigin:'runtime_config',
  34. participantKey:'isolated-s5-participant',participantName:'隔离模拟参与者',channel:'wecom',agentRuntimePlatform:'hermes',agentChannel:'wecom'};
  35. const agentProfile={key:attribution.agentKey,name:attribution.agentName,identityBasis:'runtime_config',role:'workshop',runtimePlatform:'hermes',channel:'wecom'};
  36. const db=new Map(),rows=n=>{if(!db.has(n))db.set(n,[]);return db.get(n);};let sequence=0;
  37. const equal=(a,b)=>JSON.stringify(a??null)===JSON.stringify(b??null),ptr=(className,objectId)=>({__type:'Pointer',className,objectId});
  38. const store={
  39. async transaction(fn){const before=copy(db);try{return await fn(this);}catch(e){db.clear();for(const [k,v]of before)db.set(k,v);throw e;}},
  40. async lock(){},async get(name,id){return copy(rows(name).find(x=>x.objectId===id)??null);},
  41. async list(name,{where={},afterId,limit=1000}={}){return copy(rows(name).filter(r=>Object.entries(where).every(([k,v])=>equal(r[k],v))).filter(r=>!afterId||r.objectId>afterId).sort((a,b)=>a.objectId.localeCompare(b.objectId)).slice(0,limit));},
  42. async create(name,input){for(const key of Object.keys(input))assert.ok(data.tables[name][key],name+'.'+key);const r={...copy(input),objectId:'simS5'+String(++sequence).padStart(5,'0'),createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};rows(name).push(r);return copy(r);},
  43. async update(name,id,input){for(const key of Object.keys(input))assert.ok(data.tables[name][key],name+'.'+key);const r=rows(name).find(x=>x.objectId===id);assert.ok(r);Object.assign(r,copy(input),{updatedAt:new Date().toISOString()});return copy(r);}
  44. };
  45. const sessionId='simS5Session',projectId='simS5Project',baselineId='simS3Baseline',s4Id='simS4Assessment';
  46. rows('KfSession').push({objectId:sessionId,dataMode:'live',workshopKey:'KSF-真实资料-20261102-01',stageCode:'S4',studentName:attribution.participantName,groupId:'',meta:{ownerId:actor.id,attribution}});
  47. rows('KfProject').push({objectId:projectId,dataMode:'live',workshopKey:'KSF-真实资料-20261102-01',stageCode:'S4',stage:'S4 机会验证评估',status:'workshop',progressState:'in_progress',
  48. ownerSession:sessionId,sessionRef:ptr('KfSession',sessionId),artifactCount:2,groupId:'',meta:{ownerId:actor.id,participantKey:attribution.participantKey,pendingQuestions:[]}});
  49. rows('KfArtifact').push({objectId:baselineId,stageCode:'S3',projectRef:ptr('KfProject',projectId)},{objectId:s4Id,stageCode:'S4',projectRef:ptr('KfProject',projectId)});
  50. for(const [i,type]of ['project_created','task_submitted','stage_changed','task_submitted'].entries())rows('KfStageEvent').push({objectId:'simOldEvent'+i,eventType:type,sequence:i+1,projectRef:ptr('KfProject',projectId)});
  51. const publicRows=JSON.parse(fs.readFileSync('work/S4-public-record-inspection-20261103.json','utf8')).records;
  52. for(const r of publicRows.filter(r=>r.className==='KfSource'))rows('KfSource').push({...copy(r.record),meta:{ownerId:actor.id}});
  53. const prior=JSON.parse(fs.readFileSync('work/first-live-S3-v2-template.json','utf8'));
  54. for(const [key,file]of [['consumer-cq-03','cq-03'],['consumer-cq-04','cq-04']]){
  55. const input=JSON.parse(fs.readFileSync('work/consumer-quote-inputs-20261102/'+file+'.input.json','utf8'));
  56. rows('KfSource').push({...copy(input.source),objectId:prior.report.evidence[key].sourceId,meta:{ownerId:actor.id}});
  57. }
  58. const originalSources=copy(rows('KfSource')),originalArtifacts=copy(rows('KfArtifact'));
  59. const apps=new Map();
  60. for(const [name,impl]of modules){const app=impl.createApplication({store,crypto,Parse:{},data,settings:{signingSecret:'isolated-s5-only'},files:{}});impl.attachWorkshopHandlers(app,{store,data});apps.set(name,app);}
  61. const call=async(name,params)=>copy((await (apps.get(name)||apps.get('ksfSaveWorkshopTask')).dispatch(name,copy(params),actor)).data);
  62. const execute=async(i,input)=>{const s=runbook.steps[i],r=await runWorkshop(s.command,copy(input),{requestId:s.requestId,agentProfile,call});calls.push({step:s.step,command:s.command,requestId:s.requestId,ok:true});return r;};
  63. const bind=name=>{const t=tasks[name];assert.equal(t.sessionId,null);assert.equal(t.projectId,null);return {...copy(t),sessionId,projectId};};
  64. const statusInput={sessionId,projectId};
  65. try{
  66. step='negative: no S4 artifact';
  67. rows('KfArtifact').splice(1,1);let missing;
  68. try{await call('ksfTransitionProject',{...statusInput,requestId:'isolated-s5-missing-baseline',toStage:'S5',state:'in_progress',note:'local negative control'});}catch(e){missing=e.code;}
  69. check('missing project S4 report prevents transition with no side effects',()=>{
  70. assert.equal(missing,'STAGE_REQUIREMENTS_NOT_MET');assert.equal(rows('KfProject')[0].stageCode,'S4');assert.equal(rows('KfStageEvent').length,4);assert.equal(rows('KsfOperation').length,0);
  71. });
  72. rows('KfArtifact').push(copy(originalArtifacts[1]));
  73. const before=await execute(0,statusInput);
  74. check('initial S4 status matches precondition',()=>{assert.equal(before.project.stageCode,'S4');assert.equal(before.artifacts.length,2);assert.equal(before.events.length,4);assert.equal(before.concept,null);});
  75. const move=await execute(1,bind('01-project-S5-progress.template.json'));
  76. const moved=await execute(2,statusInput);
  77. check('S5 in progress transition adds only one stage event',()=>{
  78. assert.equal(move.stageCode,'S5');assert.equal(move.progressState,'in_progress');assert.equal(move.enterpriseApproval,false);
  79. assert.equal(moved.project.stageCode,'S5');assert.equal(moved.events.length,5);assert.equal(moved.artifacts.length,2);assert.equal(moved.concept,null);
  80. });
  81. step='negative: reject skipped initial concept version';let skipped;
  82. try{await call('ksfSaveWorkshopConcept',{...bind('02-concept-save.template.json'),version:2,requestId:'isolated-s5-skipped-version'});}catch(e){skipped=e.code;}
  83. check('first concept must be v1; rejected version creates no partial concept',()=>{assert.equal(skipped,'VERSION_CONFLICT');assert.equal(rows('KfConcept').length,0);assert.equal(rows('KfStageEvent').length,5);});
  84. const c=await execute(3,bind('02-concept-save.template.json'));
  85. const conceptStatus=await execute(4,statusInput),t=tasks['02-concept-save.template.json'];
  86. check('concept v1 preserves explicit unknowns, empty arrays and 13 project questions',()=>{
  87. assert.equal(c.version,1);assert.equal(c.enterpriseApproval,false);assert.deepEqual(c.formulaItemIds,[]);assert.deepEqual(c.pendingQuestions,t.concept.pendingQuestions);
  88. assert.equal(conceptStatus.concept.objectId,c.conceptId);assert.equal(conceptStatus.concept.version,1);assert.equal(conceptStatus.concept.dataMode,'live');
  89. assert.deepEqual(conceptStatus.concept.projectRef,ptr('KfProject',projectId));assert.deepEqual(conceptStatus.project.currentConceptRef,ptr('KfConcept',c.conceptId));
  90. for(const k of ['name','targetAudience','usageScenario','specification','valueProposition','flavor','sellingPoints','productionSteps'])assert.deepEqual(conceptStatus.concept[k],t.concept[k]);
  91. assert.deepEqual(conceptStatus.project.meta.pendingQuestions,t.concept.pendingQuestions);assert.equal('pendingQuestions' in conceptStatus.concept,false);
  92. assert.deepEqual(conceptStatus.formulaItems,[]);assert.equal(conceptStatus.events.length,6);assert.equal(conceptStatus.events[5].eventType,'concept_saved');
  93. });
  94. const reportInput=bind('03-project-S5-save.template.json'),saved=await execute(5,reportInput);
  95. const record=(await execute(6,{className:'KfArtifact',objectId:saved.artifactId})).record;
  96. check('S5 report body, four quotes, five tables and pending questions persist',()=>{
  97. assert.equal(saved.artifactId,record.objectId);assert.equal(saved.projectId,projectId);assert.equal(saved.opportunityId,null);assert.equal(saved.boardVisible,true);
  98. assert.equal(saved.formalReportPublished,false);assert.equal(saved.employeeConfirmed,false);
  99. assert.equal(record.artifactType,'concept');assert.equal(record.reportType,'workshop_auxiliary');assert.equal(record.artifactSubtype,'classroom_task');
  100. assert.equal(record.rawText,reportInput.rawText);assert.equal(record.reportSeriesKey,reportInput.taskKey);assert.equal(record.stageCode,'S5');assert.equal(record.version,1);assert.equal(record.evidenceCount,4);
  101. assert.deepEqual(record.projectRef,ptr('KfProject',projectId));assert.equal(record.projectId,projectId);assert.deepEqual(record.meta.workshop.sourceIds,reportInput.sourceIds);
  102. assert.deepEqual(record.meta.workshop.pendingQuestions,reportInput.pendingQuestions);
  103. for(const k of ['chapters','evidence','method','sources','subtitle'])assert.deepEqual(record.report[k],reportInput.report[k]);
  104. assert.deepEqual(record.report.executive.slice(1),reportInput.report.executive);assert.equal(record.report.executive[0],'课堂过程成果,未经员工正式确认;待验证项不作为市场结论。');
  105. assert.equal('conceptRef' in record,false);assert.equal(rows('KfSource').find(s=>s.objectId===saved.sourceId).rawText,reportInput.rawText);
  106. });
  107. const final=await execute(7,statusInput);
  108. check('final S5 in progress, three reports, one concept and seven ordered events',()=>{
  109. assert.equal(final.project.stageCode,'S5');assert.equal(final.project.progressState,'in_progress');assert.equal(final.project.artifactCount,3);
  110. assert.equal(final.artifacts.length,3);assert.equal(final.concept.objectId,c.conceptId);assert.equal(final.concept.version,1);assert.equal(final.formulaItems.length,0);
  111. assert.deepEqual(final.project.meta.pendingQuestions,reportInput.pendingQuestions);
  112. assert.deepEqual(final.events.map(e=>e.eventType),['project_created','task_submitted','stage_changed','task_submitted','stage_changed','concept_saved','task_submitted']);
  113. assert.deepEqual(final.events.map(e=>e.sequence),[1,2,3,4,5,6,7]);
  114. });
  115. check('eight commands, three writes and no duplicate entities or old record changes',()=>{
  116. assert.equal(calls.length,8);assert.deepEqual(calls.map(c=>c.requestId),runbook.steps.map(s=>s.requestId));
  117. assert.equal(rows('KsfOperation').length,3);assert.equal(rows('KfConcept').length,1);assert.equal(rows('KfProject').length,1);
  118. assert.equal(rows('KfSource').length,originalSources.length+1);assert.equal(rows('KfArtifact').length,3);
  119. assert.deepEqual(rows('KfArtifact').filter(a=>originalArtifacts.some(o=>o.objectId===a.objectId)),originalArtifacts);
  120. assert.deepEqual(rows('KfSource').filter(s=>originalSources.some(o=>o.objectId===s.objectId)),originalSources);
  121. for(const cls of ['KfFormulaItem','KfProduct','KfSku','KfMaterial','KfProcess','KfOpportunity'])assert.equal(rows(cls).length,0);
  122. });
  123. const result={ok:true,at:new Date().toISOString(),scope:'Local RAM only, current captured deployed handler code/runtime schema, synthetic identity/session/project and accepted source text',
  124. simulatedOnly:true,productionCalls:0,productionWrites:0,checksPassed:checks.length,checks,calls,deployedCode:proof,templatesModified:false,browserVerified:false,
  125. negativeControls:['S5 refused without project S4 artifact','Initial concept version 2 refused without partial write'],expectedFinal:runbook.expectedFinal,
  126. limits:['No current-message identity or real platform database transport/transaction test.','Production readbacks remain required; stop rules apply.']};
  127. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});
  128. console.log(JSON.stringify({ok:true,checksPassed:checks.length,orderedCommandCount:calls.length,negativeControls:2,productionCalls:0,productionWrites:0,expectedFinal:runbook.expectedFinal},null,2));
  129. }catch(error){
  130. const result={ok:false,at:new Date().toISOString(),step,errorCode:'S5_SIMULATION_STOPPED',failureCode:error.code??null,
  131. checkLine:error.stack?.match(/simulate-S5-classroom-next\.mjs:(\d+):/)?.[1]??null,commandsCompleted:calls.length,checksPassed:checks.length,
  132. productionCalls:0,productionWrites:0,simulatedOnly:true};
  133. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result,null,2));process.exitCode=1;
  134. }