Files
KArtSell.Aegis/docs/Design/kbx-foundation-v36/scripts/validate-external-data-governance.mjs
T

37 lines
3.9 KiB
JavaScript

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`)