/** Read back this intake's allowlisted records; publish a credential-free reference snapshot. No DB writes. */ import fs from 'node:fs/promises'; import crypto from 'node:crypto'; import { invoke, appId, functionsUrl } from '../server/scripts/platform-client.mjs'; const archive = 'docs/noodle-innovation-20261108'; const read = async name => JSON.parse(await fs.readFile(name, 'utf8')); const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex'); const must = (v, code) => { if (!v) throw Error(code); }; const FIELDS = { KfCategory: ['code','name','axis','scopeNote','taxonomyVersion'], KfCategoryRelation: ['fromCategoryRef','toCategoryRef','relationType','rationale','claimKind'], KfStandard: ['standardCode','edition','title','status','effectiveDate','scope'], KfMaterial: ['name','aliases','materialType','function','constraints','specification','storageConditions','sensoryProfile'], KfProcess: ['name','processType','processKind','description','steps','parameters','factoryContext'], KfSupplier: ['name','supplierType','region','supplyScope','approvalState','qualification'], }; async function main() { const [plan, receipt, sourceIndex, scope, contexts, locators] = await Promise.all([ read('work/noodle-innovation-20261108-r1/intake-plan.json'), read(archive+'/receipts/intake-summary.json'), read(archive+'/source-index.json'), read(archive+'/scope-audit.json'), read(archive+'/review-context-candidates.json'), read(archive+'/product-locator-index.json'), ]); const config = {}; for (const line of (await fs.readFile('F:/文档/康师傅数字人受限连接.env','utf8')).split(/\r?\n/)) { const m = line.match(/^(KSF_SERVICE_TOKEN|KSF_FUNCTIONS_URL|KSF_PARSE_APP_ID)=(.*)$/); if (m) config[m[1]]=m[2].trim(); } must(config.KSF_SERVICE_TOKEN && config.KSF_FUNCTIONS_URL === functionsUrl && config.KSF_PARSE_APP_ID === appId, 'CONFIG_MISMATCH'); const headers = {'X-Parse-Application-Id':appId,'X-Ksf-Service-Token':config.KSF_SERVICE_TOKEN,'Content-Type':'application/json'}; let reads=0; async function get(className,objectId) { const result=await invoke('ksfGetRecord',{className,objectId},headers); must(result.http===200 && result.body.result?.ok, 'READBACK_FAILED:'+className+':'+objectId); reads++;return result.body.result.data.record; } const records=[], sources=[]; const seen=new Set(); // Sequential groups preserve source binding; no inferred IDs from array positions. for (const group of receipt.groups) { const planned=plan.groups.find(g=>g.key===group.key); const source=await get('KfSource',group.sourceId); must(sha(Buffer.from(source.rawText))===group.sourceContentHash,'SOURCE_CHANGED'); if (!seen.has(group.sourceId)) { seen.add(group.sourceId); sources.push({className:'KfSource',objectId:group.sourceId,title:source.title, contentHash:group.sourceContentHash,originalUrl:sourceIndex.find(s=>s.id===group.key)?.originalUrl||null}); } for (const ref of group.records) { const row=await get(ref.className,ref.objectId); const original=planned.records.find(r=>r.externalKey===row.externalKey); must(original && (!row.sourceRef || row.sourceRef.objectId===group.sourceId) && row.recordState==='active','RECORD_BINDING_CHANGED'); must(['pending','approved','rejected'].includes(row.reviewState),'UNKNOWN_REVIEW_STATE'); // Publish current DB values only, and only explicitly public research fields. records.push({key:original.itemKey,className:ref.className,objectId:ref.objectId, sourceId:group.sourceId,sourceBinding:row.sourceRef?'record_pointer':'verified_intake_receipt',reviewState:row.reviewState, ...Object.fromEntries(FIELDS[ref.className].filter(k=>row[k]!==undefined).map(k=>[k,row[k]]))}); } } must(records.length===58 && sources.length===14,'INCOMPLETE_INTAKE'); const snapshot=await read('work/industry-monitoring-20261107-r1/snapshot.json'); const reviews=new Map(snapshot.entities.reviews.map(r=>[r.record_id,r])); const products=new Map(locators.products.map(p=>[p.productId,p])); for (const row of contexts) { const raw=reviews.get(row.reviewId); must(raw && raw.content===row.text && raw.product_id===row.productId && products.has(row.productId) && raw.review_date_raw===row.date && raw.source_record_id===row.sourceRecordId && raw.rating_raw===row.rating,'REVIEW_CONTEXT_CHANGED'); } const versionRes=await fetch('https://kangshifu.fmode.cn/api/library',{ method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({op:'version'}),signal:AbortSignal.timeout(30000)}); const version=await versionRes.json(); must(versionRes.ok && version.ok && version.generation===plan.libraryGeneration,'LIBRARY_GENERATION_CHANGED'); const data={schema:'ksf.development-references.v1',publishedAt:new Date().toISOString(), snapshotKind:'verified_database_publication',libraryGeneration:plan.libraryGeneration, coverageAsOf:scope.snapshotExportedAt,scope:{products:scope.scopeProducts,excluded:scope.excludedProducts, pending:scope.pendingScopeProducts,categories:scope.categories,coverage:scope.coverage,brandCoverage:scope.brandCoverage}, records,sources,reviewExamples:contexts.map(r=>({...r,productTitle:products.get(r.productId).title})), locatorSourceId:'T_o8CUTF83',databaseWrites:0,paidCalls:0}; await fs.mkdir('src/data',{recursive:true}); const text=JSON.stringify(data,null,2)+'\n'; await fs.writeFile('src/data/development-references.json',text); await fs.mkdir('docs/development-board-20261108',{recursive:true}); await fs.writeFile('docs/development-board-20261108/data-publication.json',JSON.stringify({ok:true, publishedAt:data.publishedAt,reads,records:records.length,sources:sources.length,reviewExamples:contexts.length, sha256:sha(Buffer.from(text)),generation:version.generation,credentialsIncluded:false,databaseWrites:0,paidCalls:0},null,2)+'\n'); console.log(JSON.stringify({ok:true,reads,records:records.length,sources:sources.length,databaseWrites:0,paidCalls:0})); } main().catch(e=>{console.error(JSON.stringify({ok:false,code:/^[A-Z_]+(?::[A-Za-z0-9:_-]+)?$/.test(e.message)?e.message:'PUBLICATION_FAILED',type:e.name}));process.exitCode=1;});