import fs from 'node:fs' import path from 'node:path' const release=JSON.parse(fs.readFileSync('governance/release.json','utf8')) const baseDir=path.join('governance/baselines',release.baseline) const read=p=>JSON.parse(fs.readFileSync(p,'utf8')) const prev={ components:read(path.join(baseDir,'component-manifest.json')), screens:read(path.join(baseDir,'screen-manifest.json')), tokens:read(path.join(baseDir,'token-manifest.json')), fields:read(path.join(baseDir,'field-manifest.json')), api:fs.existsSync(path.join(baseDir,'api-manifest.json'))?read(path.join(baseDir,'api-manifest.json')):{operations:[]}, problems:fs.existsSync(path.join(baseDir,'problem-manifest.json'))?read(path.join(baseDir,'problem-manifest.json')):{types:[]}, permissions:fs.existsSync(path.join(baseDir,'permission-manifest.json'))?read(path.join(baseDir,'permission-manifest.json')):{permissions:[]}, sensitive:fs.existsSync(path.join(baseDir,'sensitive-data-manifest.json'))?read(path.join(baseDir,'sensitive-data-manifest.json')):{policies:[]}, telemetry:fs.existsSync(path.join(baseDir,'telemetry-manifest.json'))?read(path.join(baseDir,'telemetry-manifest.json')):{events:[],metrics:[]}, experiments:fs.existsSync(path.join(baseDir,'experiment-manifest.json'))?read(path.join(baseDir,'experiment-manifest.json')):{featureFlags:[],experiments:[]}, testing:fs.existsSync(path.join(baseDir,'test-scenario-manifest.json'))?read(path.join(baseDir,'test-scenario-manifest.json')):{scenarios:[]}, integrations:fs.existsSync(path.join(baseDir,'integration-manifest.json'))?read(path.join(baseDir,'integration-manifest.json')):{integrations:[]}, providers:fs.existsSync(path.join(baseDir,'provider-manifest.json'))?read(path.join(baseDir,'provider-manifest.json')):{providers:[]}, externalData:fs.existsSync(path.join(baseDir,'external-data-manifest.json'))?read(path.join(baseDir,'external-data-manifest.json')):{datasets:[]}, configuration:fs.existsSync(path.join(baseDir,'configuration-manifest.json'))?read(path.join(baseDir,'configuration-manifest.json')):{settings:[],environments:[]}, screenRecipes:fs.existsSync(path.join(baseDir,'screen-recipe-manifest.json'))?read(path.join(baseDir,'screen-recipe-manifest.json')):{recipes:[]}, } const curr={ components:read('generated/component-manifest.json'), screens:read('generated/screen-manifest.json'), tokens:read('generated/token-manifest.json'), fields:read('generated/field-manifest.json'), api:read('generated/api-manifest.json'), problems:read('generated/problem-manifest.json'), permissions:read('generated/permission-manifest.json'), sensitive:read('generated/sensitive-data-manifest.json'), telemetry:read('generated/telemetry-manifest.json'), experiments:read('generated/experiment-manifest.json'), testing:read('generated/test-scenario-manifest.json'), integrations:read('generated/integration-manifest.json'), providers:read('generated/provider-manifest.json'), externalData:read('generated/external-data-manifest.json'), configuration:read('generated/configuration-manifest.json'), screenRecipes:read('generated/screen-recipe-manifest.json'), } const levels={none:0,patch:1,minor:2,major:3} let required='none';const changes=[] const promote=l=>{if(levels[l]>levels[required])required=l} function diffNamed(kind,oldList,newList,key='name'){ const oldMap=new Map(oldList.map(x=>[x[key],x])),newMap=new Map(newList.map(x=>[x[key],x])) for(const [k] of oldMap){if(!newMap.has(k)){changes.push({level:'major',kind,change:'removed',key:k});promote('major')}} for(const [k] of newMap){if(!oldMap.has(k)){changes.push({level:'minor',kind,change:'added',key:k});promote('minor')}} return {oldMap,newMap} } const c=diffNamed('component',prev.components,curr.components) for(const [name,n] of c.newMap){const o=c.oldMap.get(name);if(!o)continue;if(o.version!==n.version){const [om,oni]=o.version.split('.').map(Number),[nm,nni]=n.version.split('.').map(Number);let l='patch';if(nm>om)l='major';else if(nni>oni)l='minor';changes.push({level:l,kind:'component',change:'version',key:name,from:o.version,to:n.version});promote(l)}} const s=diffNamed('screen',prev.screens,curr.screens,'id') for(const [id,n] of s.newMap){const o=s.oldMap.get(id);if(!o)continue;if(o.version!==n.version){const om=Number(o.version.split('.')[0]),nm=Number(n.version.split('.')[0]);const l=nm>om?'major':'patch';changes.push({level:l,kind:'screen',change:'version',key:id,from:o.version,to:n.version});promote(l)}for(const property of ['type','templateCode'])if((o[property]??null)!==(n[property]??null)){changes.push({level:'major',kind:'screen',change:property,key:id,from:o[property]??null,to:n[property]??null});promote('major')}} const sr=diffNamed('screen-recipe',prev.screenRecipes.recipes??[],curr.screenRecipes.recipes??[],'code') for(const [code,n] of sr.newMap){ const o=sr.oldMap.get(code);if(!o)continue for(const property of ['type','templateComponent'])if((o[property]??null)!==(n[property]??null)){changes.push({level:'major',kind:'screen-recipe',change:property,key:code,from:o[property]??null,to:n[property]??null});promote('major')} for(const property of ['defaultCommands','requiredPolicies','recoveryPolicies','securityPolicies','scaffoldSurfaces'])if(JSON.stringify(o[property]??[])!==JSON.stringify(n[property]??[])){changes.push({level:'major',kind:'screen-recipe',change:property,key:code});promote('major')} if(JSON.stringify(o.canonicalScenarioIds??[])!==JSON.stringify(n.canonicalScenarioIds??[])){changes.push({level:'patch',risk:'test-coverage',kind:'screen-recipe',change:'canonicalScenarioIds',key:code});promote('patch')} if(JSON.stringify(o.testProfile??null)!==JSON.stringify(n.testProfile??null)){changes.push({level:'patch',risk:'test-coverage',kind:'screen-recipe',change:'testProfile',key:code});promote('patch')} } const oldT=new Map(prev.tokens.tokens.map(x=>[x.path,x])),newT=new Map(curr.tokens.tokens.map(x=>[x.path,x])) for(const [k] of oldT){if(!newT.has(k)){changes.push({level:'major',kind:'token',change:'removed',key:k});promote('major')}} for(const [k,n] of newT){const o=oldT.get(k);if(!o){changes.push({level:'minor',kind:'token',change:'added',key:k});promote('minor');continue}if(o.value!==n.value){changes.push({level:'patch',risk:'visual-risk',kind:'token',change:'value',key:k,from:o.value,to:n.value});promote('patch')}} const f=diffNamed('field',prev.fields.fields??[],curr.fields.fields??[],'key') const structural=['dataType','maxLength','precision','scale','lookupEntity','sensitive'] for(const [key,n] of f.newMap){ const o=f.oldMap.get(key);if(!o)continue for(const property of structural){ if((o[property]??null)!==(n[property]??null)){ changes.push({level:'major',kind:'field',change:property,key,from:o[property]??null,to:n[property]??null});promote('major') } } if(o.label!==n.label || JSON.stringify(o.aliases??[])!==JSON.stringify(n.aliases??[])){ changes.push({level:'patch',risk:'mapping-risk',kind:'field',change:'label-or-alias',key});promote('patch') } if(Boolean(o.deprecated)!==Boolean(n.deprecated)){ changes.push({level:'patch',kind:'field',change:'deprecation',key,from:Boolean(o.deprecated),to:Boolean(n.deprecated)});promote('patch') } } const oldApi=new Map((prev.api.operations??[]).map(x=>[x.id,x])),newApi=new Map((curr.api.operations??[]).map(x=>[x.id,x])) for(const [id,o] of oldApi){ const n=newApi.get(id) if(!n){changes.push({level:'major',kind:'api',change:'removed',key:id});promote('major');continue} for(const property of ['method','path','permission']){ if((o[property]??null)!==(n[property]??null)){changes.push({level:'major',kind:'api',change:property,key:id,from:o[property]??null,to:n[property]??null});promote('major')} } if(o.idempotency!==n.idempotency){ const tightening=o.idempotency!=='required'&&n.idempotency==='required' const level=tightening?'major':'patch';changes.push({level,kind:'api',change:'idempotency',key:id,from:o.idempotency,to:n.idempotency});promote(level) } const oldSuccess=new Set(o.successStatuses??[200]),newSuccess=new Set(n.successStatuses??[200]) for(const status of oldSuccess)if(!newSuccess.has(status)){changes.push({level:'major',kind:'api',change:'success-status-removed',key:id,from:status});promote('major')} } for(const [id] of newApi)if(!oldApi.has(id)){changes.push({level:'minor',kind:'api',change:'added',key:id});promote('minor')} const oldProblems=new Set(prev.problems.types??[]),newProblems=new Set(curr.problems.types??[]) for(const type of oldProblems)if(!newProblems.has(type)){changes.push({level:'major',kind:'problem',change:'removed',key:type});promote('major')} for(const type of newProblems)if(!oldProblems.has(type)){changes.push({level:'minor',kind:'problem',change:'added',key:type});promote('minor')} const perm=diffNamed('permission',prev.permissions.permissions??[],curr.permissions.permissions??[],'id') for(const [id,n] of perm.newMap){const o=perm.oldMap.get(id);if(!o)continue;for(const prop of ['module','resource','action'])if((o[prop]??null)!==(n[prop]??null)){changes.push({level:'major',kind:'permission',change:prop,key:id,from:o[prop],to:n[prop]});promote('major')}} const sp=diffNamed('sensitive-policy',prev.sensitive.policies??[],curr.sensitive.policies??[],'id') for(const [id,n] of sp.newMap){const o=sp.oldMap.get(id);if(!o)continue;for(const prop of ['viewPermission','revealPermission','unmaskedExportPermission','aiExposure','telemetryExposure'])if((o[prop]??null)!==(n[prop]??null)){changes.push({level:'major',kind:'sensitive-policy',change:prop,key:id,from:o[prop],to:n[prop]});promote('major')}if(JSON.stringify(o.fields??[])!==JSON.stringify(n.fields??[])){changes.push({level:'major',kind:'sensitive-policy',change:'fields',key:id});promote('major')}} const te=diffNamed('telemetry-event',prev.telemetry.events??[],curr.telemetry.events??[],'name') for(const [name,n] of te.newMap){const o=te.oldMap.get(name);if(!o)continue;if(o.category!==n.category||JSON.stringify(o.allowedAttributes??[])!==JSON.stringify(n.allowedAttributes??[])||Boolean(o.requiresDuration)!==Boolean(n.requiresDuration)){changes.push({level:'major',kind:'telemetry-event',change:'contract',key:name});promote('major')}} const tm=diffNamed('ux-metric',prev.telemetry.metrics??[],curr.telemetry.metrics??[],'key') for(const [key,n] of tm.newMap){const o=tm.oldMap.get(key);if(!o)continue;if(JSON.stringify(o)!==JSON.stringify(n)){changes.push({level:'major',kind:'ux-metric',change:'formula',key});promote('major')}} const ef=diffNamed('feature-flag',prev.experiments.featureFlags??[],curr.experiments.featureFlags??[],'id') for(const [id,n] of ef.newMap){const o=ef.oldMap.get(id);if(!o)continue;if(o.screenId!==n.screenId||o.surface!==n.surface){changes.push({level:'major',kind:'feature-flag',change:'scope',key:id});promote('major')}} const ex=diffNamed('experiment',prev.experiments.experiments??[],curr.experiments.experiments??[],'id') for(const [id,n] of ex.newMap){const o=ex.oldMap.get(id);if(!o)continue;if(o.screenId!==n.screenId||o.flagId!==n.flagId){changes.push({level:'major',kind:'experiment',change:'scope',key:id});promote('major')}} const intg=diffNamed('integration',prev.integrations.integrations??[],curr.integrations.integrations??[],'id') for(const [id,n] of intg.newMap){ const o=intg.oldMap.get(id); if(!o) continue for(const prop of ['direction','transport','delivery','ordering','idempotency','terminalAction']){ if((o[prop]??null)!==(n[prop]??null)){changes.push({level:'major',kind:'integration',change:prop,key:id,from:o[prop],to:n[prop]});promote('major')} } if(o.timeoutMs!==n.timeoutMs || JSON.stringify(o.shortRetry)!==JSON.stringify(n.shortRetry) || JSON.stringify(o.longRetry)!==JSON.stringify(n.longRetry) || JSON.stringify(o.circuitBreaker)!==JSON.stringify(n.circuitBreaker)){ changes.push({level:'patch',risk:'resilience-policy',kind:'integration',change:'resilience-policy',key:id});promote('patch') } } const providers=diffNamed('external-provider',prev.providers.providers??[],curr.providers.providers??[],'id') for(const [id,n] of providers.newMap){ const o=providers.oldMap.get(id); if(!o) continue for(const prop of ['purpose','mutationAllowed']){if(JSON.stringify(o[prop]??null)!==JSON.stringify(n[prop]??null)){changes.push({level:'major',kind:'external-provider',change:prop,key:id,from:o[prop],to:n[prop]});promote('major')}} if(JSON.stringify(o.official)!==JSON.stringify(n.official)){changes.push({level:'patch',risk:'provider-contract',kind:'external-provider',change:'official-facts',key:id});promote('patch')} if(JSON.stringify(o.kbxPolicy)!==JSON.stringify(n.kbxPolicy)){changes.push({level:'patch',risk:'resilience-policy',kind:'external-provider',change:'kbx-policy',key:id});promote('patch')} } const datasets=diffNamed('external-dataset',prev.externalData.datasets??[],curr.externalData.datasets??[],'id') for(const [id,n] of datasets.newMap){ const o=datasets.oldMap.get(id); if(!o) continue for(const prop of ['providerId','providerOperationId','canonicalType','normalizer']){ if((o[prop]??null)!==(n[prop]??null)){changes.push({level:'major',kind:'external-dataset',change:prop,key:id,from:o[prop]??null,to:n[prop]??null});promote('major')} } if(o.normalizerVersion!==n.normalizerVersion){changes.push({level:'patch',risk:'data-normalization',kind:'external-dataset',change:'normalizer-version',key:id,from:o.normalizerVersion,to:n.normalizerVersion});promote('patch')} if(JSON.stringify(o.freshness)!==JSON.stringify(n.freshness)){changes.push({level:'patch',risk:'freshness-policy',kind:'external-dataset',change:'freshness-policy',key:id});promote('patch')} if(JSON.stringify(o.snapshot)!==JSON.stringify(n.snapshot)){changes.push({level:'patch',risk:'retention-policy',kind:'external-dataset',change:'snapshot-policy',key:id});promote('patch')} } const configSettings=diffNamed('configuration-setting',prev.configuration.settings??[],curr.configuration.settings??[],'key') for(const [key,n] of configSettings.newMap){ const o=configSettings.oldMap.get(key);if(!o)continue for(const prop of ['type','secret']){ if(JSON.stringify(o[prop]??null)!==JSON.stringify(n[prop]??null)){changes.push({level:'major',kind:'configuration-setting',change:prop,key,from:o[prop]??null,to:n[prop]??null});promote('major')} } if(JSON.stringify(o.allowedValues??[])!==JSON.stringify(n.allowedValues??[])){ const removed=(o.allowedValues??[]).some(x=>!(n.allowedValues??[]).includes(x)) const level=removed?'major':'minor';changes.push({level,kind:'configuration-setting',change:'allowed-values',key});promote(level) } if(JSON.stringify(o.requiredIn??[])!==JSON.stringify(n.requiredIn??[]) || JSON.stringify(o.requiredWhen??null)!==JSON.stringify(n.requiredWhen??null)){ changes.push({level:'minor',risk:'deployment-config',kind:'configuration-setting',change:'requirement',key});promote('minor') } if(o.default!==n.default){changes.push({level:'patch',risk:'deployment-config',kind:'configuration-setting',change:'default',key,from:o.default??null,to:n.default??null});promote('patch')} } const envProfiles=diffNamed('environment-profile',prev.configuration.environments??[],curr.configuration.environments??[],'id') for(const [id,n] of envProfiles.newMap){ const o=envProfiles.oldMap.get(id);if(!o)continue if(JSON.stringify(o)!==JSON.stringify(n)){changes.push({level:'patch',risk:'deployment-policy',kind:'environment-profile',change:'policy',key:id});promote('patch')} } const oldTestScenarios=new Map((prev.testing.scenarios??[]).map(x=>[x.id,x])) const newTestScenarios=new Map((curr.testing.scenarios??[]).map(x=>[x.id,x])) for(const [id] of oldTestScenarios){if(!newTestScenarios.has(id)){changes.push({level:'patch',kind:'test-scenario',change:'removed',key:id});promote('patch')}} for(const [id,n] of newTestScenarios){ const o=oldTestScenarios.get(id) if(!o){changes.push({level:'patch',kind:'test-scenario',change:'added',key:id});promote('patch');continue} const oldShape=JSON.stringify({screenId:o.screenId,kind:o.kind,apiOperations:o.apiOperations,requiredPermissions:o.requiredPermissions,isolation:o.isolation}) const newShape=JSON.stringify({screenId:n.screenId,kind:n.kind,apiOperations:n.apiOperations,requiredPermissions:n.requiredPermissions,isolation:n.isolation}) if(oldShape!==newShape){changes.push({level:'patch',kind:'test-scenario',change:'contract',key:id});promote('patch')} } const declared=release.declaredChangeLevel const ok=levels[declared]>=levels[required] const impact={baseline:release.baseline,contractVersion:release.contractVersion,declaredChangeLevel:declared,requiredChangeLevel:required,compatible:ok,changes} fs.writeFileSync('generated/release-impact.json',JSON.stringify(impact,null,2)+'\n') const md=['# KBX Release Impact','',`- Baseline: ${release.baseline}`,`- Contract version: ${release.contractVersion}`,`- Declared level: ${declared}`,`- Required level: ${required}`,`- Compatible: ${ok?'YES':'NO'}`,'','## Tracked contract changes',''] if(!changes.length)md.push('- Public Screen/Component/Token/Field contract change 없음.') else for(const x of changes)md.push(`- **${x.level}** ${x.kind} ${x.change}: \`${x.key}\`${x.from!==undefined?` (${JSON.stringify(x.from)} → ${JSON.stringify(x.to)})`:''}${x.risk?` — ${x.risk}`:''}`) md.push('','## Governance additions','', '- Canonical permission catalog + Screen/Command/Workflow/API parity', '- Sensitive data policy + audited disclosure', '- AI effective-permission intersection', '- KbxMaskedValue masking UX', '- Permission/Sensitive-policy release compatibility analysis', '- Semantic UX telemetry + Manual Intervention Rate governance', '- Safe UX experiment + controlled rollout + kill-switch governance', '- Deterministic synthetic fixture + executable scenario governance', '- External integration + resilience governance', '- Official-source external provider adapters + provider contract governance', '- External data normalization + provenance + freshness governance', '- Deployment configuration + environment promotion governance') fs.writeFileSync('generated/release-notes.md',md.join('\n')+'\n') if(!ok){console.error(`declared change level ${declared} is lower than required ${required}`);process.exit(1)} console.log(`release impact PASS: required=${required}, declared=${declared}, tracked changes=${changes.length}`)