/** Independent PostgreSQL, authenticated query and public-board readback. */ import assert from 'node:assert/strict'; import {isDeepStrictEqual} from 'node:util'; import {load,save,read,publicApi,unwrap} from './lib/research-enrichment.mjs'; const work=process.env.KSF_CLEANUP_WORK||'work/library-cleanup-20261111'; const input=load(work+'/sealed-plan.json'),plan=input.plan; const applied=unwrap(load(work+'/apply-result.json')); const planned=unwrap(load(work+'/plan-result.json')); const status=unwrap(load(work+'/status-result.json')); const before=unwrap(load(work+'/inventory-result.json')); const after=unwrap(load(work+'/inventory-after-result.json')); assert(applied.ok&&applied.applied&&applied.allRowsVerified); assert.equal(applied.planSha256,input.planSha256); assert.equal(applied.generation,planned.generation); assert.equal(applied.fingerprint,planned.fingerprint); assert(status.publicationFound&&status.allRowsVerified); assert.equal(status.currentGeneration,applied.generation); assert.equal(status.publishedGeneration,applied.generation); assert.equal(after.generation,applied.generation); const generation=applied.generation; function typed(row,entity,manifest){ const columns=manifest.tables.find(t=>t.name===(entity==='social'?'social_posts':entity)).columns; const result={...row};for(const key of ['payload_sha256','source_payload','_ksf_source_rowid'])delete result[key]; for(const f of columns)if(result[f.name]!=null&&f.type.toUpperCase().includes('INT')){ result[f.name]=Number(result[f.name]);assert(Number.isSafeInteger(result[f.name])); } return result; } const retainedChecks={}; for(const entity of ['products','social']){ const deletions=new Set(plan.removals.filter(r=>r.entity===entity).map(r=>r.id)); const repairs=new Map(plan.repairs.filter(r=>r.entity===entity).map(r=>[r.id,r.changes])); const existing=new Map(after[entity].map(r=>[r.id,typed(r,entity,after.manifest)])); let count=0; for(const row of before[entity]){ if(deletions.has(row.id)){assert(!existing.has(row.id));continue;} assert(isDeepStrictEqual(existing.get(row.id),{...typed(row,entity,before.manifest),...repairs.get(row.id)}),'RETAINED_RECORD_MISMATCH:'+row.id); count++; } assert.equal(existing.size,count);retainedChecks[entity]=count; } for(const name of ['assets','source_records','source_links','source_origins','collection_requests','policy_documents','policy_clauses','prices','ranking_signals']){ const b=before.manifest.tables.find(t=>t.name===name),a=after.manifest.tables.find(t=>t.name===name); assert.equal(a.sha256,b.sha256,'UNCHANGED_TABLE_HASH:'+name);assert.equal(a.count,b.count); } const contract=await read('contract');save(work+'/contract-after.json',contract); assert.equal(contract.generation,generation); const summary=await publicApi({op:'summary',generation});save(work+'/public-summary-after.json',summary); assert.equal(summary.generation,generation); for(const [table,count] of Object.entries(contract.counts)){ assert.equal(count,applied.plannedCounts[table]);assert.equal(summary.counts[table],count); } const checks=[]; for(const entity of ['products','social']){ const ids=plan.removals.filter(r=>r.entity===entity).map(r=>r.id); if(!ids.length)continue; const page=await publicApi({op:'browse',entity,quality:'audit',limit:100,filters:{recordId:ids},generation}); assert.equal(page.totalMatched,0);assert.equal(page.records.length,0); save(work+'/public-removed-'+entity+'.json',page);checks.push({entity,removedIdsVerified:ids.length}); } const productIds=plan.removals.filter(r=>r.entity==='products').map(r=>r.id); for(const entity of ['reviews','specs','coverage']){ if(!productIds.length)continue; const page=await publicApi({op:'browse',entity,quality:'audit',limit:100,filters:{productId:productIds},generation}); assert.equal(page.totalMatched,0);checks.push({entity,orphanDependents:0}); } let repairsChecked=0; for(let start=0;startr.id)},generation}); assert.equal(page.records.length,group.length);assert.equal(page.totalMatched,group.length); for(const r of page.records){assert.equal(r.fulltext_available,0);assert.equal(r.preview,'');} repairsChecked+=group.length; save(work+'/public-repairs-'+start+'.json',page); } const protectedPage=await publicApi({op:'browse',entity:'social',quality:'audit',limit:100,filters:{recordId:plan.protectedSocial.map(r=>r.id)},generation}); assert.equal(protectedPage.totalMatched,plan.protectedSocial.length); assert(protectedPage.records.every(r=>r.fulltext_available===1)); save(work+'/public-protected-articles.json',protectedPage); const sourceId='91cf603c247bcd1ac5a56c96fa88cc38a5a25b70f501bef50606c5e9d102499d'; const source=await read('source',{id:sourceId,generation});save(work+'/removed-example-source-preserved.json',source); assert(source.ok&&source.sourcePayload);assert.equal(source.citation.id,sourceId); const result={ok:true,checkedAt:new Date().toISOString(),generation,removedProducts:applied.removedProducts,removedSocial:applied.removedSocial, repairedFulltextFlags:repairsChecked,retainedRecordsIndependentlyVerified:retainedChecks,protectedArticlesVerified:plan.protectedSocial.length, allRowsVerified:true,publicBoardReadbackVerified:true,sourcePreserved:true,historyPreserved:true,checks,counts:contract.counts, criteria:['清理明确无关的饮料、邮费链接和娱乐内容','未将资料缺字段或访问受限视为删除依据','保留有研发参考价值的调味、原料及真实测评线索'], limits:['仍有仅标题或待核线索保留在全部资料中,未冒充可读正文;未判定为全库资料逐条有效。']}; save(work+'/verification-result.json',result);console.log(JSON.stringify(result));