verify-library-cleanup.mjs 5.8 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  1. /** Independent PostgreSQL, authenticated query and public-board readback. */
  2. import assert from 'node:assert/strict';
  3. import {isDeepStrictEqual} from 'node:util';
  4. import {load,save,read,publicApi,unwrap} from './lib/research-enrichment.mjs';
  5. const work=process.env.KSF_CLEANUP_WORK||'work/library-cleanup-20261111';
  6. const input=load(work+'/sealed-plan.json'),plan=input.plan;
  7. const applied=unwrap(load(work+'/apply-result.json'));
  8. const planned=unwrap(load(work+'/plan-result.json'));
  9. const status=unwrap(load(work+'/status-result.json'));
  10. const before=unwrap(load(work+'/inventory-result.json'));
  11. const after=unwrap(load(work+'/inventory-after-result.json'));
  12. assert(applied.ok&&applied.applied&&applied.allRowsVerified);
  13. assert.equal(applied.planSha256,input.planSha256);
  14. assert.equal(applied.generation,planned.generation);
  15. assert.equal(applied.fingerprint,planned.fingerprint);
  16. assert(status.publicationFound&&status.allRowsVerified);
  17. assert.equal(status.currentGeneration,applied.generation);
  18. assert.equal(status.publishedGeneration,applied.generation);
  19. assert.equal(after.generation,applied.generation);
  20. const generation=applied.generation;
  21. function typed(row,entity,manifest){
  22. const columns=manifest.tables.find(t=>t.name===(entity==='social'?'social_posts':entity)).columns;
  23. const result={...row};for(const key of ['payload_sha256','source_payload','_ksf_source_rowid'])delete result[key];
  24. for(const f of columns)if(result[f.name]!=null&&f.type.toUpperCase().includes('INT')){
  25. result[f.name]=Number(result[f.name]);assert(Number.isSafeInteger(result[f.name]));
  26. }
  27. return result;
  28. }
  29. const retainedChecks={};
  30. for(const entity of ['products','social']){
  31. const deletions=new Set(plan.removals.filter(r=>r.entity===entity).map(r=>r.id));
  32. const repairs=new Map(plan.repairs.filter(r=>r.entity===entity).map(r=>[r.id,r.changes]));
  33. const existing=new Map(after[entity].map(r=>[r.id,typed(r,entity,after.manifest)]));
  34. let count=0;
  35. for(const row of before[entity]){
  36. if(deletions.has(row.id)){assert(!existing.has(row.id));continue;}
  37. assert(isDeepStrictEqual(existing.get(row.id),{...typed(row,entity,before.manifest),...repairs.get(row.id)}),'RETAINED_RECORD_MISMATCH:'+row.id);
  38. count++;
  39. }
  40. assert.equal(existing.size,count);retainedChecks[entity]=count;
  41. }
  42. for(const name of ['assets','source_records','source_links','source_origins','collection_requests','policy_documents','policy_clauses','prices','ranking_signals']){
  43. const b=before.manifest.tables.find(t=>t.name===name),a=after.manifest.tables.find(t=>t.name===name);
  44. assert.equal(a.sha256,b.sha256,'UNCHANGED_TABLE_HASH:'+name);assert.equal(a.count,b.count);
  45. }
  46. const contract=await read('contract');save(work+'/contract-after.json',contract);
  47. assert.equal(contract.generation,generation);
  48. const summary=await publicApi({op:'summary',generation});save(work+'/public-summary-after.json',summary);
  49. assert.equal(summary.generation,generation);
  50. for(const [table,count] of Object.entries(contract.counts)){
  51. assert.equal(count,applied.plannedCounts[table]);assert.equal(summary.counts[table],count);
  52. }
  53. const checks=[];
  54. for(const entity of ['products','social']){
  55. const ids=plan.removals.filter(r=>r.entity===entity).map(r=>r.id);
  56. if(!ids.length)continue;
  57. const page=await publicApi({op:'browse',entity,quality:'audit',limit:100,filters:{recordId:ids},generation});
  58. assert.equal(page.totalMatched,0);assert.equal(page.records.length,0);
  59. save(work+'/public-removed-'+entity+'.json',page);checks.push({entity,removedIdsVerified:ids.length});
  60. }
  61. const productIds=plan.removals.filter(r=>r.entity==='products').map(r=>r.id);
  62. for(const entity of ['reviews','specs','coverage']){
  63. if(!productIds.length)continue;
  64. const page=await publicApi({op:'browse',entity,quality:'audit',limit:100,filters:{productId:productIds},generation});
  65. assert.equal(page.totalMatched,0);checks.push({entity,orphanDependents:0});
  66. }
  67. let repairsChecked=0;
  68. for(let start=0;start<plan.repairs.length;start+=100){
  69. const group=plan.repairs.slice(start,start+100);
  70. const page=await publicApi({op:'browse',entity:'social',quality:'audit',limit:100,filters:{recordId:group.map(r=>r.id)},generation});
  71. assert.equal(page.records.length,group.length);assert.equal(page.totalMatched,group.length);
  72. for(const r of page.records){assert.equal(r.fulltext_available,0);assert.equal(r.preview,'');}
  73. repairsChecked+=group.length;
  74. save(work+'/public-repairs-'+start+'.json',page);
  75. }
  76. const protectedPage=await publicApi({op:'browse',entity:'social',quality:'audit',limit:100,filters:{recordId:plan.protectedSocial.map(r=>r.id)},generation});
  77. assert.equal(protectedPage.totalMatched,plan.protectedSocial.length);
  78. assert(protectedPage.records.every(r=>r.fulltext_available===1));
  79. save(work+'/public-protected-articles.json',protectedPage);
  80. const sourceId='91cf603c247bcd1ac5a56c96fa88cc38a5a25b70f501bef50606c5e9d102499d';
  81. const source=await read('source',{id:sourceId,generation});save(work+'/removed-example-source-preserved.json',source);
  82. assert(source.ok&&source.sourcePayload);assert.equal(source.citation.id,sourceId);
  83. const result={ok:true,checkedAt:new Date().toISOString(),generation,removedProducts:applied.removedProducts,removedSocial:applied.removedSocial,
  84. repairedFulltextFlags:repairsChecked,retainedRecordsIndependentlyVerified:retainedChecks,protectedArticlesVerified:plan.protectedSocial.length,
  85. allRowsVerified:true,publicBoardReadbackVerified:true,sourcePreserved:true,historyPreserved:true,checks,counts:contract.counts,
  86. criteria:['清理明确无关的饮料、邮费链接和娱乐内容','未将资料缺字段或访问受限视为删除依据','保留有研发参考价值的调味、原料及真实测评线索'],
  87. limits:['仍有仅标题或待核线索保留在全部资料中,未冒充可读正文;未判定为全库资料逐条有效。']};
  88. save(work+'/verification-result.json',result);console.log(JSON.stringify(result));