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

23 lines
4.2 KiB
JavaScript

import fs from 'node:fs'
import path from 'node:path'
import { spawnSync } from 'node:child_process'
const gen=spawnSync(process.execPath,['scripts/generate-experiment-contracts.mjs'],{stdio:'inherit'});if(gen.status!==0)process.exit(gen.status??1)
const source=JSON.parse(fs.readFileSync('contracts/experiments/kbx.experiments.json','utf8')), manifest=JSON.parse(fs.readFileSync('generated/experiment-manifest.json','utf8')), telemetry=JSON.parse(fs.readFileSync('contracts/telemetry/kbx.telemetry.json','utf8'))
const metrics=new Set(telemetry.metrics.map(x=>x.key)), errors=[]
if(source.principles.safeUxOnly!==true||source.principles.killSwitchRequired!==true||source.principles.exposureAfterRender!==true)errors.push('safe rollout principles are incomplete')
for(const x of ['permission','sensitive-data','domain-rule','validation','idempotency','workflow-safety','wms-scan-rule','inventory-truth'])if(!source.principles.prohibitedScopes.includes(x))errors.push(`missing prohibited experiment scope: ${x}`)
for(const exp of source.experiments){for(const m of [exp.primaryMetric,...exp.guardrails,...exp.globalGuardrails])if(!metrics.has(m.key))errors.push(`unknown metric ${m.key}`);if(exp.state==='running'&&exp.rolloutPercent===0)errors.push(`running experiment has zero rollout: ${exp.id}`)}
const running=new Map();for(const exp of source.experiments.filter(x=>x.state==='running'))running.set(exp.screenId,(running.get(exp.screenId)??0)+1);for(const [screen,count] of running)if(count>source.principles.maxRunningExperimentsPerScreen)errors.push(`concurrent experiment limit exceeded: ${screen}`)
const migration=fs.readFileSync('backend/Database/Migrations/20260808_012_kbx_experiment_rollout.sql','utf8');for(const table of ['kbx.experiment_runtime','kbx.experiment_assignments','kbx.experiment_audit'])if(!migration.includes(table))errors.push(`missing experiment table: ${table}`)
if(!migration.includes('experiment_id')||!migration.includes('experiment_variant'))errors.push('telemetry experiment context columns missing')
const assignment=fs.readFileSync('backend/Shared/Experiments/KbxExperimentAssignmentService.cs','utf8'), assignmentPolicy=fs.readFileSync('backend/Shared/Experiments/KbxExperimentAssignmentPolicy.cs','utf8');if(!assignmentPolicy.includes('SHA256')||!assignment.includes('experiment_assignments')||!assignment.includes('state!="running"')||!assignment.includes('continue;'))errors.push('stable/minimal server-authoritative assignment missing')
const runtime=fs.readFileSync('backend/Shared/Experiments/KbxExperimentRuntimeStore.cs','utf8');if(!runtime.includes("'rolled-back'")||!runtime.includes('experiment_audit')||!runtime.includes('publisher.PublishAsync'))errors.push('kill switch/audit/realtime rollback path missing')
const web=fs.readFileSync('apps/web/src/experiments/kbxExperimentClient.ts','utf8'), realtimeWeb=fs.readFileSync('apps/web/src/experiments/createExperimentRolloutConnection.ts','utf8');if(!web.includes("experiment.exposed")||!web.includes('assignment.enrolled')||!realtimeWeb.includes('ExperimentChanged'))errors.push('exposure/realtime frontend contract missing')
const telemetryRepo=fs.readFileSync('backend/Shared/Telemetry/KbxUxTelemetryRepository.cs','utf8');if(!telemetryRepo.includes('NormalizeExperimentContextAsync')||!telemetryRepo.includes('KbxExperimentAssignmentPolicy.IsEnrolled'))errors.push('server-side experiment telemetry attribution validation missing')
if(!fs.existsSync('backend/Shared/Experiments/KbxExperimentRealtime.cs'))errors.push('SignalR rollout invalidation missing')
const page=fs.readFileSync('apps/web/src/modules/oms/orders/search/OrderListPage.vue','utf8');if(!page.includes('exp.oms.order-list.exception-summary-v2'))errors.push('reference experiment integration missing')
if(!fs.existsSync('apps/web/src/modules/common/experiments/ExperimentsPage.vue'))errors.push('COMMON-EXP-001 missing')
if(manifest.featureFlags.length!==source.featureFlags.length||manifest.experiments.length!==source.experiments.length)errors.push('experiment manifest drift')
if(errors.length){console.error('KBX experiment governance FAILED');errors.forEach(x=>console.error(' - '+x));process.exit(1)}
console.log(`KBX experiment governance PASS (${manifest.featureFlags.length} flags, ${manifest.experiments.length} experiments, safe scopes enforced)`)