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