verify-S4-public-readback.mjs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. // Independently verify archived public material and existing import history. Read only.
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import crypto from 'node:crypto';
  5. import assert from 'node:assert/strict';
  6. import {invoke,classRows} from '../server/scripts/platform-client.mjs';
  7. const output='work/S4-public-readback-20261103.json';
  8. assert.equal(fs.existsSync(output),false,'Preserve prior acceptance');
  9. const directory=path.resolve('work/S4-public-materials-20261103');
  10. const zip='F:/文档/工作坊当前资料/ksf-S4-public-materials-20261103.zip';
  11. const taskKey='KSF-S4公开资料-20261103-01';
  12. const sessionId='btrFqkz0yw',workshopKey='KSF-真实资料-20261102-01';
  13. const archiveSha='88776415ab6e80e9a5b1220bf5836f5022d4c0a7814cc251128e052b0cb9009f';
  14. const sha=value=>crypto.createHash('sha256').update(value).digest('hex');
  15. const ptr=(className,objectId)=>({__type:'Pointer',className,objectId});
  16. const normalize=value=>Array.isArray(value)?value.map(normalize):value&&typeof value==='object'?
  17. value.__type==='Date'?value.iso:Object.fromEntries(Object.entries(value).map(([k,v])=>[k,normalize(v)])):value;
  18. const read=async(className,objectId)=>{
  19. const r=await invoke('ksfGetRecord',{className,objectId});
  20. assert.equal(r.http,200);assert.equal(r.body?.result?.ok,true);
  21. const row=r.body.result.data.record;assert.equal(row?.objectId,objectId);return row;
  22. };
  23. const imports=[
  24. {key:'p1',sourceId:'PjORcvaRHn',evidenceId:'xHGV_mRWcG',name:'suning-photo-117',lines:[5,31],length:188,
  25. start:122,end:136,quote:'净含量\n585克\n包装\n袋装',sourceKind:'ecommerce',platform:'suning',
  26. url:'https://m.suning.com/bigimages/12447641634.html',externalKey:taskKey+':p1:listing:v1',itemKey:'s4-p1-listing',
  27. evidenceKind:'retailer_listing_observation',parentObjectId:'12447641634',authorLabel:'苏宁易购商品页(平台标示,无个人作者)',
  28. rawSha:'26f9865d7061009a002f2fbb8d5c9bd21f18cc5097444781b0de3ddd08470a5c',publishedAtReported:null},
  29. {key:'p2',sourceId:'5S_Y4IgT5q',evidenceId:'CeAEIwymTa',name:'post-label-review',lines:[12,49],length:766,
  30. start:327,end:356,quote:'包装上写的煮面6-7分钟,实测写少了,煮了10分钟面才软。',sourceKind:'social',platform:'smzdm',
  31. url:'https://post.smzdm.com/p/aeqxm74k/',externalKey:taskKey+':p2:experience:v1',itemKey:'s4-p2-experience',
  32. evidenceKind:'public_user_experience_self_report',parentObjectId:'aeqxm74k',authorLabel:'没事儿聊聊呗',
  33. rawSha:'2a5e1057b91f313ea6fb9ee747f6fcc2f7b11dca55342672950cb51e4a6ada62',publishedAtReported:'2023-12-18 21:20:15'}
  34. ];
  35. let step='original package and four exact reads';
  36. const checks=[],reports=[];
  37. const check=(name,fn)=>{fn();checks.push({name,passed:true});};
  38. try{
  39. const archive=fs.readFileSync(zip);assert.equal(archive.length,6100444);assert.equal(sha(archive),archiveSha);
  40. const inspection=JSON.parse(fs.readFileSync('work/S4-public-record-inspection-20261103.json','utf8'));
  41. assert.equal(inspection.ok,true);assert.equal(inspection.records.length,4);
  42. const index=JSON.parse(fs.readFileSync(path.join(directory,'SOURCE-INDEX.json'),'utf8'));
  43. for(const ids of imports){
  44. step=ids.key+' source and evidence';
  45. const source=inspection.records.find(x=>x.className==='KfSource'&&x.objectId===ids.sourceId).record;
  46. const evidence=inspection.records.find(x=>x.className==='KfEvidence'&&x.objectId===ids.evidenceId).record;
  47. const origin=index.sources.find(x=>x.id===ids.name);
  48. const raw=fs.readFileSync(path.join(directory,'browser',ids.name+'.text.txt'),'utf8').split(/\r?\n/).slice(ids.lines[0]-1,ids.lines[1]).join('\n');
  49. check(ids.key+' source body and archive extraction',()=>{
  50. assert.equal(source.rawText,raw);assert.equal([...raw].length,ids.length);assert.equal(sha(raw),ids.rawSha);assert.equal(source.contentHash,ids.rawSha);
  51. assert.equal(source.sourceUrl,ids.url);assert.equal(source.title,origin.title);assert.equal(source.sourceKind,ids.sourceKind);
  52. assert.equal(source.platform,ids.platform);assert.equal(source.collectedAt,origin.observedAt);
  53. });
  54. check(ids.key+' literal evidence fields and source pointer',()=>{
  55. for(const row of [source,evidence]){
  56. assert.equal(row.dataMode,'live');assert.equal(row.reviewState,'pending');assert.equal(row.recordState,'active');assert.equal(row.schemaVersion,'ksf.v0.7');
  57. }
  58. assert.equal(evidence.externalKey,ids.externalKey);assert.equal(evidence.platform,ids.platform);assert.equal(evidence.quote,ids.quote);
  59. assert.equal(evidence.evidenceKind,ids.evidenceKind);assert.equal(evidence.quoteForm,'verbatim_excerpt');
  60. assert.equal(evidence.verificationState,'archived_text_and_screenshot_checked');
  61. assert.equal(evidence.authorLabel,ids.authorLabel);assert.equal(evidence.parentObjectId,ids.parentObjectId);
  62. assert.equal(evidence.collectedAt,origin.observedAt);assert.equal(evidence.publishedAt??null,null);
  63. assert.equal(evidence.productRef??null,null);assert.equal(evidence.artifactRef??null,null);
  64. assert.deepEqual(evidence.sourceRef,ptr('KfSource',ids.sourceId));
  65. });
  66. const locator=evidence.locator;
  67. check(ids.key+' locator and date distinctions',()=>{
  68. assert.equal(locator.url,ids.url);assert.equal(locator.startCharacter,ids.start);assert.equal(locator.endCharacter,ids.end);
  69. assert.equal(locator.unit,'Unicode code point');assert.equal(locator.startBase,0);assert.equal(locator.endExclusive,true);
  70. assert.equal([...raw].slice(ids.start,ids.end).join(''),ids.quote);assert.equal(locator.archiveSha256,archiveSha);
  71. assert.equal(locator.archiveFile,path.basename(zip));assert.equal(locator.authenticated,false);assert.equal(locator.purchaseVerified,false);
  72. assert.equal(locator.browserReadAt,origin.observedAt);assert.equal(locator.httpCollectedAt,origin.startedAt);
  73. assert.equal(locator.publishedAtReported,ids.publishedAtReported);assert.equal(locator.publicationTimezone,'unknown');
  74. assert.equal(locator.collectionPerformedBy,'Codex');
  75. });
  76. const fileResults=[];
  77. for(const file of [locator.textOrigin,locator.responseOriginal,locator.articleDom,locator.screenshot,...locator.imageReadings]){
  78. const target=path.resolve(directory,file.file);
  79. assert.ok(target.startsWith(directory+path.sep),'Archive paths stay inside packet');
  80. assert.equal(sha(fs.readFileSync(target)),file.sha256,file.file);
  81. fileResults.push({file:file.file,sha256:file.sha256,verified:true});
  82. }
  83. checks.push({name:ids.key+' five locator-linked original file hashes',passed:true});
  84. for(const image of locator.imageReadings){
  85. const original=index.images.find(x=>x.file===image.file);assert.ok(original);assert.equal(original.sha256,image.sha256);
  86. }
  87. reports.push({key:ids.key,sourceId:ids.sourceId,evidenceId:ids.evidenceId,externalKey:ids.externalKey,
  88. sourceBodyMatchesArchivedExtraction:true,sourceTextSha256:sha(raw),quoteSliceVerified:true,
  89. evidenceKind:evidence.evidenceKind,verificationState:evidence.verificationState,quoteForm:evidence.quoteForm,
  90. collectedAt:evidence.collectedAt,publishedAt:null,publishedAtReported:locator.publishedAtReported,publicationTimezone:'unknown',
  91. authorLabel:evidence.authorLabel,parentObjectId:evidence.parentObjectId,sourceRefMatched:true,productRef:null,
  92. locatorOriginalFiles:fileResults,inputFileByteHashBasis:'Remote operator receipt; complete input JSON not obtained by Codex'});
  93. }
  94. step='current session, import jobs/items, duplicate and stage history';
  95. const sources=imports.map(x=>ptr('KfSource',x.sourceId));
  96. const settled=await Promise.allSettled([
  97. read('KfSession',sessionId),
  98. classRows('KfSource',{sourceUrl:{$in:imports.map(x=>x.url)}}),
  99. classRows('KfEvidence',{$or:[{sourceRef:{$in:sources}},{externalKey:{$in:imports.map(x=>x.externalKey)}}]}),
  100. classRows('KfImportJob',{inputSourceRef:{$in:sources}}),
  101. classRows('KfImportItem',{businessKey:{$in:imports.map(x=>x.externalKey)}}),
  102. classRows('KfProduct',{sourceRef:{$in:sources}}),
  103. classRows('KfArtifact',{workshopKey}),
  104. classRows('KfOpportunity',{sessionRef:ptr('KfSession',sessionId)}),
  105. classRows('KfProject',{sessionRef:ptr('KfSession',sessionId)}),
  106. ]);
  107. const failures=settled.flatMap((r,i)=>r.status==='rejected'?[i]:[]);assert.deepEqual(failures,[],'Read group failures (indexes only)');
  108. const [session,sourceHistory,evidenceHistory,jobs,items,products,artifacts,opportunities,projects]=settled.map(r=>normalize(r.value));
  109. check('same live S3 session and no extra business objects',()=>{
  110. assert.equal(session.dataMode,'live');assert.equal(session.stageCode,'S3');assert.equal(session.workshopKey,workshopKey);
  111. assert.equal(products.length,0);assert.equal(opportunities.length,0);assert.equal(projects.length,0);
  112. assert.equal(artifacts.filter(x=>x.stageCode==='S4').length,0);assert.equal(artifacts.length,5);
  113. });
  114. for(const ids of imports){
  115. step=ids.key+' unique import history';
  116. check(ids.key+' one source/evidence/job/item and created operation',()=>{
  117. assert.deepEqual(sourceHistory.filter(x=>x.sourceUrl===ids.url).map(x=>x.objectId),[ids.sourceId]);
  118. assert.deepEqual(evidenceHistory.filter(x=>x.sourceRef?.objectId===ids.sourceId).map(x=>x.objectId),[ids.evidenceId]);
  119. const group=jobs.filter(x=>x.inputSourceRef?.objectId===ids.sourceId);assert.equal(group.length,1);
  120. const job=group[0],related=items.filter(x=>x.jobRef?.objectId===job.objectId);assert.equal(related.length,1);
  121. assert.equal(job.status,'succeeded');assert.equal(job.expectedCount,1);assert.equal(job.successCount,1);assert.equal(job.failureCount,0);
  122. assert.equal(job.producer.agent.key,session.meta.attribution.agentKey);assert.equal(job.producer.agent.name,'王刚001');
  123. assert.equal(related[0].targetClass,'KfEvidence');assert.equal(related[0].targetId,ids.evidenceId);
  124. assert.equal(related[0].itemKey,ids.itemKey);assert.equal(related[0].businessKey,ids.externalKey);assert.equal(related[0].operation,'created');
  125. assert.equal(related[0].status,'succeeded');assert.equal(related[0].attempts,1);
  126. Object.assign(reports.find(x=>x.key===ids.key),{jobId:job.objectId,itemId:related[0].objectId,created:1,unchanged:0,importJobSucceeded:true,recordOperation:'created',sameAgent:true,sourceCount:1,evidenceCount:1});
  127. });
  128. }
  129. step='old reports unchanged';
  130. const oldReports=[['G8IrkkDG7X','work/first-live-S1-v2-template.json'],['b7NOby5ys0','work/first-live-S2-template.json'],
  131. ['oiNxX7o27j','work/first-live-S3-template.json'],['ZW1FB5zeHr','work/first-live-S3-v2-template.json']];
  132. for(const [id,file]of oldReports){
  133. const row=artifacts.find(x=>x.objectId===id);assert.ok(row);const template=JSON.parse(fs.readFileSync(file,'utf8'));
  134. check(id+' template fields preserved',()=>{
  135. for(const field of ['title','rawText','version','stageCode'])assert.deepEqual(row[field],template[field]);
  136. assert.deepEqual(row.report.chapters,template.report.chapters);assert.deepEqual(row.report.evidence,template.report.evidence);
  137. assert.deepEqual(row.meta.workshop.sourceIds,template.sourceIds);assert.deepEqual(row.meta.workshop.pendingQuestions,template.pendingQuestions);
  138. assert.equal(row.ownerSession,sessionId);assert.equal(row.dataMode,'live');assert.equal(row.dashboardState,'workshop');
  139. assert.equal(row.dashboardPublishedAt??null,null);assert.equal(row.contentConfirmedAt??null,null);
  140. });
  141. }
  142. const result={ok:true,operation:'S4-public-import-independently-verified',at:new Date().toISOString(),readOnly:true,businessWrites:0,
  143. taskKey,sessionId,systemStage:'S3',checksPassed:checks.length,checks,imports:reports,
  144. archive:{localZip:zip,bytes:6100444,sha256:archiveSha,locatorLinkedFileHashesVerified:10,remoteArchiveLocationBasis:'Operations receipt'},
  145. distinctImportJobs:jobs.length,distinctImportItems:items.length,productsFromNewSources:0,opportunities:0,projects:0,s4Artifacts:0,
  146. oldReportTemplatesPreserved:true,purchaseVerified:false,physicalSampleVerified:false,marketDemandValidated:false,
  147. remoteInputFilesIndependentlyObtained:false,remoteReceiptsAndFilePermissionsBasis:'Operator report, not independent filesystem inspection',
  148. timingNote:'locator.httpCollectedAt equals browser navigation startedAt in the supplied source index; browserReadAt/collectedAt equals observedAt. Do not interpret it as a separate successful HTTP response timestamp.',
  149. publicationNote:'publishedAt null preserves unknown timezone; P2 locator.publishedAtReported preserves the displayed historical timestamp.'};
  150. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});
  151. console.log(JSON.stringify({ok:true,checksPassed:checks.length,imports:reports.map(({key,sourceId,evidenceId,jobId,evidenceKind,verificationState,quoteForm,publishedAtReported})=>({key,sourceId,evidenceId,jobId,evidenceKind,verificationState,quoteForm,publishedAtReported})),systemStage:'S3',opportunities:0,projects:0,s4Artifacts:0,locatorLinkedFileHashesVerified:10,oldReportTemplatesPreserved:true},null,2));
  152. }catch(error){
  153. const result={ok:false,operation:'S4-public-import-independent-verification-stopped',at:new Date().toISOString(),step,
  154. checkLine:error.stack?.match(/verify-S4-public-readback\.mjs:(\d+):/)?.[1]??null,checksPassed:checks.length,
  155. errorCode:'S4_PUBLIC_VERIFICATION_FAILED',readOnly:true,businessWrites:0};
  156. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result,null,2));process.exitCode=1;
  157. }