verify-consumer-quotes-readback.mjs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  1. // Read-only production verification. Uses ksfGetRecord and Parse GETs; no import/save calls.
  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. const taskKey='KSF-消费者原话-20261102-01';
  7. const directory='work/consumer-quote-inputs-20261102/';
  8. const originals='work/consumer-research-codex-20261102/delivery/';
  9. const archiveFile='work/consumer-quotes-codex-20261102.zip';
  10. const output='work/consumer-quotes-readback-20261102-complete.json';
  11. const imports=[
  12. {key:'cq-01',sourceId:'2mzBadmvws',evidenceId:'FO47R0yR8D',jobId:'Li-h4TsoS2'},
  13. {key:'cq-02',sourceId:'Pi04eTwTQ3',evidenceId:'rD8XZgV1Xo',jobId:'KGKMHZl8Mm'},
  14. {key:'cq-03',sourceId:'m1OQx9aynA',evidenceId:'_9mUml0wmh',jobId:'plOFt3HvXJ'},
  15. {key:'cq-04',sourceId:'QdeMcyTTjX',evidenceId:'2bFGP7axMo',jobId:'MVmsZHRZbj'},
  16. ];
  17. const sha=value=>crypto.createHash('sha256').update(value).digest('hex');
  18. const pointer=(className,objectId)=>({__type:'Pointer',className,objectId});
  19. // Parse REST serializes Date fields as envelopes; ksfGetRecord returns ISO strings.
  20. // Compare the same date value while retaining pointers and every other field.
  21. const normalizeDates=value=>{
  22. if(Array.isArray(value))return value.map(normalizeDates);
  23. if(value&&typeof value==='object'){
  24. if(value.__type==='Date'&&typeof value.iso==='string')return value.iso;
  25. return Object.fromEntries(Object.entries(value).map(([key,item])=>[key,normalizeDates(item)]));
  26. }
  27. return value;
  28. };
  29. const read=async(className,objectId)=>{
  30. const response=await invoke('ksfGetRecord',{className,objectId});
  31. assert.equal(response.http,200);assert.equal(response.body?.result?.ok,true);
  32. const row=response.body.result.data.record;assert.ok(row);assert.equal(row.objectId,objectId);
  33. return row;
  34. };
  35. let check='local fixed inputs',cq04Verified=false;
  36. const reports=[];
  37. assert.equal(fs.existsSync(output),false,'Retain existing verification');
  38. try{
  39. const receipt=JSON.parse(fs.readFileSync('work/consumer-quote-import-package-receipt-20261102.json','utf8'));
  40. const archive=fs.readFileSync(archiveFile);assert.equal(archive.length,1287263);assert.equal(sha(archive),receipt.originalArchiveSha256);
  41. const tasks=new Map(imports.map(ids=>{
  42. const bytes=fs.readFileSync(directory+ids.key+'.input.json');assert.equal(sha(bytes),receipt.inputHashes[ids.key+'.input.json']);
  43. return[ids.key,JSON.parse(bytes)];
  44. }));
  45. check='cq-04 precise evidence read';
  46. const cq04=await read('KfEvidence',imports[3].evidenceId),last=tasks.get('cq-04');
  47. for(const[field,value]of Object.entries(last.records[0].data))assert.deepEqual(cq04[field]??null,value,'cq-04 evidence '+field);
  48. assert.deepEqual(cq04.sourceRef,pointer('KfSource',imports[3].sourceId));
  49. assert.equal(cq04.productRef??null,null);assert.equal(cq04.recordState,'active');
  50. cq04Verified=true;
  51. console.log(JSON.stringify({step:'cq-04 precise read',ok:true,evidenceId:cq04.objectId,allFixedFieldsMatched:true}));
  52. check='read source, evidence, job and item groups';
  53. const sourcePointers=imports.map(x=>pointer('KfSource',x.sourceId));
  54. const jobPointers=imports.map(x=>pointer('KfImportJob',x.jobId));
  55. const results=await Promise.allSettled([
  56. classRows('KfSource',{sourceUrl:{$in:[...tasks.values()].map(x=>x.source.sourceUrl)}}),
  57. classRows('KfEvidence',{sourceRef:{$in:sourcePointers}}),
  58. classRows('KfImportJob',{inputSourceRef:{$in:sourcePointers}}),
  59. classRows('KfImportItem',{jobRef:{$in:jobPointers}}),
  60. classRows('KfProduct',{sourceRef:{$in:sourcePointers}}),
  61. classRows('KfArtifact',{$or:[{sourceRef:{$in:sourcePointers}},{workshopKey:'KSF-真实资料-20261102-01',stageCode:'S2'}]}),
  62. read('KfArtifact','G8IrkkDG7X'),
  63. ]);
  64. const failures=results.map((r,index)=>r.status==='rejected'?index:null).filter(x=>x!==null);
  65. assert.deepEqual(failures,[],'Read group failed; indexes only, no raw exception or credentials');
  66. const [sources,evidences,jobs,items,products,newArtifacts,s1]=results.map(r=>normalizeDates(r.value));
  67. assert.equal(sources.length,4);assert.equal(evidences.length,4);assert.equal(jobs.length,4);assert.equal(items.length,4);
  68. assert.equal(products.length,0);assert.equal(newArtifacts.length,0);
  69. for(const ids of imports){
  70. check=ids.key+' fixed fields, reference, history and archive';
  71. const input=tasks.get(ids.key),row=input.records[0];
  72. const source=sources.find(x=>x.objectId===ids.sourceId),evidence=evidences.find(x=>x.objectId===ids.evidenceId);
  73. const job=jobs.find(x=>x.objectId===ids.jobId),item=items.find(x=>x.jobRef?.objectId===ids.jobId);
  74. assert.ok(source);assert.ok(evidence);assert.ok(job);assert.ok(item);
  75. for(const[field,value]of Object.entries(input.source))assert.deepEqual(source[field]??null,value,ids.key+' source '+field);
  76. for(const[field,value]of Object.entries(row.data))assert.deepEqual(evidence[field]??null,value,ids.key+' evidence '+field);
  77. assert.equal(source.contentHash,sha(input.source.rawText));assert.equal(source.recordState,'active');assert.equal(evidence.recordState,'active');
  78. assert.equal(source.meta.requestId,taskKey+':'+ids.key+':import:v1:source');
  79. assert.deepEqual(evidence.sourceRef,pointer('KfSource',ids.sourceId));
  80. assert.equal(evidence.productRef??null,null);assert.equal(evidence.artifactRef??null,null);assert.equal(evidence.publishedAt??null,null);
  81. assert.equal(evidence.locator.publicationTimezone,'unknown');assert.equal(evidence.locator.archiveSha256,sha(archive));
  82. assert.equal([...source.rawText].slice(evidence.locator.startCharacter,evidence.locator.endCharacter).join(''),evidence.quote);
  83. for(const field of ['textOrigin','responseOriginal','screenshot','articleDom']){
  84. const file=evidence.locator[field];assert.equal(sha(fs.readFileSync(originals+file.file)),file.sha256,ids.key+' '+field);
  85. }
  86. assert.equal(job.status,'succeeded');assert.equal(job.expectedCount,1);assert.equal(job.successCount,1);assert.equal(job.failureCount,0);
  87. assert.deepEqual(job.inputSourceRef,pointer('KfSource',ids.sourceId));
  88. assert.equal(job.producer.agent.name,'王刚001');assert.equal(job.producer.agent.key,s1.meta.attribution.agentKey);
  89. assert.equal(item.targetClass,'KfEvidence');assert.equal(item.targetId,ids.evidenceId);
  90. assert.equal(item.itemKey,row.itemKey);assert.equal(item.businessKey,row.externalKey);assert.equal(item.status,'succeeded');assert.equal(item.operation,'created');
  91. assert.equal(sources.filter(x=>x.sourceUrl===source.sourceUrl).length,1);
  92. assert.equal(evidences.filter(x=>x.sourceRef?.objectId===ids.sourceId).length,1);
  93. assert.equal(jobs.filter(x=>x.inputSourceRef?.objectId===ids.sourceId).length,1);
  94. reports.push({...ids,sourceOperation:'created',created:1,unchanged:0,sourceMatchesFixedInput:true,evidenceMatchesFixedInput:true,
  95. quoteAndUnicodeLocatorMatched:true,originalArchiveAndFilesVerified:true,actualCollectionTimePreserved:true,
  96. publicationTimeAndTimezonePreserved:true,productRef:null,importJobSucceeded:true,sameAgent:true,duplicateSourceCount:1});
  97. }
  98. check='original S1 preserved';
  99. const template=JSON.parse(fs.readFileSync('work/first-live-S1-v2-template.json','utf8'));
  100. for(const field of ['title','rawText','version','stageCode'])assert.deepEqual(s1[field],template[field]);
  101. assert.equal(s1.dataMode,'live');assert.equal(s1.dashboardState,'workshop');assert.equal(s1.ownerSession,'btrFqkz0yw');
  102. assert.deepEqual(s1.report.chapters,template.report.chapters);assert.deepEqual(s1.report.evidence,template.report.evidence);
  103. assert.deepEqual(s1.meta.workshop.sourceIds,template.sourceIds);
  104. const result={ok:true,operation:'consumer-quotes-independently-verified',at:new Date().toISOString(),taskKey,readOnly:true,businessWrites:0,
  105. dataMode:'live',sources:4,evidence:4,imports:reports,cq04EvidenceIndependentlyVerified:true,
  106. originalReadbackErrorRetained:true,originalReadbackRequestResent:false,originalReadbackFailureCauseVerified:false,
  107. newProductsFromTheseSources:0,s2Saved:false,originalS1Preserved:true,originalS1ArtifactId:'G8IrkkDG7X',
  108. archive:{sha256:sha(archive),bytes:archive.length,localOriginalsVerified:true,remoteLocationBasis:'operations receipt'},
  109. dateComparison:'Parse Date envelope normalized to ISO value; first verification attempt retained separately.',
  110. purchaseVerified:false,realIdentityVerified:false,marketDemandValidated:false,formalReportPublished:false};
  111. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.log(JSON.stringify(result,null,2));
  112. }catch(error){
  113. const result={ok:false,operation:'consumer-quotes-independent-readback-stopped',check,cq04EvidenceIndependentlyVerified:cq04Verified,
  114. importsVerified:reports.length,errorCode:'CONSUMER_QUOTES_READBACK_FAILED',checkLine:error.stack?.match(/verify-consumer-quotes-readback\.mjs:(\d+):/)?.[1]??null,
  115. readOnly:true,businessWrites:0};
  116. fs.writeFileSync(output,JSON.stringify(result,null,2)+'\n',{flag:'wx'});console.error(JSON.stringify(result,null,2));process.exitCode=1;
  117. }