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