publish-development-references.mjs 6.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889
  1. /** Read back this intake's allowlisted records; publish a credential-free reference snapshot. No DB writes. */
  2. import fs from 'node:fs/promises';
  3. import crypto from 'node:crypto';
  4. import { invoke, appId, functionsUrl } from '../server/scripts/platform-client.mjs';
  5. const archive = 'docs/noodle-innovation-20261108';
  6. const read = async name => JSON.parse(await fs.readFile(name, 'utf8'));
  7. const sha = bytes => crypto.createHash('sha256').update(bytes).digest('hex');
  8. const must = (v, code) => { if (!v) throw Error(code); };
  9. const FIELDS = {
  10. KfCategory: ['code','name','axis','scopeNote','taxonomyVersion'],
  11. KfCategoryRelation: ['fromCategoryRef','toCategoryRef','relationType','rationale','claimKind'],
  12. KfStandard: ['standardCode','edition','title','status','effectiveDate','scope'],
  13. KfMaterial: ['name','aliases','materialType','function','constraints','specification','storageConditions','sensoryProfile'],
  14. KfProcess: ['name','processType','processKind','description','steps','parameters','factoryContext'],
  15. KfSupplier: ['name','supplierType','region','supplyScope','approvalState','qualification'],
  16. };
  17. async function main() {
  18. const [plan, receipt, sourceIndex, scope, contexts, locators] = await Promise.all([
  19. read('work/noodle-innovation-20261108-r1/intake-plan.json'), read(archive+'/receipts/intake-summary.json'),
  20. read(archive+'/source-index.json'), read(archive+'/scope-audit.json'),
  21. read(archive+'/review-context-candidates.json'), read(archive+'/product-locator-index.json'),
  22. ]);
  23. const config = {};
  24. for (const line of (await fs.readFile('F:/文档/康师傅数字人受限连接.env','utf8')).split(/\r?\n/)) {
  25. const m = line.match(/^(KSF_SERVICE_TOKEN|KSF_FUNCTIONS_URL|KSF_PARSE_APP_ID)=(.*)$/);
  26. if (m) config[m[1]]=m[2].trim();
  27. }
  28. must(config.KSF_SERVICE_TOKEN && config.KSF_FUNCTIONS_URL === functionsUrl && config.KSF_PARSE_APP_ID === appId, 'CONFIG_MISMATCH');
  29. const headers = {'X-Parse-Application-Id':appId,'X-Ksf-Service-Token':config.KSF_SERVICE_TOKEN,'Content-Type':'application/json'};
  30. let reads=0;
  31. async function get(className,objectId) {
  32. const result=await invoke('ksfGetRecord',{className,objectId},headers);
  33. must(result.http===200 && result.body.result?.ok, 'READBACK_FAILED:'+className+':'+objectId);
  34. reads++;return result.body.result.data.record;
  35. }
  36. const records=[], sources=[];
  37. const seen=new Set();
  38. // Sequential groups preserve source binding; no inferred IDs from array positions.
  39. for (const group of receipt.groups) {
  40. const planned=plan.groups.find(g=>g.key===group.key);
  41. const source=await get('KfSource',group.sourceId);
  42. must(sha(Buffer.from(source.rawText))===group.sourceContentHash,'SOURCE_CHANGED');
  43. if (!seen.has(group.sourceId)) {
  44. seen.add(group.sourceId);
  45. sources.push({className:'KfSource',objectId:group.sourceId,title:source.title,
  46. contentHash:group.sourceContentHash,originalUrl:sourceIndex.find(s=>s.id===group.key)?.originalUrl||null});
  47. }
  48. for (const ref of group.records) {
  49. const row=await get(ref.className,ref.objectId);
  50. const original=planned.records.find(r=>r.externalKey===row.externalKey);
  51. must(original && (!row.sourceRef || row.sourceRef.objectId===group.sourceId) && row.recordState==='active','RECORD_BINDING_CHANGED');
  52. must(['pending','approved','rejected'].includes(row.reviewState),'UNKNOWN_REVIEW_STATE');
  53. // Publish current DB values only, and only explicitly public research fields.
  54. records.push({key:original.itemKey,className:ref.className,objectId:ref.objectId,
  55. sourceId:group.sourceId,sourceBinding:row.sourceRef?'record_pointer':'verified_intake_receipt',reviewState:row.reviewState,
  56. ...Object.fromEntries(FIELDS[ref.className].filter(k=>row[k]!==undefined).map(k=>[k,row[k]]))});
  57. }
  58. }
  59. must(records.length===58 && sources.length===14,'INCOMPLETE_INTAKE');
  60. const snapshot=await read('work/industry-monitoring-20261107-r1/snapshot.json');
  61. const reviews=new Map(snapshot.entities.reviews.map(r=>[r.record_id,r]));
  62. const products=new Map(locators.products.map(p=>[p.productId,p]));
  63. for (const row of contexts) {
  64. const raw=reviews.get(row.reviewId);
  65. must(raw && raw.content===row.text && raw.product_id===row.productId && products.has(row.productId) &&
  66. raw.review_date_raw===row.date && raw.source_record_id===row.sourceRecordId && raw.rating_raw===row.rating,'REVIEW_CONTEXT_CHANGED');
  67. }
  68. const versionRes=await fetch('https://kangshifu.fmode.cn/api/library',{
  69. method:'POST',headers:{'content-type':'application/json'},body:JSON.stringify({op:'version'}),signal:AbortSignal.timeout(30000)});
  70. const version=await versionRes.json();
  71. must(versionRes.ok && version.ok && version.generation===plan.libraryGeneration,'LIBRARY_GENERATION_CHANGED');
  72. const data={schema:'ksf.development-references.v1',publishedAt:new Date().toISOString(),
  73. snapshotKind:'verified_database_publication',libraryGeneration:plan.libraryGeneration,
  74. coverageAsOf:scope.snapshotExportedAt,scope:{products:scope.scopeProducts,excluded:scope.excludedProducts,
  75. pending:scope.pendingScopeProducts,categories:scope.categories,coverage:scope.coverage,brandCoverage:scope.brandCoverage},
  76. records,sources,reviewExamples:contexts.map(r=>({...r,productTitle:products.get(r.productId).title})),
  77. locatorSourceId:'T_o8CUTF83',databaseWrites:0,paidCalls:0};
  78. await fs.mkdir('src/data',{recursive:true});
  79. const text=JSON.stringify(data,null,2)+'\n';
  80. await fs.writeFile('src/data/development-references.json',text);
  81. await fs.mkdir('docs/development-board-20261108',{recursive:true});
  82. await fs.writeFile('docs/development-board-20261108/data-publication.json',JSON.stringify({ok:true,
  83. publishedAt:data.publishedAt,reads,records:records.length,sources:sources.length,reviewExamples:contexts.length,
  84. sha256:sha(Buffer.from(text)),generation:version.generation,credentialsIncluded:false,databaseWrites:0,paidCalls:0},null,2)+'\n');
  85. console.log(JSON.stringify({ok:true,reads,records:records.length,sources:sources.length,databaseWrites:0,paidCalls:0}));
  86. }
  87. 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;});