import fs from 'node:fs' const read=p=>JSON.parse(fs.readFileSync(p,'utf8')) const src=read('contracts/external-data/kbx.external-data.json') const providers=read('contracts/providers/kbx.providers.json') const api=read('contracts/api/kbx.api.json') const auth=read('contracts/authorization/kbx.authorization.json') const errors=[] const providerMap=new Map(providers.providers.map(x=>[x.id,x])) const operationIds=new Set(providers.providers.flatMap(p=>(p.operations??[]).map(o=>o.id))) const ids=new Set() for(const d of src.datasets??[]){ if(ids.has(d.id))errors.push(`duplicate dataset ${d.id}`);ids.add(d.id) if(!providerMap.has(d.providerId))errors.push(`${d.id}: unknown provider ${d.providerId}`) if(!operationIds.has(d.providerOperationId))errors.push(`${d.id}: unknown provider operation ${d.providerOperationId}`) if(!['strict','stale-while-revalidate','provider-defined'].includes(d.freshness?.mode))errors.push(`${d.id}: invalid freshness mode`) if(d.freshness?.mode==='strict' && (d.freshness.maxStaleSeconds??0)!==0)errors.push(`${d.id}: strict dataset cannot serve stale fallback`) if(d.snapshot?.rawRetention!=='hash-only')errors.push(`${d.id}: v21 reference datasets must retain payload hash only unless licensing/security policy is explicitly reviewed`) if(!d.normalizerVersion)errors.push(`${d.id}: normalizer version required`) } const kis=src.datasets.find(x=>x.id==='dataset.kis.domestic-stock.current-price') if(!kis||kis.freshness.mode!=='strict'||kis.freshness.maxStaleSeconds!==0)errors.push('KIS current-price must stay strict/no stale fallback') const krx=src.datasets.find(x=>x.id==='dataset.krx.approved-service') if(!krx||krx.freshness.mode!=='provider-defined'||krx.freshness.freshForSeconds!==null||!krx.freshness.requireExplicitServicePolicy)errors.push('KRX approved-service must not invent a universal freshness TTL') for(const p of ['common.externalData.status','common.externalData.refresh'])if(!api.operations.some(x=>x.id===p))errors.push(`missing external-data API ${p}`) for(const p of ['common.external-data.read','common.external-data.refresh'])if(!auth.permissions.some(x=>x.id===p))errors.push(`missing external-data permission ${p}`) for(const f of ['backend/Database/Migrations/20260808_014_kbx_external_data.sql','backend/Shared/ExternalData/KbxExternalDataRepository.cs','backend/Shared/ExternalData/KbxExternalDataFreshnessPolicy.cs','backend/Shared/ExternalData/KbxExternalDataRetentionJob.cs','packages/kbx-ui/src/components/KbxDataProvenance.vue','apps/web/src/modules/common/external-data/external-data.definition.ts'])if(!fs.existsSync(f))errors.push(`missing v21 artifact ${f}`) const manifest=read('generated/external-data-manifest.json') if(manifest.datasets.length!==src.datasets.length)errors.push('external-data manifest count drift') const ts=fs.readFileSync('packages/kbx-contracts/src/generated/externalDataCatalog.ts','utf8'),cs=fs.readFileSync('backend/Shared/ExternalData/Generated/KbxExternalDataCatalog.g.cs','utf8') if(!ts.includes(manifest.sourceSha256)||!cs.includes(manifest.sourceSha256))errors.push('external-data generated source SHA parity missing') const migration=fs.readFileSync('backend/Database/Migrations/20260808_014_kbx_external_data.sql','utf8') for(const token of ['request_descriptor','payload_sha256','provider_observed_at','received_at','ingested_at','fresh_until','usable_until','normalizer_version'])if(!migration.includes(token))errors.push(`external data migration missing ${token}`) if(/(AUTH_KEY|crtfc_key|appsecret|access_token)/i.test(migration))errors.push('external data persistence must not contain provider secrets') if(/raw_payload\s+(?:jsonb|text|bytea)/i.test(migration))errors.push('raw provider payload column is forbidden in v21 default persistence') if(errors.length){console.error('external-data governance FAIL');for(const e of errors)console.error(`- ${e}`);process.exit(1)} console.log(`external-data governance PASS: datasets=${src.datasets.length}, provenance/freshness/raw-retention boundaries verified`)