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

47 lines
4.8 KiB
JavaScript

import fs from 'node:fs'
const read=p=>JSON.parse(fs.readFileSync(p,'utf8'))
const src=read('contracts/integrations/kbx.integrations.json')
const manifest=read('generated/integration-manifest.json')
const api=read('generated/api-manifest.json')
const perms=read('generated/permission-manifest.json')
const scenarios=read('generated/test-scenario-manifest.json')
const errors=[]
const ids=new Set()
const allowedTransport=new Set(['outbox-http','outbox-event','inbox-event'])
const allowedTerminal=new Set(['operations-exception','dead-letter'])
for(const x of src.integrations??[]){
if(ids.has(x.id))errors.push(`duplicate integration ${x.id}`);ids.add(x.id)
if(!/^integration\.[a-z0-9.-]+$/.test(x.id))errors.push(`invalid integration id ${x.id}`)
if(!allowedTransport.has(x.transport))errors.push(`${x.id}: invalid transport ${x.transport}`)
if(!allowedTerminal.has(x.terminalAction))errors.push(`${x.id}: invalid terminalAction`)
if(x.delivery==='at-least-once'&&x.idempotency==='none')errors.push(`${x.id}: at-least-once requires idempotency`)
if(x.shortRetry.maxRetryAttempts>3)errors.push(`${x.id}: short retry must stay bounded; use Hangfire for durable retry`)
if(x.longRetry.scheduler!=='hangfire')errors.push(`${x.id}: durable retry must use Hangfire`)
if(x.longRetry.scheduleSeconds.length!==x.longRetry.maxAttempts)errors.push(`${x.id}: durable schedule/attempt count mismatch`)
if(x.criticality==='high'&&x.terminalAction!=='operations-exception')errors.push(`${x.id}: high criticality must surface in operations exception`)
for(const code of x.retryable??[])if((x.permanent??[]).includes(code))errors.push(`${x.id}: failure classification overlaps ${code}`)
}
if(manifest.integrations.length!==src.integrations.length)errors.push('generated integration manifest count drift')
const ts=fs.readFileSync('packages/kbx-contracts/src/generated/integrationCatalog.ts','utf8')
const cs=fs.readFileSync('backend/Shared/Integrations/Generated/KbxIntegrationCatalog.g.cs','utf8')
if(!ts.includes(manifest.sourceSha256)||!cs.includes(manifest.sourceSha256))errors.push('integration source SHA parity missing')
for(const id of ['common.integrations.getAttempt','common.integrations.retryAttempt'])if(!(api.operations??[]).some(x=>x.id===id))errors.push(`missing integration API ${id}`)
for(const id of ['common.integration.read','common.integration.retry'])if(!(perms.permissions??[]).some(x=>x.id===id))errors.push(`missing integration permission ${id}`)
for(const id of ['scenario.integration.outbox.transient-retry','scenario.integration.outbox.permanent-failure','scenario.integration.inbox.duplicate','scenario.integration.manual-retry.idempotent'])if(!(scenarios.scenarios??[]).some(x=>x.id===id))errors.push(`missing integration scenario ${id}`)
const migration=fs.readFileSync('backend/Database/Migrations/20260808_013_kbx_integration_resilience.sql','utf8')
for(const required of ['integration_attempts','integration_receipts','unique (tenant_id, integration_id, message_id, attempt_no)'])if(!migration.includes(required))errors.push(`migration missing ${required}`)
const dispatcher=fs.readFileSync('backend/Shared/Integrations/KbxOutboxIntegrationDispatcher.cs','utf8')
if(/while\s*\(true\)|RetryForever|Thread\.Sleep/.test(dispatcher))errors.push('integration dispatcher contains unbounded in-process retry')
const component=fs.readFileSync('packages/kbx-ui/src/components/KbxIntegrationState.vue','utf8')
const catalog=fs.readFileSync('packages/kbx-ui/src/catalog/componentCatalog.ts','utf8')
for(const label of ['전송 대기','자동 재시도','연계 실패'])if(!component.includes(label))errors.push(`KbxIntegrationState missing label ${label}`)
if(!catalog.includes("component:'KbxIntegrationState'"))errors.push('KbxIntegrationState missing from component catalog')
const polly=fs.readFileSync('backend/Shared/Integrations/KbxPollyIntegrationPipeline.cs','utf8')
for(const token of ['ResiliencePipelineBuilder','AddRetry','AddTimeout','AddCircuitBreaker'])if(!polly.includes(token))errors.push(`Polly pipeline missing ${token}`)
const projector=fs.readFileSync('backend/Shared/Integrations/KbxIntegrationOperationsProjector.cs','utf8')
if(!projector.includes('INTEGRATION_FAILED')||!projector.includes('OperationsProjectionWriter'))errors.push('permanent integration failure is not projected to Operations Center')
const retryEndpoint=fs.readFileSync('backend/Modules/Common/Integrations/Attempts/Retry/Endpoint.cs','utf8')
if(!retryEndpoint.includes('kbx.command_receipts')||!retryEndpoint.includes('Idempotency-Key'))errors.push('manual integration retry lacks command-receipt idempotency')
if(errors.length){console.error('integration governance FAIL');for(const e of errors)console.error(`- ${e}`);process.exit(1)}
console.log(`integration governance PASS: integrations=${src.integrations.length}, scenarios=4, bounded short retry + durable Hangfire retry`)