accept-workshop-retest.mjs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136
  1. // Independent-case outputs exercise current application handlers in memory only.
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import os from 'node:os';
  5. import crypto from 'node:crypto';
  6. import assert from 'node:assert/strict';
  7. import {fileURLToPath} from 'node:url';
  8. import {createApplication} from '../server/src/core.mjs';
  9. import {attachWorkshopHandlers} from '../server/src/workshop.mjs';
  10. import {runWorkshop} from '../skills/ksf-workshop-runtime/scripts/ksf-workshop.mjs';
  11. import {registerAgentProfile,registerParticipantProfile,readAgentProfileByKey,readParticipantProfile} from '../skills/ksf-workshop-runtime/scripts/ksf-identity.mjs';
  12. const root=path.resolve(path.dirname(fileURLToPath(import.meta.url)),'..');
  13. const work=path.join(root,'docs/workshop-skill-retest-20261109-r2');
  14. const read=p=>JSON.parse(fs.readFileSync(p,'utf8').replace(/^\uFEFF/,''));
  15. const data=read(path.join(root,'server/references/runtime-data.json'));
  16. const sectionText=s=>s.kind==='table'?[s.title,s.data.cols.join(' | '),...s.data.rows.map(r=>r.join(' | '))].join('\n'):s.title+'\n'+s.text;
  17. function memory(){
  18. let db={},seq=0,writes=0;
  19. return {
  20. get writes(){return writes;},
  21. async transaction(fn){const before=structuredClone(db),count=writes;try{return await fn(this);}catch(e){db=before;writes=count;throw e;}},async lock(){},
  22. async get(n,id){return structuredClone(db[n]?.find(r=>r.objectId===id)||null);},
  23. async list(n,{where={},afterId,limit=200,search}={}){return structuredClone((db[n]||[]).filter(r=>Object.entries(where).every(([k,v])=>JSON.stringify(r[k]??null)===JSON.stringify(v))).filter(r=>!search||JSON.stringify(r).includes(search)).filter(r=>!afterId||r.objectId>afterId).sort((a,b)=>a.objectId.localeCompare(b.objectId)).slice(0,limit));},
  24. async create(n,p){for(const k of Object.keys(p))assert.ok(data.tables[n]?.[k],n+'.'+k);const r={...structuredClone(p),objectId:'retest'+String(++seq).padStart(8,'0'),createdAt:new Date().toISOString(),updatedAt:new Date().toISOString()};(db[n]??=[]).push(r);writes++;return structuredClone(r);},
  25. async update(n,id,p){for(const k of Object.keys(p))assert.ok(data.tables[n]?.[k],n+'.'+k);const r=db[n]?.find(r=>r.objectId===id);assert.ok(r);Object.assign(r,structuredClone(p));writes++;return structuredClone(r);}
  26. };
  27. }
  28. function preflight(m){
  29. assert.ok(m.id&&m.stage&&m.type&&m.finding&&m.meaning&&m.reply&&m.next,m.id+' module contract');
  30. assert.ok(typeof m.reply==='string'&&Array.isArray(m.usedRecordIds),m.id+' reply/provenance contract');
  31. assert.ok(Array.isArray(m.sections)&&m.sections.length,m.id+' full sections');
  32. for(const s of m.sections){
  33. assert.ok(s.title,m.id+' section title');
  34. if(s.kind==='table'){
  35. assert.ok(s.data?.cols?.length&&Array.isArray(s.data.rows));
  36. for(const row of s.data.rows){assert.equal(row.length,s.data.cols.length,m.id+' rectangular table');assert.ok(row.every(c=>typeof c==='string'),m.id+' string cells');}
  37. }else assert.ok(['para','longpara'].includes(s.kind)&&typeof s.text==='string'&&s.text.trim());
  38. }
  39. }
  40. const table=(title,cols,rows)=>({kind:'table',title,data:{cols,rows:rows.map(r=>r.map(String))}});
  41. const para=(title,text)=>({kind:'para',title,text});
  42. export async function acceptRetest(){
  43. const draft=read(path.join(work,'independent-run/case-draft.json'));
  44. const fixture=read(path.join(work,'inputs/raw-materials.json'));
  45. const modules=draft.modules,checks=[],outputs=[],cardLinks=[],cardOutputs=[],supplementalOutputs=[],events=[];
  46. const expected=['D1-01','D1-02','D1-03','D1-04','D1-N','D2-01','D2-02','D2-03','D2-04','D2-05','D2-06','D2-07','D2-08'];
  47. assert.deepEqual(modules.map(m=>m.id),expected);
  48. const stages=['S1','S1','S1','S1','S2','S2','S2','S3','S3','S4','S4','S5','S5'];
  49. assert.deepEqual(modules.map(m=>m.stage),stages);
  50. const verify=(label,value)=>{assert.ok(value,label);checks.push(label);};
  51. const sourceRecords=[...fixture.products.map(p=>({...p,text:p.title,platform:'product-fixture'})),...fixture.reviews,...fixture.social];
  52. const byId=Object.fromEntries(sourceRecords.map(r=>[r.id,r]));
  53. modules.forEach(preflight);verify('13环节顺序、阶段与完整内容结构有效',true);
  54. const temp=fs.mkdtempSync(path.join(os.tmpdir(),'ksf-retest-case-'));
  55. const opts={profileDir:path.join(temp,'agents'),participantDir:path.join(temp,'people')};
  56. const agent=readAgentProfileByKey(registerAgentProfile({name:'本地独立案例执行者',role:'workshop',identityBasis:'operator_registration',runtimePlatform:'local-test',channel:'local-test',account:{platform:'local-test',accountId:'retest-agent'}},opts).profile.key,opts.profileDir);
  57. const person=readParticipantProfile(registerParticipantProfile({name:'本地案例学员',identityBasis:'participant_registration',account:{platform:'local-test'}},opts).profile.key,opts.participantDir);
  58. const store=memory(),app=createApplication({store,crypto,Parse:{},data,settings:{signingSecret:'local-retest-only'},files:{}});attachWorkshopHandlers(app,{store,data});
  59. const actor={id:'local-retest-only-service',kind:'service',scopes:['ingest:write','data:read','source:write'],projectIds:['*']};
  60. let seq=0,networkAttempts=0;
  61. const run=async(command,input,requestId='retest:'+String(++seq))=>{events.push({command,requestId});return runWorkshop(command,structuredClone(input),{requestId,agentProfile:agent,participantProfile:person,call:async(n,p)=>(await app.dispatch(n,structuredClone(p),actor)).data});};
  62. const oldFetch=globalThis.fetch;globalThis.fetch=()=>{networkAttempts++;throw Error('RETEST_NETWORK_FORBIDDEN');};
  63. try{
  64. const raw=JSON.stringify(fixture,null,2);
  65. const imported=await run('workshop-import',{source:{title:'独立复测构造题干;非真实研究资料',sourceKind:'dataset',dataMode:'sample',rawText:raw},records:Object.values(byId).map(r=>({itemKey:r.id,className:'KfEvidence',externalKey:'retest:input:'+r.id,data:{dataMode:'sample',quote:r.text,evidenceKind:'synthetic_test_only',quoteForm:'verbatim_excerpt',verificationState:'synthetic_only',platform:r.platform,locator:{fixture:'raw-materials.json',recordId:r.id}}}))});
  66. verify('6条商品及17条原始文字逐条桥接,保留重复/占位原件',imported.items.length===23);
  67. for(const item of imported.items){const r=await run('workshop-read',{className:item.className,objectId:item.objectId});verify('逐字回读 '+item.itemKey,r.record.quote===byId[item.itemKey].text);}
  68. const session=await run('workshop-start',{workshopKey:'independent-retest-r2',participant:{profileKey:person.key},conversation:{key:'retest-conversation',channel:'local-test'},dataMode:'sample'});
  69. const inputFor=m=>{
  70. const used=m.usedRecordIds||[];
  71. for(const id of used)assert.ok(byId[id],m.id+' unknown source '+id);
  72. const evidence=Object.fromEntries([...new Set(used)].map(id=>['ev_'+id,{sourceId:imported.sourceId,claim:'构造题干 '+id+';不能当真实消费者证据',quotes:[{text:byId[id].text,ref:id}]}]));
  73. return {sessionId:session.sessionId,taskKey:'retest:'+m.id,version:1,stageCode:m.stage,title:'本地独立复测/'+m.id+'/'+m.title,artifactType:m.type,category:'拌面',sourceIds:[imported.sourceId],pendingQuestions:(draft.unresolved||[]).map(x=>typeof x==='string'?x:JSON.stringify(x)),rawText:[draft.notice,m.finding,m.meaning,...m.sections.map(sectionText),m.next].join('\n\n'),report:{executive:[m.finding,m.meaning,m.next],chapters:[{id:m.id.toLowerCase(),title:m.title,stage:m.stage,sections:m.sections}],evidence}};
  74. };
  75. let firstInput;
  76. for(const original of modules){
  77. const m=structuredClone(original);
  78. if(m.id==='D2-04'){
  79. assert.ok(draft.candidates.length>=2,'case has alternatives');
  80. for(const c of draft.candidates){
  81. const sections=c.reportSections||[table('候选详情',['字段','内容'],Object.entries(c).filter(([k])=>k!=='reportSections').map(([k,v])=>[k,typeof v==='string'?v:JSON.stringify(v)]))];
  82. const one={id:'card-'+c.key,stage:'S3',type:'opportunity_card',title:c.title,finding:c.benefitHypothesis||c.title,meaning:c.task||c.title,reply:c.title,next:typeof c.decisiveTest==='string'?c.decisiveTest:JSON.stringify(c.decisiveTest),sections,usedRecordIds:[...new Set([...(c.supportRecordIds||[]),...(c.counterRecordIds||[])])]};
  83. preflight(one);const p=inputFor(one);p.opportunity={key:'retest:'+c.key,title:c.title,demandSummary:one.finding,targetAudience:'依题干自述,规模和画像待核',usageScenario:one.meaning,fusionDefinition:'依本候选独立正文'};
  84. const saved=await run('workshop-save',p),a=await run('workshop-read',{className:'KfArtifact',objectId:saved.artifactId}),o=await run('workshop-read',{className:'KfOpportunity',objectId:saved.opportunityId});
  85. assert.deepEqual(a.record.report.chapters,p.report.chapters);verify(c.key+' 独立卡正文与关联精确回读',o.record.linkedArtifacts.includes(saved.artifactId));
  86. cardLinks.push({key:c.key,title:c.title,artifactId:saved.artifactId,opportunityId:saved.opportunityId,version:1});
  87. cardOutputs.push({...one,artifactId:saved.artifactId,automaticSourceId:saved.sourceId,report:a.record.report,rawText:a.record.rawText});
  88. }
  89. }
  90. if(m.supplementalReports?.length){
  91. const linked=[];
  92. for(const [index,sup] of m.supplementalReports.entries()){
  93. const first=sup.sections.find(s=>s.kind==='para'||s.kind==='longpara');
  94. const one={...sup,id:m.id+'-supp-'+String(index+1),finding:first?.text||sup.title,meaning:'本节独立核心产出;完整正文见各章节',reply:'本地研究草稿的独立配套报告,后续状态按实际回读核实。',next:m.next,usedRecordIds:sup.usedRecordIds||m.usedRecordIds};
  95. preflight(one);const p=inputFor(one),saved=await run('workshop-save',p),r=await run('workshop-read',{className:'KfArtifact',objectId:saved.artifactId});
  96. assert.deepEqual(r.record.report.chapters,p.report.chapters);assert.equal(r.record.rawText,p.rawText);
  97. verify(m.id+' 配套核心报告 '+String(index+1)+' 全文保存回读',r.record.artifactType===sup.type&&r.record.dataMode==='sample');
  98. const out={...one,moduleId:m.id,artifactId:saved.artifactId,automaticSourceId:saved.sourceId,report:r.record.report,rawText:r.record.rawText};
  99. linked.push(out);supplementalOutputs.push(out);
  100. }
  101. m.supplementalReports=linked;
  102. m.sections.push(table('独立配套成果实际本地编号',['成果','类型','报告编号','版本'],linked.map(s=>[s.title,s.type,s.artifactId,'1'])));
  103. }
  104. if(['D2-04','D2-05','D2-06','D2-08'].includes(m.id))m.sections.push(table('候选实际本地编号',['稳定键','机会编号','报告编号','版本'],cardLinks.map(c=>[c.key,c.opportunityId,c.artifactId,c.version])));
  105. if(m.id==='D2-08')m.sections.push(table('已保存环节导航',['环节','报告编号','版本'],outputs.map(o=>[o.id,o.artifactId,'1'])));
  106. const p=inputFor(m);firstInput??=structuredClone(p);
  107. const saved=await run('workshop-save',p,'retest:save:'+m.id),r=await run('workshop-read',{className:'KfArtifact',objectId:saved.artifactId});
  108. assert.deepEqual(r.record.report.chapters,p.report.chapters);assert.equal(r.record.rawText,p.rawText);
  109. verify(m.id+' 全部正文和表格精确回读',r.record.stageCode===m.stage&&r.record.version===1);
  110. verify(m.id+' 样例/未确认状态保留',r.record.dataMode==='sample'&&!saved.formalReportPublished&&!saved.employeeConfirmed);
  111. outputs.push({...m,artifactId:saved.artifactId,automaticSourceId:saved.sourceId,report:r.record.report,rawText:r.record.rawText});
  112. }
  113. const repeated=await run('workshop-save',firstInput,'retest:save:D1-01');verify('重放相同请求不重复建报告',repeated.artifactId===outputs[0].artifactId);
  114. const board=(await app.dispatch('ksfGetPublicBoard',{limit:500},null)).data;
  115. verify('每份报告、配套成果与候选在本地公开投影可见',[...outputs,...supplementalOutputs].every(o=>board.artifact.some(a=>a.objectId===o.artifactId))&&cardLinks.every(c=>board.opportunity.some(o=>o.objectId===c.opportunityId)));
  116. verify('单节多种核心成果分别保存且有真实编号导航',supplementalOutputs.length===3&&new Set([...outputs,...supplementalOutputs].map(o=>o.artifactId)).size===16);
  117. const unique=[...new Map(fixture.reviews.map(r=>[r.upstreamId,r])).values()];
  118. verify('原材料独立复算:15电商原件→14唯一→13非占位→12可讨论食用体验',fixture.reviews.length===15&&unique.length===14&&unique.filter(r=>r.id!=='E11').length===13&&unique.filter(r=>!['E10','E11'].includes(r.id)).length===12);
  119. const cond=fixture.scenarioConditions;
  120. const allowed=cond.netRevenuePerServing*(1-cond.minimumGrossMargin);
  121. verify('成本边界独立复算2.73元;新增成本上限0.23元',Math.abs(allowed-2.73)<1e-12&&Math.abs(allowed-cond.baseUnitCost-.23)<1e-12);
  122. verify('无外部网络调用',networkAttempts===0);
  123. const result={schema:'ksf.independent-case-local-acceptance.v1',checkedAt:new Date().toISOString(),ok:true,checksPassed:checks.length,checks,researchModules:13,candidates:cardLinks.length,supplementalReports:supplementalOutputs.length,independentAgentRun:true,caseType:'synthetic scenario, current Skill applied by separate context agent',parentEditedCaseOutput:false,localMemoryWrites:store.writes,productionDatabaseWrites:0,networkCalls:networkAttempts,paidCalls:0,candidateDeployed:false,formalReportSubmitted:false};
  124. fs.writeFileSync(path.join(work,'interface-acceptance.json'),JSON.stringify(result,null,2)+'\n');
  125. fs.writeFileSync(path.join(work,'sample-outputs.json'),JSON.stringify({notice:draft.notice,fixture,outputs,supplementalOutputs,cardOutputs,candidates:draft.candidates,cardLinks,finalRecommendation:draft.finalRecommendation,unresolved:draft.unresolved},null,2)+'\n');
  126. fs.writeFileSync(path.join(work,'local-board.json'),JSON.stringify(board,null,2)+'\n');
  127. fs.writeFileSync(path.join(work,'interface-events.json'),JSON.stringify(events,null,2)+'\n');
  128. return result;
  129. }finally{
  130. globalThis.fetch=oldFetch;
  131. const absolute=fs.realpathSync(temp);assert.ok(absolute.startsWith(fs.realpathSync(os.tmpdir())+path.sep)&&path.basename(absolute).startsWith('ksf-retest-case-'));fs.rmSync(absolute,{recursive:true});
  132. }
  133. }
  134. if(process.argv[1]&&path.resolve(process.argv[1])===fileURLToPath(import.meta.url)){const r=await acceptRetest();console.log(JSON.stringify({ok:r.ok,modules:r.researchModules,candidates:r.candidates,checks:r.checksPassed,productionWrites:0}));}