@msn-control/liftoff 0.9.9 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/DEVELOPER.md +142 -0
  2. package/README.md +26 -18
  3. package/assets/governance/single-maintainer-gitflow/policy.md +13 -11
  4. package/dist/args.js +23 -1
  5. package/dist/args.js.map +1 -1
  6. package/dist/artifact-lifecycle.d.ts +1 -1
  7. package/dist/artifact-lifecycle.js +5 -0
  8. package/dist/artifact-lifecycle.js.map +1 -1
  9. package/dist/catalogs.d.ts +2 -0
  10. package/dist/catalogs.js +13 -1
  11. package/dist/catalogs.js.map +1 -1
  12. package/dist/commands.js +365 -17
  13. package/dist/commands.js.map +1 -1
  14. package/dist/file-system.js +100 -13
  15. package/dist/file-system.js.map +1 -1
  16. package/dist/governance-activation/activation-state.d.ts +32 -0
  17. package/dist/governance-activation/activation-state.js +165 -0
  18. package/dist/governance-activation/activation-state.js.map +1 -0
  19. package/dist/governance-activation/approvals.d.ts +188 -0
  20. package/dist/governance-activation/approvals.js +508 -0
  21. package/dist/governance-activation/approvals.js.map +1 -0
  22. package/dist/governance-activation/canonical-json.d.ts +3 -0
  23. package/dist/governance-activation/canonical-json.js +36 -0
  24. package/dist/governance-activation/canonical-json.js.map +1 -0
  25. package/dist/governance-activation/commands.d.ts +10 -0
  26. package/dist/governance-activation/commands.js +1023 -0
  27. package/dist/governance-activation/commands.js.map +1 -0
  28. package/dist/governance-activation/compatibility.d.ts +68 -0
  29. package/dist/governance-activation/compatibility.js +447 -0
  30. package/dist/governance-activation/compatibility.js.map +1 -0
  31. package/dist/governance-activation/credentials.d.ts +160 -0
  32. package/dist/governance-activation/credentials.js +403 -0
  33. package/dist/governance-activation/credentials.js.map +1 -0
  34. package/dist/governance-activation/doctor.d.ts +10 -0
  35. package/dist/governance-activation/doctor.js +379 -0
  36. package/dist/governance-activation/doctor.js.map +1 -0
  37. package/dist/governance-activation/evidence.d.ts +49 -0
  38. package/dist/governance-activation/evidence.js +246 -0
  39. package/dist/governance-activation/evidence.js.map +1 -0
  40. package/dist/governance-activation/graph.d.ts +397 -0
  41. package/dist/governance-activation/graph.js +441 -0
  42. package/dist/governance-activation/graph.js.map +1 -0
  43. package/dist/governance-activation/identity.d.ts +35 -0
  44. package/dist/governance-activation/identity.js +89 -0
  45. package/dist/governance-activation/identity.js.map +1 -0
  46. package/dist/governance-activation/index.d.ts +18 -0
  47. package/dist/governance-activation/index.js +19 -0
  48. package/dist/governance-activation/index.js.map +1 -0
  49. package/dist/governance-activation/migration.d.ts +44 -0
  50. package/dist/governance-activation/migration.js +268 -0
  51. package/dist/governance-activation/migration.js.map +1 -0
  52. package/dist/governance-activation/readiness.d.ts +23 -0
  53. package/dist/governance-activation/readiness.js +158 -0
  54. package/dist/governance-activation/readiness.js.map +1 -0
  55. package/dist/governance-activation/reconciliation.d.ts +11 -0
  56. package/dist/governance-activation/reconciliation.js +58 -0
  57. package/dist/governance-activation/reconciliation.js.map +1 -0
  58. package/dist/governance-activation/release-integrity.d.ts +11 -0
  59. package/dist/governance-activation/release-integrity.js +49 -0
  60. package/dist/governance-activation/release-integrity.js.map +1 -0
  61. package/dist/governance-activation/seed-lifecycle.d.ts +116 -0
  62. package/dist/governance-activation/seed-lifecycle.js +609 -0
  63. package/dist/governance-activation/seed-lifecycle.js.map +1 -0
  64. package/dist/governance-activation/source-of-truth.d.ts +164 -0
  65. package/dist/governance-activation/source-of-truth.js +986 -0
  66. package/dist/governance-activation/source-of-truth.js.map +1 -0
  67. package/dist/governance-activation/task-projection.d.ts +21 -0
  68. package/dist/governance-activation/task-projection.js +75 -0
  69. package/dist/governance-activation/task-projection.js.map +1 -0
  70. package/dist/governance-activation/transitions.d.ts +163 -0
  71. package/dist/governance-activation/transitions.js +2128 -0
  72. package/dist/governance-activation/transitions.js.map +1 -0
  73. package/dist/governance-activation/type-contract.d.ts +1 -0
  74. package/dist/governance-activation/type-contract.js +27 -0
  75. package/dist/governance-activation/type-contract.js.map +1 -0
  76. package/dist/governance-activation/types.d.ts +390 -0
  77. package/dist/governance-activation/types.js +108 -0
  78. package/dist/governance-activation/types.js.map +1 -0
  79. package/dist/governance-activation/validators.d.ts +17 -0
  80. package/dist/governance-activation/validators.js +1305 -0
  81. package/dist/governance-activation/validators.js.map +1 -0
  82. package/dist/init-filesystem.js +5 -1
  83. package/dist/init-filesystem.js.map +1 -1
  84. package/dist/interactive.js +4 -3
  85. package/dist/interactive.js.map +1 -1
  86. package/dist/planner.js +5 -5
  87. package/dist/planner.js.map +1 -1
  88. package/dist/power-apps-templates.js +14 -1
  89. package/dist/power-apps-templates.js.map +1 -1
  90. package/dist/process-runner.d.ts +3 -0
  91. package/dist/process-runner.js +68 -10
  92. package/dist/process-runner.js.map +1 -1
  93. package/dist/repository-governance.d.ts +13 -3
  94. package/dist/repository-governance.js +357 -51
  95. package/dist/repository-governance.js.map +1 -1
  96. package/dist/telemetry/contract.d.ts +1 -1
  97. package/dist/telemetry/contract.js +6 -0
  98. package/dist/telemetry/contract.js.map +1 -1
  99. package/dist/templates.js +320 -25
  100. package/dist/templates.js.map +1 -1
  101. package/dist/types.d.ts +15 -7
  102. package/docs/cli-reference.md +61 -13
  103. package/docs/configuration-and-manifests.md +43 -16
  104. package/docs/existing-repositories.md +22 -11
  105. package/docs/getting-started.md +46 -10
  106. package/docs/project-structure.md +35 -6
  107. package/docs/repository-governance.md +200 -245
  108. package/docs/safety-and-consent.md +24 -2
  109. package/docs/troubleshooting.md +59 -4
  110. package/docs/workloads.md +1 -1
  111. package/package.json +2 -1
@@ -0,0 +1,986 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { applyProjectFileTransaction, readProjectFile, resolveProjectPath, validateArtifactPathParts } from '../file-system.js';
3
+ import { generatedSeedChangeName } from './seed-lifecycle.js';
4
+ import { canonicalJson, canonicalSha256 } from './canonical-json.js';
5
+ import { activationContractVersion, activationStateSchemaVersion, approvalEnvelopeSchemaVersion, credentialPolicySchemaVersion, evidenceHeaderSchemaVersion, liftoffManifestArtifactVersion, phaseGraphSchemaVersion, supersessionSchemaVersion } from './identity.js';
6
+ import { calculateGraphReconciliation } from './reconciliation.js';
7
+ import { canonicalPhaseGraph, canonicalPhaseGraphHash, currentActivationIdentity, phaseContractDigests } from './graph.js';
8
+ import { evidenceContextForPhase, validateEvidenceFreshness } from './evidence.js';
9
+ import { phaseIds } from './types.js';
10
+ import { validateSupersessionRecord } from './validators.js';
11
+ import { toSafeProjectName } from '../planner.js';
12
+ export const governanceChangeMetadataFileName = 'liftoff-governance.json';
13
+ export const governanceSupersessionPathParts = ['governance', 'supersessions'];
14
+ export const governanceReconciliationPathParts = ['governance', 'reconciliation'];
15
+ export const governanceSourceOfTruthMarker = 'liftoff-governance-source-of-truth';
16
+ export const governanceChangeMetadataSchemaVersion = 1;
17
+ const hex64Pattern = /^[a-f0-9]{64}$/u;
18
+ const isoLikePattern = /^\d{4}-\d{2}-\d{2}T/u;
19
+ const workflowKinds = new Set(['openspec', 'spec-kit']);
20
+ function errorCode(error) {
21
+ if (typeof error !== 'object' || error === null || !('code' in error)) {
22
+ return undefined;
23
+ }
24
+ const code = error.code;
25
+ return typeof code === 'string' ? code : undefined;
26
+ }
27
+ function errorMessage(error) {
28
+ return error instanceof Error ? error.message : String(error);
29
+ }
30
+ function record(value, label) {
31
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
32
+ throw new Error(`${label} must be an object.`);
33
+ }
34
+ return value;
35
+ }
36
+ function exact(value, keys, label) {
37
+ const item = record(value, label);
38
+ for (const key of keys) {
39
+ if (!Object.hasOwn(item, key)) {
40
+ throw new Error(`${label}.${key} is required.`);
41
+ }
42
+ }
43
+ const allowed = new Set(keys);
44
+ for (const key of Object.keys(item)) {
45
+ if (!allowed.has(key)) {
46
+ throw new Error(`${label}.${key} is not allowed.`);
47
+ }
48
+ }
49
+ return item;
50
+ }
51
+ function stringField(item, key, label) {
52
+ const value = item[key];
53
+ if (typeof value !== 'string' || value.length === 0) {
54
+ throw new Error(`${label}.${key} must be a non-empty string.`);
55
+ }
56
+ return value;
57
+ }
58
+ function numberField(item, key, expected, label) {
59
+ const value = item[key];
60
+ if (value !== expected) {
61
+ throw new Error(`${label}.${key} must be ${expected}.`);
62
+ }
63
+ return value;
64
+ }
65
+ function stringArray(value, label) {
66
+ if (!Array.isArray(value)) {
67
+ throw new Error(`${label} must be an array.`);
68
+ }
69
+ return value.map((entry, index) => {
70
+ if (typeof entry !== 'string' || entry.length === 0) {
71
+ throw new Error(`${label}[${index}] must be a non-empty string.`);
72
+ }
73
+ return entry;
74
+ });
75
+ }
76
+ function strictActivationIdentity(value, label) {
77
+ const identity = exact(value, [
78
+ 'liftoffVersion',
79
+ 'manifestArtifactVersion',
80
+ 'policyVersion',
81
+ 'activationContractVersion',
82
+ 'phaseGraphSchemaVersion',
83
+ 'phaseGraphHash',
84
+ 'activationStateSchemaVersion',
85
+ 'evidenceHeaderSchemaVersion',
86
+ 'approvalEnvelopeSchemaVersion',
87
+ 'supersessionSchemaVersion',
88
+ 'credentialPolicySchemaVersion'
89
+ ], label);
90
+ const phaseGraphHash = stringField(identity, 'phaseGraphHash', label);
91
+ if (!hex64Pattern.test(phaseGraphHash)) {
92
+ throw new Error(`${label}.phaseGraphHash must be a SHA-256 hex digest.`);
93
+ }
94
+ return {
95
+ liftoffVersion: stringField(identity, 'liftoffVersion', label),
96
+ manifestArtifactVersion: numberField(identity, 'manifestArtifactVersion', liftoffManifestArtifactVersion, label),
97
+ policyVersion: stringField(identity, 'policyVersion', label),
98
+ activationContractVersion: numberField(identity, 'activationContractVersion', activationContractVersion, label),
99
+ phaseGraphSchemaVersion: numberField(identity, 'phaseGraphSchemaVersion', phaseGraphSchemaVersion, label),
100
+ phaseGraphHash,
101
+ activationStateSchemaVersion: numberField(identity, 'activationStateSchemaVersion', activationStateSchemaVersion, label),
102
+ evidenceHeaderSchemaVersion: numberField(identity, 'evidenceHeaderSchemaVersion', evidenceHeaderSchemaVersion, label),
103
+ approvalEnvelopeSchemaVersion: numberField(identity, 'approvalEnvelopeSchemaVersion', approvalEnvelopeSchemaVersion, label),
104
+ supersessionSchemaVersion: numberField(identity, 'supersessionSchemaVersion', supersessionSchemaVersion, label),
105
+ credentialPolicySchemaVersion: numberField(identity, 'credentialPolicySchemaVersion', credentialPolicySchemaVersion, label)
106
+ };
107
+ }
108
+ function validateCurrentIdentity(identity, label) {
109
+ const issues = [];
110
+ const expected = currentActivationIdentity;
111
+ for (const key of Object.keys(expected)) {
112
+ if (identity[key] !== expected[key]) {
113
+ issues.push(`${label}.${key} expected ${JSON.stringify(expected[key])}, found ${JSON.stringify(identity[key])}.`);
114
+ }
115
+ }
116
+ return issues;
117
+ }
118
+ function strictPhaseTaskMapping(value, label) {
119
+ const mapping = exact(value, ['phaseId', 'taskId', 'marker', 'policy'], label);
120
+ const phaseId = stringField(mapping, 'phaseId', label);
121
+ if (!phaseIds.includes(phaseId)) {
122
+ throw new Error(`${label}.phaseId contains unsupported phase ${phaseId}.`);
123
+ }
124
+ const taskId = stringField(mapping, 'taskId', label);
125
+ const marker = stringField(mapping, 'marker', label);
126
+ if (marker !== `<!-- liftoff-phase: ${phaseId} -->`) {
127
+ throw new Error(`${label}.marker must match its phaseId.`);
128
+ }
129
+ if (mapping.policy !== 'evidence-projection-v1') {
130
+ throw new Error(`${label}.policy must be evidence-projection-v1.`);
131
+ }
132
+ return { phaseId: phaseId, taskId, marker: marker, policy: 'evidence-projection-v1' };
133
+ }
134
+ export function validateGovernanceChangeMetadata(value) {
135
+ const metadata = exact(value, [
136
+ 'schemaVersion',
137
+ 'marker',
138
+ 'changeId',
139
+ 'workflowKind',
140
+ 'activationIdentity',
141
+ 'phaseGraphHash',
142
+ 'baselineSha',
143
+ 'phaseTaskMapping',
144
+ 'currentPolicy',
145
+ 'createdFrom',
146
+ 'acknowledgedAt',
147
+ 'owner'
148
+ ], 'governanceChange');
149
+ numberField(metadata, 'schemaVersion', governanceChangeMetadataSchemaVersion, 'governanceChange');
150
+ if (metadata.marker !== governanceSourceOfTruthMarker) {
151
+ throw new Error(`governanceChange.marker must be ${governanceSourceOfTruthMarker}.`);
152
+ }
153
+ const changeId = stringField(metadata, 'changeId', 'governanceChange');
154
+ const workflowKind = stringField(metadata, 'workflowKind', 'governanceChange');
155
+ if (!workflowKinds.has(workflowKind)) {
156
+ throw new Error(`governanceChange.workflowKind contains unsupported workflow ${workflowKind}.`);
157
+ }
158
+ const activationIdentity = strictActivationIdentity(metadata.activationIdentity, 'governanceChange.activationIdentity');
159
+ const phaseGraphHash = stringField(metadata, 'phaseGraphHash', 'governanceChange');
160
+ if (phaseGraphHash !== activationIdentity.phaseGraphHash) {
161
+ throw new Error('governanceChange.phaseGraphHash must match activationIdentity.phaseGraphHash.');
162
+ }
163
+ const baselineSha = stringField(metadata, 'baselineSha', 'governanceChange');
164
+ if (!hex64Pattern.test(baselineSha)) {
165
+ throw new Error('governanceChange.baselineSha must be a SHA-256 hex digest.');
166
+ }
167
+ if (!Array.isArray(metadata.phaseTaskMapping)) {
168
+ throw new Error('governanceChange.phaseTaskMapping must be an array.');
169
+ }
170
+ const seenPhases = new Set();
171
+ const seenTasks = new Set();
172
+ const phaseTaskMapping = metadata.phaseTaskMapping.map((entry, index) => {
173
+ const mapping = strictPhaseTaskMapping(entry, `governanceChange.phaseTaskMapping[${index}]`);
174
+ if (seenPhases.has(mapping.phaseId)) {
175
+ throw new Error(`governanceChange.phaseTaskMapping contains duplicate phase ${mapping.phaseId}.`);
176
+ }
177
+ if (seenTasks.has(mapping.taskId)) {
178
+ throw new Error(`governanceChange.phaseTaskMapping contains duplicate task ${mapping.taskId}.`);
179
+ }
180
+ seenPhases.add(mapping.phaseId);
181
+ seenTasks.add(mapping.taskId);
182
+ return mapping;
183
+ });
184
+ for (const phaseId of phaseIds) {
185
+ if (!seenPhases.has(phaseId)) {
186
+ throw new Error(`governanceChange.phaseTaskMapping.${phaseId} is required.`);
187
+ }
188
+ }
189
+ const policy = exact(metadata.currentPolicy, [
190
+ 'phaseAuthority',
191
+ 'taskCompletion',
192
+ 'approvalPolicy'
193
+ ], 'governanceChange.currentPolicy');
194
+ if (policy.phaseAuthority !== 'managed-phase-graph') {
195
+ throw new Error('governanceChange.currentPolicy.phaseAuthority must be managed-phase-graph.');
196
+ }
197
+ if (policy.taskCompletion !== 'authoritative-evidence-projection') {
198
+ throw new Error('governanceChange.currentPolicy.taskCompletion must be authoritative-evidence-projection.');
199
+ }
200
+ if (policy.approvalPolicy !== 'approval-envelope-required-for-gated-phases') {
201
+ throw new Error('governanceChange.currentPolicy.approvalPolicy must be approval-envelope-required-for-gated-phases.');
202
+ }
203
+ const createdFrom = exact(metadata.createdFrom, [
204
+ 'kind',
205
+ 'approvedFactDigest',
206
+ 'evidenceIds'
207
+ ], 'governanceChange.createdFrom');
208
+ if (createdFrom.kind !== 'approved-phase-0-facts') {
209
+ throw new Error('governanceChange.createdFrom.kind must be approved-phase-0-facts.');
210
+ }
211
+ const approvedFactDigest = stringField(createdFrom, 'approvedFactDigest', 'governanceChange.createdFrom');
212
+ if (!hex64Pattern.test(approvedFactDigest)) {
213
+ throw new Error('governanceChange.createdFrom.approvedFactDigest must be a SHA-256 hex digest.');
214
+ }
215
+ const acknowledgedAt = stringField(metadata, 'acknowledgedAt', 'governanceChange');
216
+ if (!isoLikePattern.test(acknowledgedAt) || Number.isNaN(Date.parse(acknowledgedAt))) {
217
+ throw new Error('governanceChange.acknowledgedAt must be a valid ISO timestamp.');
218
+ }
219
+ return {
220
+ schemaVersion: governanceChangeMetadataSchemaVersion,
221
+ marker: governanceSourceOfTruthMarker,
222
+ changeId,
223
+ workflowKind: workflowKind,
224
+ activationIdentity,
225
+ phaseGraphHash,
226
+ baselineSha,
227
+ phaseTaskMapping,
228
+ currentPolicy: {
229
+ phaseAuthority: 'managed-phase-graph',
230
+ taskCompletion: 'authoritative-evidence-projection',
231
+ approvalPolicy: 'approval-envelope-required-for-gated-phases'
232
+ },
233
+ createdFrom: {
234
+ kind: 'approved-phase-0-facts',
235
+ approvedFactDigest,
236
+ evidenceIds: stringArray(createdFrom.evidenceIds, 'governanceChange.createdFrom.evidenceIds')
237
+ },
238
+ acknowledgedAt,
239
+ owner: stringField(metadata, 'owner', 'governanceChange')
240
+ };
241
+ }
242
+ export function governancePhaseTaskMappings() {
243
+ return phaseIds.map((phaseId, index) => ({
244
+ phaseId,
245
+ taskId: `${index + 1}.1`,
246
+ marker: `<!-- liftoff-phase: ${phaseId} -->`,
247
+ policy: 'evidence-projection-v1'
248
+ }));
249
+ }
250
+ export function approvedPhase0FactDigest(facts) {
251
+ return canonicalSha256({
252
+ approvedFacts: facts.approvedFacts,
253
+ baselineSha: facts.baselineSha,
254
+ defaultBranch: facts.defaultBranch,
255
+ evidenceIds: facts.evidenceIds,
256
+ repositoryId: facts.repositoryId,
257
+ repositoryName: facts.repositoryName,
258
+ workflowKind: facts.workflowKind
259
+ });
260
+ }
261
+ export function deterministicGovernanceChangeId(facts) {
262
+ const safeProject = toSafeProjectName(facts.projectName);
263
+ const suffix = facts.baselineSha.slice(0, 12);
264
+ return facts.workflowKind === 'openspec'
265
+ ? `governance-${safeProject}-${suffix}`
266
+ : `001-liftoff-governance-${safeProject}-${suffix}`;
267
+ }
268
+ function metadataForFacts(facts, changeId) {
269
+ return validateGovernanceChangeMetadata({
270
+ schemaVersion: governanceChangeMetadataSchemaVersion,
271
+ marker: governanceSourceOfTruthMarker,
272
+ changeId,
273
+ workflowKind: facts.workflowKind,
274
+ activationIdentity: currentActivationIdentity,
275
+ phaseGraphHash: canonicalPhaseGraphHash,
276
+ baselineSha: facts.baselineSha,
277
+ phaseTaskMapping: governancePhaseTaskMappings(),
278
+ currentPolicy: {
279
+ phaseAuthority: 'managed-phase-graph',
280
+ taskCompletion: 'authoritative-evidence-projection',
281
+ approvalPolicy: 'approval-envelope-required-for-gated-phases'
282
+ },
283
+ createdFrom: {
284
+ kind: 'approved-phase-0-facts',
285
+ approvedFactDigest: approvedPhase0FactDigest(facts),
286
+ evidenceIds: facts.evidenceIds
287
+ },
288
+ acknowledgedAt: facts.approvedAt,
289
+ owner: facts.approver
290
+ });
291
+ }
292
+ function approvedFactsMarkdown(facts) {
293
+ if (facts.approvedFacts.length === 0) {
294
+ return '- No additional Phase 0 facts were discovered.\n';
295
+ }
296
+ return facts.approvedFacts
297
+ .map((fact) => `- ${fact.id}: ${fact.value === null ? 'null' : String(fact.value)}`)
298
+ .join('\n') + '\n';
299
+ }
300
+ function openSpecGovernanceSpec(metadata, facts) {
301
+ return `## ADDED Requirements
302
+
303
+ ### Requirement: Liftoff activation follows the installed phase graph
304
+ The project SHALL use the Liftoff governance activation identity acknowledged by
305
+ \`${metadata.changeId}\` and SHALL treat the managed phase graph as the sole
306
+ source of execution order.
307
+
308
+ #### Scenario: Phase evidence is authoritative
309
+ - **WHEN** a phase task is checked or unchecked
310
+ - **THEN** Liftoff reconciles it from current validated evidence for the same repository, baseline SHA, activation identity, graph hash, and phase mapping
311
+ - **AND** prose or checkbox state alone does not authorize a transition
312
+
313
+ #### Scenario: Phase 0 facts are the only creation input
314
+ - **WHEN** this governance change is inspected
315
+ - **THEN** only these approved Phase 0 facts are in scope:
316
+ ${approvedFactsMarkdown(facts).split('\n').filter(Boolean).map((line) => ` ${line}`).join('\n')}
317
+ `;
318
+ }
319
+ function renderOpenSpecFiles(metadata, facts) {
320
+ const idLines = [
321
+ `- Liftoff version: ${metadata.activationIdentity.liftoffVersion}`,
322
+ `- Manifest artifact version: ${metadata.activationIdentity.manifestArtifactVersion}`,
323
+ `- Policy version: ${metadata.activationIdentity.policyVersion}`,
324
+ `- Activation contract version: ${metadata.activationIdentity.activationContractVersion}`,
325
+ `- Phase-graph schema version: ${metadata.activationIdentity.phaseGraphSchemaVersion}`,
326
+ `- Phase-graph hash: ${metadata.phaseGraphHash}`,
327
+ `- Activation-state schema version: ${metadata.activationIdentity.activationStateSchemaVersion}`,
328
+ `- Evidence-header schema version: ${metadata.activationIdentity.evidenceHeaderSchemaVersion}`,
329
+ `- Approval-envelope schema version: ${metadata.activationIdentity.approvalEnvelopeSchemaVersion}`,
330
+ `- Supersession schema version: ${metadata.activationIdentity.supersessionSchemaVersion}`,
331
+ `- Credential-policy schema version: ${metadata.activationIdentity.credentialPolicySchemaVersion}`,
332
+ `- Baseline SHA: ${metadata.baselineSha}`,
333
+ `- Approved fact digest: ${metadata.createdFrom.approvedFactDigest}`
334
+ ].join('\n');
335
+ const taskSections = metadata.phaseTaskMapping.map((mapping) => `## ${mapping.taskId.split('.')[0]}. ${mapping.phaseId}
336
+
337
+ - [ ] ${mapping.taskId} Reconcile \`${mapping.phaseId}\` from validated phase evidence. ${mapping.marker}`).join('\n\n');
338
+ const mappingLines = metadata.phaseTaskMapping.map((mapping) => `- \`${mapping.phaseId}\` -> task \`${mapping.taskId}\` (${mapping.policy})`).join('\n');
339
+ return [
340
+ {
341
+ pathParts: ['openspec', 'changes', metadata.changeId, '.openspec.yaml'],
342
+ content: 'schema: spec-driven\n'
343
+ },
344
+ {
345
+ pathParts: ['openspec', 'changes', metadata.changeId, governanceChangeMetadataFileName],
346
+ content: `${canonicalJson(metadata)}\n`
347
+ },
348
+ {
349
+ pathParts: ['openspec', 'changes', metadata.changeId, 'proposal.md'],
350
+ content: `# Proposal: ${metadata.changeId}
351
+
352
+ ## Why
353
+
354
+ Activate repository governance using one deterministic Liftoff source of truth
355
+ created from approved Phase 0 facts.
356
+
357
+ ## What Changes
358
+
359
+ - Acknowledge the complete compatible activation identity and phase graph hash.
360
+ - Map every Liftoff phase to a task marker that is projected from evidence.
361
+ - Keep implementation, credentials, remote resources, and approvals outside this change until their phase evidence and approval envelopes authorize them.
362
+
363
+ ## Capabilities
364
+
365
+ ### New Capabilities
366
+
367
+ - \`liftoff-governance-activation\`: Evidence-backed repository governance activation for ${facts.repositoryName}.
368
+
369
+ ### Modified Capabilities
370
+
371
+ - None.
372
+
373
+ ## Impact
374
+
375
+ - User-owned governance activation artifacts only.
376
+ - No product behavior, Git, GitHub, Azure, credential, or infrastructure mutation.
377
+ `
378
+ },
379
+ {
380
+ pathParts: ['openspec', 'changes', metadata.changeId, 'design.md'],
381
+ content: `# Design: ${metadata.changeId}
382
+
383
+ ## Context
384
+
385
+ Repository: ${facts.repositoryName}
386
+ Baseline SHA: ${metadata.baselineSha}
387
+ Workflow: ${metadata.workflowKind}
388
+
389
+ ## Activation identity
390
+
391
+ ${idLines}
392
+
393
+ ## Phase mapping and policy
394
+
395
+ The managed phase graph is the execution authority. Task completion is a
396
+ projection of authoritative evidence; approval-gated phases require a validated
397
+ approval envelope.
398
+
399
+ ${mappingLines}
400
+
401
+ ## Approved Phase 0 facts
402
+
403
+ ${approvedFactsMarkdown(facts)}
404
+ `
405
+ },
406
+ {
407
+ pathParts: ['openspec', 'changes', metadata.changeId, 'specs', 'liftoff-governance-activation', 'spec.md'],
408
+ content: openSpecGovernanceSpec(metadata, facts)
409
+ },
410
+ {
411
+ pathParts: ['openspec', 'changes', metadata.changeId, 'tasks.md'],
412
+ content: `${taskSections}
413
+
414
+ `
415
+ }
416
+ ];
417
+ }
418
+ function renderSpecKitFiles(metadata, facts) {
419
+ const base = ['specs', metadata.changeId];
420
+ const taskSections = metadata.phaseTaskMapping.map((mapping) => `## ${mapping.taskId.split('.')[0]}. ${mapping.phaseId}
421
+
422
+ - [ ] ${mapping.taskId} Reconcile \`${mapping.phaseId}\` from validated phase evidence. ${mapping.marker}`).join('\n\n');
423
+ return [
424
+ {
425
+ pathParts: [...base, governanceChangeMetadataFileName],
426
+ content: `${canonicalJson(metadata)}\n`
427
+ },
428
+ {
429
+ pathParts: [...base, 'spec.md'],
430
+ content: `# Feature Specification: Liftoff Governance Activation
431
+
432
+ **Feature Branch**: \`${metadata.changeId}\`
433
+ **Created**: ${metadata.acknowledgedAt}
434
+ **Status**: Draft
435
+ **Input**: Approved Phase 0 facts for ${facts.repositoryName}
436
+
437
+ ## User Scenarios & Testing
438
+
439
+ ### Primary User Story
440
+
441
+ As the repository owner, I need governance activation to resume from one
442
+ deterministic Liftoff source of truth using validated evidence rather than
443
+ checkboxes or prose.
444
+
445
+ ## Requirements
446
+
447
+ - **REQ-001**: Acknowledge activation identity \`${metadata.createdFrom.approvedFactDigest}\`, graph hash \`${metadata.phaseGraphHash}\`, and baseline \`${metadata.baselineSha}\`.
448
+ - **REQ-002**: Use the managed phase graph as the only execution-order authority.
449
+ - **REQ-003**: Project task completion from authoritative phase evidence and validated approval envelopes only.
450
+
451
+ ## Approved Phase 0 Facts
452
+
453
+ ${approvedFactsMarkdown(facts)}
454
+ `
455
+ },
456
+ {
457
+ pathParts: [...base, 'plan.md'],
458
+ content: `# Implementation Plan: Liftoff Governance Activation
459
+
460
+ **Branch**: \`${metadata.changeId}\` | **Date**: ${metadata.acknowledgedAt} | **Spec**: ./spec.md
461
+ **Input**: Approved Phase 0 facts from Liftoff governance status.
462
+
463
+ ## Technical Context
464
+
465
+ Activation identity: \`${metadata.activationIdentity.liftoffVersion}/${metadata.activationIdentity.policyVersion}/${metadata.activationIdentity.activationContractVersion}\`
466
+ Phase graph hash: \`${metadata.phaseGraphHash}\`
467
+ Baseline SHA: \`${metadata.baselineSha}\`
468
+
469
+ ## Constitution Check
470
+
471
+ No Git, GitHub, Azure, credential, product, or infrastructure mutation is
472
+ authorized by this plan. Later transition adapters must consume validated
473
+ approval envelopes and evidence.
474
+ `
475
+ },
476
+ {
477
+ pathParts: [...base, 'tasks.md'],
478
+ content: `# Tasks: Liftoff Governance Activation
479
+
480
+ ${taskSections}
481
+ `
482
+ }
483
+ ];
484
+ }
485
+ export function renderGovernanceChangeWritePlan(facts) {
486
+ if (!hex64Pattern.test(facts.baselineSha)) {
487
+ throw new Error('Approved Phase 0 baselineSha must be a SHA-256 hex digest.');
488
+ }
489
+ if (!workflowKinds.has(facts.workflowKind)) {
490
+ throw new Error(`Unsupported governance workflow kind ${facts.workflowKind}.`);
491
+ }
492
+ if (!isoLikePattern.test(facts.approvedAt) || Number.isNaN(Date.parse(facts.approvedAt))) {
493
+ throw new Error('Approved Phase 0 approvedAt must be a valid ISO timestamp.');
494
+ }
495
+ const changeId = deterministicGovernanceChangeId(facts);
496
+ const metadata = metadataForFacts(facts, changeId);
497
+ const files = facts.workflowKind === 'openspec'
498
+ ? renderOpenSpecFiles(metadata, facts)
499
+ : renderSpecKitFiles(metadata, facts);
500
+ return {
501
+ changeId,
502
+ workflowKind: facts.workflowKind,
503
+ metadata,
504
+ files,
505
+ transactionalWrite: {
506
+ functionName: 'writeGovernanceChangeArtifacts',
507
+ writesAllFilesOrThrows: true
508
+ }
509
+ };
510
+ }
511
+ export async function writeGovernanceChangeArtifacts(projectRoot, plan) {
512
+ const validated = validateGovernanceChangeMetadata(plan.metadata);
513
+ if (validated.changeId !== plan.changeId || validated.workflowKind !== plan.workflowKind) {
514
+ throw new Error('Governance change write plan metadata does not match plan identity.');
515
+ }
516
+ for (const file of plan.files) {
517
+ validateArtifactPathParts([...file.pathParts], 'Governance change artifact path');
518
+ }
519
+ for (const file of plan.files) {
520
+ const existing = await readProjectFile(projectRoot, [...file.pathParts]);
521
+ if (existing !== undefined) {
522
+ throw new Error(`Refusing to overwrite existing governance change artifact ${file.pathParts.join('/')}.`);
523
+ }
524
+ }
525
+ try {
526
+ await applyProjectFileTransaction(projectRoot, plan.files.map((file) => ({
527
+ type: 'write',
528
+ pathParts: [...file.pathParts],
529
+ content: file.content
530
+ })));
531
+ }
532
+ catch (error) {
533
+ throw new Error(`Unable to write governance change artifacts transactionally: ${errorMessage(error)}`);
534
+ }
535
+ }
536
+ async function directoryEntries(projectRoot, pathParts) {
537
+ const directory = await resolveProjectPath(projectRoot, validateArtifactPathParts([...pathParts], 'Governance directory path'));
538
+ try {
539
+ const entries = await readdir(directory, { withFileTypes: true });
540
+ return entries
541
+ .filter((entry) => entry.isDirectory())
542
+ .map((entry) => entry.name)
543
+ .sort((left, right) => left.localeCompare(right, 'en'));
544
+ }
545
+ catch (error) {
546
+ if (errorCode(error) === 'ENOENT') {
547
+ return [];
548
+ }
549
+ throw new Error(`Unable to inspect ${pathParts.join('/')}: ${errorMessage(error)}.`);
550
+ }
551
+ }
552
+ async function activeSeedBlockers(projectRoot, manifest) {
553
+ if (manifest.project.specWorkflow !== 'openspec') {
554
+ return [];
555
+ }
556
+ const expectedSeed = generatedSeedChangeName(manifest);
557
+ const active = await directoryEntries(projectRoot, ['openspec', 'changes']);
558
+ const activeSeeds = active.filter((entry) => entry.startsWith('bootstrap-') && entry !== 'archive');
559
+ if (activeSeeds.length === 0) {
560
+ return [];
561
+ }
562
+ if (activeSeeds.includes(expectedSeed)) {
563
+ return [`Generated OpenSpec seed ${expectedSeed} is still active; archive it before governance creation.`];
564
+ }
565
+ return [`Active bootstrap seed changes overlap governance setup: ${activeSeeds.join(', ')}.`];
566
+ }
567
+ async function readMetadataCandidate(projectRoot, workflowKind, changeId, pathParts, requireMetadata) {
568
+ const metadataBytes = await readProjectFile(projectRoot, [...pathParts, governanceChangeMetadataFileName]);
569
+ if (metadataBytes === undefined) {
570
+ return {
571
+ changeId,
572
+ workflowKind,
573
+ pathParts,
574
+ status: requireMetadata ? 'missing-metadata' : 'invalid-metadata',
575
+ issues: [`${[...pathParts, governanceChangeMetadataFileName].join('/')} is missing.`]
576
+ };
577
+ }
578
+ let parsed;
579
+ try {
580
+ parsed = JSON.parse(metadataBytes.toString('utf8'));
581
+ }
582
+ catch (error) {
583
+ return {
584
+ changeId,
585
+ workflowKind,
586
+ pathParts,
587
+ status: 'invalid-metadata',
588
+ issues: [`Unable to parse governance metadata: ${errorMessage(error)}`]
589
+ };
590
+ }
591
+ try {
592
+ const metadata = validateGovernanceChangeMetadata(parsed);
593
+ const issues = [];
594
+ if (metadata.changeId !== changeId) {
595
+ issues.push(`metadata changeId ${metadata.changeId} does not match path ${changeId}.`);
596
+ }
597
+ if (metadata.workflowKind !== workflowKind) {
598
+ issues.push(`metadata workflowKind ${metadata.workflowKind} does not match ${workflowKind}.`);
599
+ }
600
+ issues.push(...validateCurrentIdentity(metadata.activationIdentity, 'activationIdentity'));
601
+ if (metadata.phaseGraphHash !== canonicalPhaseGraphHash) {
602
+ issues.push(`phaseGraphHash expected ${canonicalPhaseGraphHash}, found ${metadata.phaseGraphHash}.`);
603
+ }
604
+ return {
605
+ changeId,
606
+ workflowKind,
607
+ pathParts,
608
+ status: issues.length === 0 ? 'compatible' : 'identity-mismatch',
609
+ metadata,
610
+ issues
611
+ };
612
+ }
613
+ catch (error) {
614
+ return {
615
+ changeId,
616
+ workflowKind,
617
+ pathParts,
618
+ status: 'invalid-metadata',
619
+ issues: [errorMessage(error)]
620
+ };
621
+ }
622
+ }
623
+ async function activeGovernanceCandidates(projectRoot, manifest, state) {
624
+ const candidates = [];
625
+ if (manifest.project.specWorkflow === 'openspec') {
626
+ for (const changeId of await directoryEntries(projectRoot, ['openspec', 'changes'])) {
627
+ if (changeId === 'archive' || changeId.startsWith('bootstrap-')) {
628
+ continue;
629
+ }
630
+ const pathParts = ['openspec', 'changes', changeId];
631
+ const metadataBytes = await readProjectFile(projectRoot, [...pathParts, governanceChangeMetadataFileName]);
632
+ if (metadataBytes !== undefined || state.activeChange?.id === changeId) {
633
+ candidates.push(await readMetadataCandidate(projectRoot, 'openspec', changeId, pathParts, state.activeChange?.id === changeId));
634
+ }
635
+ }
636
+ }
637
+ else {
638
+ for (const changeId of await directoryEntries(projectRoot, ['specs'])) {
639
+ const pathParts = ['specs', changeId];
640
+ const metadataBytes = await readProjectFile(projectRoot, [...pathParts, governanceChangeMetadataFileName]);
641
+ if (metadataBytes !== undefined || state.activeChange?.id === changeId) {
642
+ candidates.push(await readMetadataCandidate(projectRoot, 'spec-kit', changeId, pathParts, state.activeChange?.id === changeId));
643
+ }
644
+ }
645
+ }
646
+ if (state.activeChange && !candidates.some((candidate) => candidate.changeId === state.activeChange?.id)) {
647
+ const pathParts = state.activeChange.kind === 'openspec'
648
+ ? ['openspec', 'changes', state.activeChange.id]
649
+ : ['specs', state.activeChange.id];
650
+ candidates.push({
651
+ changeId: state.activeChange.id,
652
+ workflowKind: state.activeChange.kind === 'openspec' ? 'openspec' : 'spec-kit',
653
+ pathParts,
654
+ status: 'missing-metadata',
655
+ issues: [`Activation state activeChange ${state.activeChange.id} has no inspectable governance metadata.`]
656
+ });
657
+ }
658
+ return candidates.sort((left, right) => left.changeId.localeCompare(right.changeId, 'en'));
659
+ }
660
+ async function readSupersessionRecords(projectRoot, candidates) {
661
+ const directory = await resolveProjectPath(projectRoot, [...governanceSupersessionPathParts]);
662
+ let entries;
663
+ try {
664
+ entries = await readdir(directory, { withFileTypes: true });
665
+ }
666
+ catch (error) {
667
+ if (errorCode(error) === 'ENOENT') {
668
+ return { records: [], invalidRecords: [], selectedChangeId: null, issues: [] };
669
+ }
670
+ throw new Error(`Unable to inspect ${governanceSupersessionPathParts.join('/')}: ${errorMessage(error)}.`);
671
+ }
672
+ const byId = new Map(candidates.map((candidate) => [candidate.changeId, candidate]));
673
+ const records = [];
674
+ const invalidRecords = [];
675
+ const issues = [];
676
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, 'en'))) {
677
+ if (!entry.isFile() || !entry.name.endsWith('.json')) {
678
+ continue;
679
+ }
680
+ const pathParts = [...governanceSupersessionPathParts, entry.name];
681
+ const bytes = await readProjectFile(projectRoot, pathParts);
682
+ if (bytes === undefined) {
683
+ throw new Error(`${pathParts.join('/')} disappeared during inspection.`);
684
+ }
685
+ try {
686
+ const parsed = JSON.parse(bytes.toString('utf8'));
687
+ const record = validateSupersessionRecord(parsed);
688
+ const superseded = byId.get(record.supersededChangeId);
689
+ const superseding = byId.get(record.supersedingChangeId);
690
+ if (!superseded || !superseding) {
691
+ issues.push(`${pathParts.join('/')} references a change that is not active and valid in this inspection.`);
692
+ }
693
+ else if (superseded.status !== 'compatible' || superseding.status !== 'compatible') {
694
+ issues.push(`${pathParts.join('/')} references a change without a compatible identity.`);
695
+ }
696
+ else {
697
+ records.push(record);
698
+ }
699
+ }
700
+ catch (error) {
701
+ invalidRecords.push(`${pathParts.join('/')}: ${errorMessage(error)}`);
702
+ }
703
+ }
704
+ const compatible = candidates.filter((candidate) => candidate.status === 'compatible');
705
+ if (invalidRecords.length > 0 || issues.length > 0) {
706
+ return {
707
+ records,
708
+ invalidRecords,
709
+ selectedChangeId: null,
710
+ issues
711
+ };
712
+ }
713
+ const selectable = compatible.filter((candidate) => compatible
714
+ .filter((other) => other.changeId !== candidate.changeId)
715
+ .every((other) => records.some((record) => record.supersededChangeId === other.changeId &&
716
+ record.supersedingChangeId === candidate.changeId)));
717
+ return {
718
+ records,
719
+ invalidRecords,
720
+ selectedChangeId: selectable.length === 1 ? selectable[0].changeId : null,
721
+ issues
722
+ };
723
+ }
724
+ export function buildApprovedPhase0FactsFromState(manifest, state, evidence) {
725
+ const validPhase0 = evidence
726
+ .filter((record) => record.header.phaseId === 'phase-0-complete')
727
+ .flatMap((record) => {
728
+ const context = evidenceContextForPhase('phase-0-complete', {
729
+ repositoryId: state.repository.id,
730
+ identity: state.identity,
731
+ phaseGraphHash: state.identity.phaseGraphHash,
732
+ baselineSha: record.header.baselineSha,
733
+ inputDigest: record.header.inputDigest
734
+ });
735
+ const validation = validateEvidenceFreshness(record, context);
736
+ return validation.valid ? [validation.record] : [];
737
+ })
738
+ .sort((left, right) => {
739
+ const time = right.producedAtEpochMs - left.producedAtEpochMs;
740
+ if (time !== 0) {
741
+ return time;
742
+ }
743
+ const evidenceId = right.evidenceId.localeCompare(left.evidenceId, 'en');
744
+ return evidenceId === 0 ? right.headerDigest.localeCompare(left.headerDigest, 'en') : evidenceId;
745
+ });
746
+ const phase0 = validPhase0[0];
747
+ if (!phase0) {
748
+ return undefined;
749
+ }
750
+ const latestResults = new Set(validPhase0
751
+ .filter((record) => record.producedAtEpochMs === phase0.producedAtEpochMs)
752
+ .map((record) => record.header.result));
753
+ if (latestResults.size !== 1 || phase0.header.result !== 'verified') {
754
+ return undefined;
755
+ }
756
+ return {
757
+ projectName: manifest.project.name,
758
+ repositoryId: state.repository.id,
759
+ repositoryName: state.repository.name,
760
+ defaultBranch: state.repository.defaultBranch,
761
+ workflowKind: manifest.project.specWorkflow,
762
+ baselineSha: phase0.header.baselineSha,
763
+ evidenceIds: [phase0.evidenceId],
764
+ approvedFacts: [
765
+ { id: 'repositoryId', value: state.repository.id },
766
+ { id: 'repositoryName', value: state.repository.name },
767
+ { id: 'defaultBranch', value: state.repository.defaultBranch },
768
+ { id: 'specWorkflow', value: manifest.project.specWorkflow }
769
+ ],
770
+ approvedAt: phase0.header.producedAt,
771
+ approver: phase0.header.producer
772
+ };
773
+ }
774
+ function createPreview(manifest, state, evidence) {
775
+ const facts = buildApprovedPhase0FactsFromState(manifest, state, evidence);
776
+ if (facts) {
777
+ return {
778
+ status: 'ready',
779
+ reason: 'Verified Phase 0 evidence is available for deterministic governance change rendering.',
780
+ changeId: deterministicGovernanceChangeId(facts),
781
+ workflowKind: facts.workflowKind,
782
+ requiredFacts: []
783
+ };
784
+ }
785
+ const fallback = {
786
+ projectName: manifest.project.name,
787
+ baselineSha: '0'.repeat(64),
788
+ workflowKind: manifest.project.specWorkflow
789
+ };
790
+ return {
791
+ status: 'blocked',
792
+ reason: 'Verified Phase 0 evidence is required before rendering user-owned governance artifacts.',
793
+ changeId: deterministicGovernanceChangeId(fallback),
794
+ workflowKind: manifest.project.specWorkflow,
795
+ requiredFacts: ['phase-0-complete verified evidence', 'repository id', 'repository name', 'default branch', 'baseline SHA']
796
+ };
797
+ }
798
+ export function reconcileActiveGovernanceChange(input) {
799
+ const toGraph = input.toGraph ?? canonicalPhaseGraph;
800
+ const fromGraph = input.fromGraph ?? toGraph;
801
+ const toIdentity = input.toIdentity ?? currentActivationIdentity;
802
+ if (canonicalJson(input.metadata.activationIdentity) === canonicalJson(toIdentity)) {
803
+ return {
804
+ status: 'not-required',
805
+ approvalRequired: false,
806
+ preservedPhaseIds: phaseIds,
807
+ invalidPhaseIds: [],
808
+ issues: []
809
+ };
810
+ }
811
+ if (input.metadata.activationIdentity.policyVersion !== toIdentity.policyVersion ||
812
+ input.metadata.activationIdentity.activationContractVersion !== toIdentity.activationContractVersion ||
813
+ input.metadata.activationIdentity.phaseGraphSchemaVersion !== toIdentity.phaseGraphSchemaVersion) {
814
+ return {
815
+ status: 'blocked',
816
+ approvalRequired: false,
817
+ issues: ['Active governance change identity is incompatible with the installed policy, contract, or graph schema.'],
818
+ preservedPhaseIds: [],
819
+ invalidPhaseIds: phaseIds
820
+ };
821
+ }
822
+ const fromHash = input.metadata.activationIdentity.phaseGraphHash;
823
+ const toHash = toIdentity.phaseGraphHash;
824
+ if (fromHash !== toHash && input.fromGraph === undefined) {
825
+ return {
826
+ status: 'blocked',
827
+ approvalRequired: false,
828
+ issues: [
829
+ 'Active governance change records a different phase-graph hash, but no recognized source graph or explicit mapping was provided for phase contract reconciliation.'
830
+ ],
831
+ preservedPhaseIds: [],
832
+ invalidPhaseIds: phaseIds
833
+ };
834
+ }
835
+ const fromDigests = phaseContractDigests(fromGraph);
836
+ const toDigests = phaseContractDigests(toGraph);
837
+ const record = {
838
+ schemaVersion: currentActivationIdentity.activationStateSchemaVersion,
839
+ fromGraphHash: fromHash,
840
+ toGraphHash: toHash,
841
+ fromIdentity: input.metadata.activationIdentity,
842
+ toIdentity,
843
+ phaseMappings: phaseIds.map((phaseId) => ({
844
+ phaseId,
845
+ fromContractDigest: fromDigests[phaseId],
846
+ toContractDigest: toDigests[phaseId],
847
+ preserveEvidence: fromDigests[phaseId] === toDigests[phaseId]
848
+ })),
849
+ reconciledAt: input.reconciledAt ?? new Date().toISOString(),
850
+ producer: 'liftoff-governance-source-of-truth'
851
+ };
852
+ try {
853
+ const reconciliation = calculateGraphReconciliation(record, {
854
+ fromGraph,
855
+ toGraph,
856
+ recognizedGraphHashes: input.recognizedGraphHashes ?? new Set([toHash])
857
+ });
858
+ const invalid = new Set(reconciliation.invalidPhaseIds);
859
+ return {
860
+ status: 'required',
861
+ approvalRequired: true,
862
+ fromIdentity: input.metadata.activationIdentity,
863
+ toIdentity,
864
+ record: reconciliation.record,
865
+ preservedPhaseIds: reconciliation.preservedPhaseIds,
866
+ invalidPhaseIds: reconciliation.invalidPhaseIds,
867
+ evidenceToReview: input.evidence
868
+ .filter((entry) => invalid.has(entry.header.phaseId))
869
+ .map((entry) => entry.evidenceId),
870
+ issues: ['Active governance change must acknowledge the installed activation identity before execution can continue.']
871
+ };
872
+ }
873
+ catch (error) {
874
+ return {
875
+ status: 'blocked',
876
+ approvalRequired: false,
877
+ issues: [errorMessage(error)],
878
+ preservedPhaseIds: [],
879
+ invalidPhaseIds: phaseIds
880
+ };
881
+ }
882
+ }
883
+ export function stateWithSelectedActiveChange(state, selected) {
884
+ if (selected.status !== 'compatible') {
885
+ throw new Error(`Cannot record incompatible active change ${selected.changeId}.`);
886
+ }
887
+ return {
888
+ ...state,
889
+ activeChange: {
890
+ id: selected.changeId,
891
+ kind: selected.workflowKind === 'openspec' ? 'openspec' : 'spec-kit'
892
+ }
893
+ };
894
+ }
895
+ export async function inspectGovernanceSourceOfTruth(input) {
896
+ const seedBlockers = await activeSeedBlockers(input.projectRoot, input.manifest);
897
+ const candidates = await activeGovernanceCandidates(input.projectRoot, input.manifest, input.state);
898
+ if (seedBlockers.length > 0) {
899
+ return {
900
+ status: 'seed-blocked',
901
+ seedChangeId: generatedSeedChangeName(input.manifest),
902
+ blockers: seedBlockers,
903
+ selected: null,
904
+ candidates
905
+ };
906
+ }
907
+ const invalid = candidates.filter((candidate) => candidate.status === 'invalid-metadata' || candidate.status === 'missing-metadata');
908
+ if (invalid.length > 0) {
909
+ return {
910
+ status: 'incompatible',
911
+ selected: null,
912
+ candidates,
913
+ blockers: invalid.flatMap((candidate) => candidate.issues.map((issue) => `${candidate.changeId}: ${issue}`)),
914
+ reconciliation: {
915
+ status: 'blocked',
916
+ approvalRequired: false,
917
+ issues: invalid.flatMap((candidate) => candidate.issues),
918
+ preservedPhaseIds: [],
919
+ invalidPhaseIds: phaseIds
920
+ }
921
+ };
922
+ }
923
+ const mismatched = candidates.filter((candidate) => candidate.status === 'identity-mismatch');
924
+ if (mismatched.length > 0) {
925
+ return {
926
+ status: 'incompatible',
927
+ selected: null,
928
+ candidates,
929
+ blockers: mismatched.flatMap((candidate) => candidate.issues.map((issue) => `${candidate.changeId}: ${issue}`)),
930
+ reconciliation: mismatched[0]?.metadata
931
+ ? reconcileActiveGovernanceChange({ metadata: mismatched[0].metadata, evidence: input.evidence })
932
+ : {
933
+ status: 'blocked',
934
+ approvalRequired: false,
935
+ issues: ['Active governance change lacks valid metadata.'],
936
+ preservedPhaseIds: [],
937
+ invalidPhaseIds: phaseIds
938
+ }
939
+ };
940
+ }
941
+ const compatible = candidates.filter((candidate) => candidate.status === 'compatible');
942
+ if (compatible.length === 0) {
943
+ return {
944
+ status: 'none',
945
+ selected: null,
946
+ candidates,
947
+ createPlan: createPreview(input.manifest, input.state, input.evidence)
948
+ };
949
+ }
950
+ const supersession = await readSupersessionRecords(input.projectRoot, compatible);
951
+ if (compatible.length > 1) {
952
+ if (supersession.selectedChangeId) {
953
+ const selected = compatible.find((candidate) => candidate.changeId === supersession.selectedChangeId);
954
+ return {
955
+ status: 'selected',
956
+ selected,
957
+ candidates,
958
+ recordActiveChangeOnNextMutation: input.state.activeChange?.id !== selected.changeId,
959
+ reconciliation: reconcileActiveGovernanceChange({ metadata: selected.metadata, evidence: input.evidence }),
960
+ supersession
961
+ };
962
+ }
963
+ return {
964
+ status: 'ambiguous',
965
+ selected: null,
966
+ candidates,
967
+ blockers: [
968
+ 'Multiple compatible governance changes overlap activation scope.',
969
+ 'Add explicit schema-valid supersession records; Liftoff never selects by timestamp or name.',
970
+ ...supersession.invalidRecords,
971
+ ...supersession.issues
972
+ ],
973
+ supersession
974
+ };
975
+ }
976
+ const selected = compatible[0];
977
+ return {
978
+ status: 'selected',
979
+ selected,
980
+ candidates,
981
+ recordActiveChangeOnNextMutation: input.state.activeChange?.id !== selected.changeId,
982
+ reconciliation: reconcileActiveGovernanceChange({ metadata: selected.metadata, evidence: input.evidence }),
983
+ supersession
984
+ };
985
+ }
986
+ //# sourceMappingURL=source-of-truth.js.map