@msn-control/liftoff 0.9.8 → 0.10.0

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 (107) hide show
  1. package/DEVELOPER.md +142 -0
  2. package/README.md +26 -18
  3. package/assets/governance/single-maintainer-gitflow/policy.md +62 -13
  4. package/dist/args.js +21 -0
  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/commands.js +355 -13
  10. package/dist/commands.js.map +1 -1
  11. package/dist/file-system.js +100 -13
  12. package/dist/file-system.js.map +1 -1
  13. package/dist/governance-activation/activation-state.d.ts +32 -0
  14. package/dist/governance-activation/activation-state.js +165 -0
  15. package/dist/governance-activation/activation-state.js.map +1 -0
  16. package/dist/governance-activation/approvals.d.ts +188 -0
  17. package/dist/governance-activation/approvals.js +508 -0
  18. package/dist/governance-activation/approvals.js.map +1 -0
  19. package/dist/governance-activation/canonical-json.d.ts +3 -0
  20. package/dist/governance-activation/canonical-json.js +36 -0
  21. package/dist/governance-activation/canonical-json.js.map +1 -0
  22. package/dist/governance-activation/commands.d.ts +10 -0
  23. package/dist/governance-activation/commands.js +1023 -0
  24. package/dist/governance-activation/commands.js.map +1 -0
  25. package/dist/governance-activation/compatibility.d.ts +68 -0
  26. package/dist/governance-activation/compatibility.js +447 -0
  27. package/dist/governance-activation/compatibility.js.map +1 -0
  28. package/dist/governance-activation/credentials.d.ts +160 -0
  29. package/dist/governance-activation/credentials.js +403 -0
  30. package/dist/governance-activation/credentials.js.map +1 -0
  31. package/dist/governance-activation/doctor.d.ts +10 -0
  32. package/dist/governance-activation/doctor.js +379 -0
  33. package/dist/governance-activation/doctor.js.map +1 -0
  34. package/dist/governance-activation/evidence.d.ts +49 -0
  35. package/dist/governance-activation/evidence.js +246 -0
  36. package/dist/governance-activation/evidence.js.map +1 -0
  37. package/dist/governance-activation/graph.d.ts +397 -0
  38. package/dist/governance-activation/graph.js +441 -0
  39. package/dist/governance-activation/graph.js.map +1 -0
  40. package/dist/governance-activation/identity.d.ts +35 -0
  41. package/dist/governance-activation/identity.js +89 -0
  42. package/dist/governance-activation/identity.js.map +1 -0
  43. package/dist/governance-activation/index.d.ts +18 -0
  44. package/dist/governance-activation/index.js +19 -0
  45. package/dist/governance-activation/index.js.map +1 -0
  46. package/dist/governance-activation/migration.d.ts +44 -0
  47. package/dist/governance-activation/migration.js +268 -0
  48. package/dist/governance-activation/migration.js.map +1 -0
  49. package/dist/governance-activation/readiness.d.ts +23 -0
  50. package/dist/governance-activation/readiness.js +158 -0
  51. package/dist/governance-activation/readiness.js.map +1 -0
  52. package/dist/governance-activation/reconciliation.d.ts +11 -0
  53. package/dist/governance-activation/reconciliation.js +58 -0
  54. package/dist/governance-activation/reconciliation.js.map +1 -0
  55. package/dist/governance-activation/release-integrity.d.ts +11 -0
  56. package/dist/governance-activation/release-integrity.js +49 -0
  57. package/dist/governance-activation/release-integrity.js.map +1 -0
  58. package/dist/governance-activation/seed-lifecycle.d.ts +116 -0
  59. package/dist/governance-activation/seed-lifecycle.js +609 -0
  60. package/dist/governance-activation/seed-lifecycle.js.map +1 -0
  61. package/dist/governance-activation/source-of-truth.d.ts +164 -0
  62. package/dist/governance-activation/source-of-truth.js +986 -0
  63. package/dist/governance-activation/source-of-truth.js.map +1 -0
  64. package/dist/governance-activation/task-projection.d.ts +21 -0
  65. package/dist/governance-activation/task-projection.js +75 -0
  66. package/dist/governance-activation/task-projection.js.map +1 -0
  67. package/dist/governance-activation/transitions.d.ts +163 -0
  68. package/dist/governance-activation/transitions.js +2128 -0
  69. package/dist/governance-activation/transitions.js.map +1 -0
  70. package/dist/governance-activation/type-contract.d.ts +1 -0
  71. package/dist/governance-activation/type-contract.js +27 -0
  72. package/dist/governance-activation/type-contract.js.map +1 -0
  73. package/dist/governance-activation/types.d.ts +390 -0
  74. package/dist/governance-activation/types.js +108 -0
  75. package/dist/governance-activation/types.js.map +1 -0
  76. package/dist/governance-activation/validators.d.ts +17 -0
  77. package/dist/governance-activation/validators.js +1305 -0
  78. package/dist/governance-activation/validators.js.map +1 -0
  79. package/dist/init-filesystem.js +5 -1
  80. package/dist/init-filesystem.js.map +1 -1
  81. package/dist/planner.js +3 -3
  82. package/dist/planner.js.map +1 -1
  83. package/dist/power-apps-templates.js +14 -1
  84. package/dist/power-apps-templates.js.map +1 -1
  85. package/dist/process-runner.d.ts +3 -0
  86. package/dist/process-runner.js +68 -10
  87. package/dist/process-runner.js.map +1 -1
  88. package/dist/repository-governance.d.ts +13 -3
  89. package/dist/repository-governance.js +385 -52
  90. package/dist/repository-governance.js.map +1 -1
  91. package/dist/telemetry/contract.d.ts +1 -1
  92. package/dist/telemetry/contract.js +6 -0
  93. package/dist/telemetry/contract.js.map +1 -1
  94. package/dist/templates.js +320 -25
  95. package/dist/templates.js.map +1 -1
  96. package/dist/types.d.ts +14 -6
  97. package/docs/cli-reference.md +60 -12
  98. package/docs/configuration-and-manifests.md +40 -16
  99. package/docs/existing-repositories.md +22 -11
  100. package/docs/getting-started.md +46 -10
  101. package/docs/prerequisites.md +5 -0
  102. package/docs/project-structure.md +34 -5
  103. package/docs/repository-governance.md +200 -203
  104. package/docs/safety-and-consent.md +29 -2
  105. package/docs/troubleshooting.md +74 -4
  106. package/docs/workloads.md +1 -1
  107. package/package.json +2 -1
@@ -0,0 +1,1023 @@
1
+ import { access, readdir } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { readBooleanFlag, readStringFlag } from '../args.js';
4
+ import { findProjectRoot, loadManifest, readProjectFile, resolveProjectPath, validateArtifactPathParts } from '../file-system.js';
5
+ import { governanceArtifactPaths, validateGovernancePolicy } from '../repository-governance.js';
6
+ import { loadActivationState } from './activation-state.js';
7
+ import { canonicalSha256 } from './canonical-json.js';
8
+ import { canonicalApprovalEnvelopeHash, evaluateApprovalForTransitionPlan, transitionPlanForPhase } from './approvals.js';
9
+ import { buildPatEnrollmentGuidance, canonicalCredentialRepository, credentialPolicyPathParts, detectCredentialLeaks, runnerPreflightPermissions, validateCredentialPolicyUsage } from './credentials.js';
10
+ import { evidenceContextForPhase, requiredLiveReadbackProviders, selectLatestPhaseEvidence } from './evidence.js';
11
+ import { canonicalPhaseGraph, canonicalPhaseGraphHash, currentActivationIdentity, phaseContractDigests } from './graph.js';
12
+ import { governanceActivationPolicyVersion } from './identity.js';
13
+ import { calculatePhaseReadiness } from './readiness.js';
14
+ import { projectOpenSpecTaskCheckboxes } from './task-projection.js';
15
+ import { inspectGovernanceSourceOfTruth } from './source-of-truth.js';
16
+ import { executeApplyNext, previewApplyNext } from './transitions.js';
17
+ import { phaseIds } from './types.js';
18
+ import { validateApprovalEnvelope, validateEvidenceHeader, validateLiveReadbackProof, validateManagedPhaseGraph, validateCredentialPolicy } from './validators.js';
19
+ const governanceSubcommands = new Set([
20
+ 'status',
21
+ 'plan',
22
+ 'apply-next',
23
+ 'resume',
24
+ 'verify'
25
+ ]);
26
+ const managedPhaseGraphPathParts = ['.liftoff', 'governance', 'phase-graph.json'];
27
+ const approvalDirectoryPathParts = ['governance', 'approvals'];
28
+ const evidenceDirectoryPathParts = ['governance', 'evidence'];
29
+ const terminalEvidenceStates = new Set([
30
+ 'verified',
31
+ 'failed',
32
+ 'inapplicable',
33
+ 'retained',
34
+ 'disposed'
35
+ ]);
36
+ function errorMessage(error) {
37
+ return error instanceof Error ? error.message : String(error);
38
+ }
39
+ function errorCode(error) {
40
+ if (typeof error !== 'object' || error === null || !('code' in error)) {
41
+ return undefined;
42
+ }
43
+ const code = error.code;
44
+ return typeof code === 'string' ? code : undefined;
45
+ }
46
+ function json(presentation, value) {
47
+ presentation.rawStdout(`${JSON.stringify(value, null, 2)}\n`);
48
+ }
49
+ function parseGovernanceSubcommand(parsed) {
50
+ if (!parsed.subcommand || !governanceSubcommands.has(parsed.subcommand)) {
51
+ return undefined;
52
+ }
53
+ return parsed.subcommand;
54
+ }
55
+ async function resolveGovernanceProjectRoot(parsed, context) {
56
+ const positionalProject = parsed.positional[0];
57
+ const flagProject = readStringFlag(parsed.flags, 'project');
58
+ if (positionalProject && flagProject) {
59
+ throw new Error('Provide a project path either positionally or with --project, not both.');
60
+ }
61
+ const explicit = positionalProject ?? flagProject;
62
+ const start = explicit ? path.resolve(context.cwd, explicit) : context.cwd;
63
+ return await findProjectRoot(start);
64
+ }
65
+ function projectRootError(start) {
66
+ return {
67
+ message: `No liftoff.manifest.json found in ${start} or any parent directory.`,
68
+ remedy: 'Run this command inside a Liftoff project or provide its path explicitly.'
69
+ };
70
+ }
71
+ function emptyPhaseState(now) {
72
+ const phases = {};
73
+ for (const phaseId of phaseIds) {
74
+ phases[phaseId] = {
75
+ state: 'pending',
76
+ updatedAt: now,
77
+ evidence: [],
78
+ approvals: [],
79
+ blockers: []
80
+ };
81
+ }
82
+ return phases;
83
+ }
84
+ function notStartedState(manifest) {
85
+ const now = '1970-01-01T00:00:00.000Z';
86
+ return {
87
+ schemaVersion: currentActivationIdentity.activationStateSchemaVersion,
88
+ identity: currentActivationIdentity,
89
+ repository: {
90
+ id: `local:${manifest.project.name}`,
91
+ name: manifest.project.name,
92
+ defaultBranch: 'develop'
93
+ },
94
+ activeChange: null,
95
+ applicability: {
96
+ statePath: 'none',
97
+ privateStagingDast: false,
98
+ credentialRequired: false
99
+ },
100
+ phases: emptyPhaseState(now),
101
+ createdAt: now,
102
+ updatedAt: now
103
+ };
104
+ }
105
+ async function loadGovernanceGraph(projectRoot) {
106
+ const managed = await readProjectFile(projectRoot, [...managedPhaseGraphPathParts]);
107
+ if (managed === undefined) {
108
+ const graph = validateManagedPhaseGraph(canonicalPhaseGraph);
109
+ return { source: 'packaged', graph, hash: canonicalPhaseGraphHash };
110
+ }
111
+ let parsed;
112
+ try {
113
+ parsed = JSON.parse(managed.toString('utf8'));
114
+ }
115
+ catch (error) {
116
+ throw new Error(`Unable to parse ${managedPhaseGraphPathParts.join('/')}: ${errorMessage(error)}`);
117
+ }
118
+ const graph = validateManagedPhaseGraph(parsed);
119
+ const hash = canonicalSha256(graph);
120
+ if (hash !== canonicalPhaseGraphHash || hash !== currentActivationIdentity.phaseGraphHash) {
121
+ throw new Error(`${managedPhaseGraphPathParts.join('/')} identity drift: expected graph hash ${canonicalPhaseGraphHash}, found ${hash}.`);
122
+ }
123
+ return { source: 'managed', graph, hash };
124
+ }
125
+ async function readJsonFiles(projectRoot, directoryPathParts, label) {
126
+ const directory = await resolveProjectPath(projectRoot, validateArtifactPathParts([...directoryPathParts], `${label} directory path`));
127
+ let entries;
128
+ try {
129
+ entries = await readdir(directory, { withFileTypes: true });
130
+ }
131
+ catch (error) {
132
+ if (errorCode(error) === 'ENOENT') {
133
+ return [];
134
+ }
135
+ throw new Error(`Unable to read ${directoryPathParts.join('/')}: ${errorMessage(error)}`);
136
+ }
137
+ const values = [];
138
+ for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name, 'en'))) {
139
+ if (!entry.name.endsWith('.json')) {
140
+ continue;
141
+ }
142
+ if (!entry.isFile()) {
143
+ throw new Error(`${directoryPathParts.join('/')}/${entry.name} must be a regular JSON file.`);
144
+ }
145
+ const pathParts = validateArtifactPathParts([...directoryPathParts, entry.name], `${label} file path`);
146
+ const bytes = await readProjectFile(projectRoot, pathParts);
147
+ if (bytes === undefined) {
148
+ throw new Error(`${pathParts.join('/')} disappeared during governance inspection.`);
149
+ }
150
+ try {
151
+ values.push({ name: entry.name, value: JSON.parse(bytes.toString('utf8')) });
152
+ }
153
+ catch (error) {
154
+ throw new Error(`Unable to parse ${pathParts.join('/')}: ${errorMessage(error)}`);
155
+ }
156
+ }
157
+ return values;
158
+ }
159
+ async function loadApprovals(projectRoot, identity) {
160
+ const entries = await readJsonFiles(projectRoot, approvalDirectoryPathParts, 'Approval');
161
+ return entries.map((entry) => {
162
+ try {
163
+ return validateApprovalEnvelope(entry.value, { expectedIdentity: identity });
164
+ }
165
+ catch (error) {
166
+ throw new Error(`Invalid ${approvalDirectoryPathParts.join('/')}/${entry.name}: ${errorMessage(error)}`);
167
+ }
168
+ });
169
+ }
170
+ function asRecord(value, pathLabel) {
171
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
172
+ throw new Error(`${pathLabel} must be a JSON object.`);
173
+ }
174
+ return value;
175
+ }
176
+ function validateEvidenceRecord(value, evidenceId, pathLabel) {
177
+ const record = asRecord(value, pathLabel);
178
+ if (Object.hasOwn(record, 'header')) {
179
+ const allowed = new Set(['evidenceId', 'header', 'liveReadback', 'payload']);
180
+ for (const key of Object.keys(record)) {
181
+ if (!allowed.has(key)) {
182
+ throw new Error(`${pathLabel}.${key} is not allowed.`);
183
+ }
184
+ }
185
+ if (typeof record.evidenceId !== 'string' || record.evidenceId.length === 0) {
186
+ throw new Error(`${pathLabel}.evidenceId must be a non-empty string.`);
187
+ }
188
+ const liveReadback = record.liveReadback === undefined
189
+ ? undefined
190
+ : validateLiveReadbackArray(record.liveReadback, `${pathLabel}.liveReadback`);
191
+ return {
192
+ evidenceId: record.evidenceId,
193
+ header: validateEvidenceHeader(record.header),
194
+ ...(liveReadback ? { liveReadback } : {}),
195
+ ...(Object.hasOwn(record, 'payload') ? { payload: record.payload } : {})
196
+ };
197
+ }
198
+ return {
199
+ evidenceId,
200
+ header: validateEvidenceHeader(value)
201
+ };
202
+ }
203
+ function validateLiveReadbackArray(value, pathLabel) {
204
+ if (!Array.isArray(value)) {
205
+ throw new Error(`${pathLabel} must be an array.`);
206
+ }
207
+ return value.map((entry, index) => {
208
+ try {
209
+ return validateLiveReadbackProof(entry);
210
+ }
211
+ catch (error) {
212
+ throw new Error(`${pathLabel}[${index}]: ${errorMessage(error)}`);
213
+ }
214
+ });
215
+ }
216
+ async function loadEvidence(projectRoot) {
217
+ const entries = await readJsonFiles(projectRoot, evidenceDirectoryPathParts, 'Evidence');
218
+ return entries.map((entry) => {
219
+ try {
220
+ const evidenceId = entry.name.replace(/\.json$/u, '');
221
+ return validateEvidenceRecord(entry.value, evidenceId, `${evidenceDirectoryPathParts.join('/')}/${entry.name}`);
222
+ }
223
+ catch (error) {
224
+ throw new Error(`Invalid ${evidenceDirectoryPathParts.join('/')}/${entry.name}: ${errorMessage(error)}`);
225
+ }
226
+ });
227
+ }
228
+ function repositoryFromState(state) {
229
+ const repositoryName = state.repository.name.includes('/')
230
+ ? state.repository.name.split('/').at(-1)
231
+ : state.repository.name;
232
+ const owner = state.repository.name.includes('/')
233
+ ? state.repository.name.split('/')[0]
234
+ : 'local';
235
+ return canonicalCredentialRepository({
236
+ id: state.repository.id,
237
+ owner,
238
+ name: repositoryName
239
+ });
240
+ }
241
+ async function inspectCredentialPolicy(projectRoot, state) {
242
+ const pathLabel = credentialPolicyPathParts.join('/');
243
+ if (!state.applicability.credentialRequired) {
244
+ return {
245
+ applicable: false,
246
+ readOnly: true,
247
+ path: pathLabel,
248
+ status: 'not-applicable',
249
+ ready: false,
250
+ guidance: null,
251
+ policy: null,
252
+ issues: []
253
+ };
254
+ }
255
+ const repository = repositoryFromState(state);
256
+ const guidance = buildPatEnrollmentGuidance({ repository });
257
+ const bytes = await readProjectFile(projectRoot, [...credentialPolicyPathParts]);
258
+ if (bytes === undefined) {
259
+ return {
260
+ applicable: true,
261
+ readOnly: true,
262
+ path: pathLabel,
263
+ status: 'missing',
264
+ ready: false,
265
+ guidance,
266
+ policy: null,
267
+ issues: [`${pathLabel} is missing; deterministic PAT enrollment is required if no selected-repository App is available.`]
268
+ };
269
+ }
270
+ const text = bytes.toString('utf8');
271
+ const leaks = detectCredentialLeaks([{ source: 'imported-evidence', label: pathLabel, text }]);
272
+ if (leaks.status === 'compromised') {
273
+ return {
274
+ applicable: true,
275
+ readOnly: true,
276
+ path: pathLabel,
277
+ status: 'compromised',
278
+ ready: false,
279
+ guidance,
280
+ policy: null,
281
+ issues: leaks.guidance
282
+ };
283
+ }
284
+ let parsed;
285
+ try {
286
+ parsed = JSON.parse(text);
287
+ }
288
+ catch (error) {
289
+ return {
290
+ applicable: true,
291
+ readOnly: true,
292
+ path: pathLabel,
293
+ status: 'invalid',
294
+ ready: false,
295
+ guidance,
296
+ policy: null,
297
+ issues: [`Unable to parse ${pathLabel}: ${errorMessage(error)}`]
298
+ };
299
+ }
300
+ try {
301
+ const policy = validateCredentialPolicy(parsed);
302
+ const usage = validateCredentialPolicyUsage(policy, {
303
+ repository: policy.repository,
304
+ permissions: runnerPreflightPermissions(),
305
+ references: policy.allowedWorkflows.flatMap((entry) => entry.jobs.map((job) => ({ workflowPath: entry.path, job }))),
306
+ forwardsCredential: false,
307
+ verifiedReadbackDigest: policy.proof.readbackDigest
308
+ });
309
+ return {
310
+ applicable: true,
311
+ readOnly: true,
312
+ path: pathLabel,
313
+ status: usage.ready ? 'valid' : 'not-ready',
314
+ ready: usage.ready,
315
+ guidance: usage.ready ? null : guidance,
316
+ policy,
317
+ issues: usage.issues
318
+ };
319
+ }
320
+ catch (error) {
321
+ return {
322
+ applicable: true,
323
+ readOnly: true,
324
+ path: pathLabel,
325
+ status: 'invalid',
326
+ ready: false,
327
+ guidance,
328
+ policy: null,
329
+ issues: [errorMessage(error)]
330
+ };
331
+ }
332
+ }
333
+ function phaseMap(graph) {
334
+ return Object.fromEntries(graph.phases.map((phase) => [phase.id, phase]));
335
+ }
336
+ function buildEvidenceContexts(graph, state, now) {
337
+ const digests = phaseContractDigests(graph);
338
+ return Object.fromEntries(graph.phases.map((phase) => {
339
+ const context = evidenceContextForPhase(phase.id, {
340
+ repositoryId: state.repository.id,
341
+ identity: state.identity,
342
+ phaseGraphHash: state.identity.phaseGraphHash,
343
+ now,
344
+ liveReadbackProviders: requiredLiveReadbackProviders(phase)
345
+ });
346
+ return [phase.id, {
347
+ ...context,
348
+ phaseContractDigest: digests[phase.id]
349
+ }];
350
+ }));
351
+ }
352
+ function freshnessEntry(phase, selection) {
353
+ return {
354
+ phaseId: phase.id,
355
+ status: selection.selected ? 'fresh' : selection.issues.length > 0 ? 'stale' : 'missing',
356
+ selectedEvidenceId: selection.selected?.evidenceId ?? null,
357
+ selectedResult: selection.selected?.header.result ?? null,
358
+ requiresLiveReadback: phase.evidence.liveReadbackProviders.length > 0,
359
+ liveReadbackProviders: phase.evidence.liveReadbackProviders,
360
+ issues: selection.issues.map((issue) => `${issue.evidenceId ? `${issue.evidenceId}: ` : ''}${issue.message}`)
361
+ };
362
+ }
363
+ function buildEvidenceFreshness(graph, evidence, contexts) {
364
+ return Object.fromEntries(graph.phases.map((phase) => {
365
+ const records = evidence.filter((record) => record.header.phaseId === phase.id);
366
+ const selection = selectLatestPhaseEvidence(records, contexts[phase.id]);
367
+ return [phase.id, freshnessEntry(phase, selection)];
368
+ }));
369
+ }
370
+ async function inspectGovernance(projectRoot) {
371
+ const manifest = await loadManifest(projectRoot);
372
+ const graph = await loadGovernanceGraph(projectRoot);
373
+ await assertPolicyIdentity(projectRoot, manifest);
374
+ const loadedState = await loadActivationState(projectRoot);
375
+ const state = loadedState?.state ?? notStartedState(manifest);
376
+ if (state.identity.phaseGraphHash !== graph.hash) {
377
+ throw new Error(`Activation state graph hash ${state.identity.phaseGraphHash} does not match loaded graph hash ${graph.hash}.`);
378
+ }
379
+ if (state.identity.policyVersion !== governanceActivationPolicyVersion) {
380
+ throw new Error(`Activation state policy version ${state.identity.policyVersion} does not match supported policy ${governanceActivationPolicyVersion}.`);
381
+ }
382
+ const approvals = await loadApprovals(projectRoot, state.identity);
383
+ const evidence = await loadEvidence(projectRoot);
384
+ const sourceOfTruth = await inspectGovernanceSourceOfTruth({
385
+ projectRoot,
386
+ manifest,
387
+ state,
388
+ evidence
389
+ });
390
+ const now = new Date();
391
+ const credential = await inspectCredentialPolicy(projectRoot, state);
392
+ const contexts = buildEvidenceContexts(graph.graph, state, now);
393
+ const evidenceFreshness = buildEvidenceFreshness(graph.graph, evidence, contexts);
394
+ const readiness = calculatePhaseReadiness({
395
+ graph: graph.graph,
396
+ state,
397
+ approvals,
398
+ evidence,
399
+ transitionContexts: contexts,
400
+ now
401
+ });
402
+ return {
403
+ projectRoot,
404
+ manifest,
405
+ graph,
406
+ loadedState,
407
+ state,
408
+ stateSource: loadedState ? 'user' : 'not-started',
409
+ approvals,
410
+ evidence,
411
+ contexts,
412
+ evidenceFreshness,
413
+ readiness,
414
+ sourceOfTruth,
415
+ credential
416
+ };
417
+ }
418
+ async function assertPolicyIdentity(projectRoot, manifest) {
419
+ if (manifest.governance.profile === 'none' || manifest.governance.profile === 'unspecified') {
420
+ return;
421
+ }
422
+ if (manifest.governance.policyVersion !== governanceActivationPolicyVersion) {
423
+ throw new Error(`Manifest governance policyVersion ${manifest.governance.policyVersion ?? 'missing'} does not match ${governanceActivationPolicyVersion}.`);
424
+ }
425
+ const bytes = await readProjectFile(projectRoot, [...governanceArtifactPaths.policy]);
426
+ if (bytes === undefined) {
427
+ throw new Error(`${governanceArtifactPaths.policy.join('/')} is missing.`);
428
+ }
429
+ validateGovernancePolicy(bytes.toString('utf8'));
430
+ }
431
+ function approvalStatus(approval, now = new Date()) {
432
+ return Date.parse(approval.expiresAt) > now.getTime() ? 'valid' : 'expired';
433
+ }
434
+ function approvalEvaluationForPhase(phase, inspection) {
435
+ const plan = transitionPlanForPhase(phase, inspection.state, inspection.contexts[phase.id].transition, inspection.projectRoot);
436
+ return evaluateApprovalForTransitionPlan(plan, inspection.approvals);
437
+ }
438
+ function approvalEvaluationJson(evaluation) {
439
+ return {
440
+ questionKind: evaluation.questionKind,
441
+ approvalRequired: evaluation.approvalRequired,
442
+ status: evaluation.status,
443
+ envelopeId: evaluation.envelopeId,
444
+ envelopeHash: evaluation.envelopeHash,
445
+ reasons: evaluation.reasons,
446
+ expansionReasons: evaluation.expansionReasons
447
+ };
448
+ }
449
+ function statusJson(inspection, command) {
450
+ const blockers = phaseIds.flatMap((phaseId) => inspection.readiness.phases[phaseId].blockers.map((message) => ({ phaseId, message })));
451
+ return {
452
+ schemaVersion: 1,
453
+ command: `governance ${command}`,
454
+ projectRoot: inspection.projectRoot,
455
+ readOnly: command !== 'apply-next',
456
+ stateSource: inspection.stateSource,
457
+ activationIdentity: inspection.state.identity,
458
+ graphHash: inspection.graph.hash,
459
+ graph: {
460
+ source: inspection.graph.source,
461
+ hash: inspection.graph.hash,
462
+ schemaVersion: inspection.graph.graph.schemaVersion
463
+ },
464
+ activeChange: inspection.state.activeChange,
465
+ activeSourceOfTruth: inspection.sourceOfTruth,
466
+ credential: inspection.credential,
467
+ phases: inspection.graph.graph.phases.map((phase) => ({
468
+ id: phase.id,
469
+ label: phase.label,
470
+ state: inspection.readiness.phases[phase.id].state,
471
+ storedState: inspection.state.phases[phase.id].state,
472
+ blockers: inspection.readiness.phases[phase.id].blockers,
473
+ evidence: {
474
+ schema: phase.evidence.schema,
475
+ required: phase.evidence.required,
476
+ freshness: inspection.evidenceFreshness[phase.id]
477
+ },
478
+ approvalGate: phase.approvalGate,
479
+ approval: approvalEvaluationJson(approvalEvaluationForPhase(phase, inspection)),
480
+ allowedMutations: phase.allowedMutations
481
+ })),
482
+ nextReadyPhase: inspection.readiness.nextReadyPhase,
483
+ blockers,
484
+ approvals: inspection.approvals.map((approval) => ({
485
+ id: approval.id,
486
+ phaseId: approval.phaseId,
487
+ gateKind: approval.gateKind,
488
+ status: approvalStatus(approval),
489
+ envelopeHash: canonicalApprovalEnvelopeHash(approval),
490
+ expiresAt: approval.expiresAt
491
+ })),
492
+ evidenceFreshness: phaseIds.map((phaseId) => inspection.evidenceFreshness[phaseId])
493
+ };
494
+ }
495
+ function renderStatusHuman(inspection, command) {
496
+ const presentation = inspectionPresentation(inspection);
497
+ presentation.commandIdentity(`governance ${command}`, 'Deterministic activation status');
498
+ presentation.definitions('Activation identity', [
499
+ { label: 'Project', value: inspection.projectRoot },
500
+ { label: 'State', value: inspection.stateSource },
501
+ { label: 'Policy', value: inspection.state.identity.policyVersion },
502
+ { label: 'Contract', value: String(inspection.state.identity.activationContractVersion) },
503
+ { label: 'Graph hash', value: inspection.graph.hash },
504
+ { label: 'Active change', value: inspection.state.activeChange?.id ?? 'none' },
505
+ {
506
+ label: 'Active source',
507
+ value: inspection.sourceOfTruth.status === 'selected'
508
+ ? inspection.sourceOfTruth.selected.changeId
509
+ : inspection.sourceOfTruth.status
510
+ }
511
+ ]);
512
+ if (inspection.sourceOfTruth.status === 'seed-blocked') {
513
+ presentation.status('error', 'Seed blocker', inspection.sourceOfTruth.blockers.join('; '));
514
+ }
515
+ else if (inspection.sourceOfTruth.status === 'ambiguous' || inspection.sourceOfTruth.status === 'incompatible') {
516
+ presentation.status('error', 'Active source blocked', inspection.sourceOfTruth.blockers.join('; '));
517
+ }
518
+ else if (inspection.sourceOfTruth.status === 'selected') {
519
+ presentation.status(inspection.sourceOfTruth.reconciliation.status === 'not-required' ? 'success' : 'pending', 'Source acknowledgment', inspection.sourceOfTruth.reconciliation.status);
520
+ }
521
+ else {
522
+ presentation.status('pending', 'Governance change plan', inspection.sourceOfTruth.createPlan.reason);
523
+ }
524
+ if (inspection.credential.applicable) {
525
+ presentation.status(inspection.credential.ready ? 'success' : inspection.credential.status === 'compromised' ? 'error' : 'pending', 'Credential policy', inspection.credential.ready
526
+ ? 'credential-ready metadata has verified payload-free readback'
527
+ : inspection.credential.issues[0] ?? 'deterministic credential enrollment required');
528
+ }
529
+ const next = inspection.readiness.nextReadyPhase ?? 'none';
530
+ presentation.status(next === 'none' ? 'info' : 'pending', 'Next ready phase', next);
531
+ if (inspection.readiness.nextReadyPhase) {
532
+ const phase = phaseMap(inspection.graph.graph)[inspection.readiness.nextReadyPhase];
533
+ const approval = approvalEvaluationForPhase(phase, inspection);
534
+ presentation.status(approval.approvalRequired ? 'pending' : 'success', 'Approval', `${approval.questionKind ?? 'none'}; ${approval.status}; ${approval.reasons.join('; ')}`);
535
+ }
536
+ const blockerRows = phaseIds
537
+ .filter((phaseId) => inspection.readiness.phases[phaseId].blockers.length > 0)
538
+ .slice(0, 6)
539
+ .map((phaseId) => [
540
+ phaseId,
541
+ inspection.readiness.phases[phaseId].state,
542
+ inspection.readiness.phases[phaseId].blockers[0] ?? ''
543
+ ]);
544
+ if (blockerRows.length > 0) {
545
+ presentation.table('Current blockers', ['Phase', 'State', 'Reason'], blockerRows);
546
+ }
547
+ }
548
+ function inspectionPresentation(inspection) {
549
+ return inspection.presentation;
550
+ }
551
+ function planJson(inspection) {
552
+ const ready = inspection.graph.graph.phases
553
+ .filter((phase) => inspection.readiness.phases[phase.id].state === 'ready')
554
+ .map((phase) => planPhase(phase, inspection));
555
+ const blocked = inspection.graph.graph.phases
556
+ .filter((phase) => inspection.readiness.phases[phase.id].state === 'blocked')
557
+ .map((phase) => ({
558
+ ...planPhase(phase, inspection),
559
+ blockers: inspection.readiness.phases[phase.id].blockers
560
+ }));
561
+ return {
562
+ schemaVersion: 1,
563
+ command: 'governance plan',
564
+ projectRoot: inspection.projectRoot,
565
+ readOnly: true,
566
+ noWrites: true,
567
+ activationIdentity: inspection.state.identity,
568
+ graphHash: inspection.graph.hash,
569
+ activeChange: inspection.state.activeChange,
570
+ activeSourceOfTruth: inspection.sourceOfTruth,
571
+ credential: inspection.credential,
572
+ readyPhases: ready,
573
+ blockedPhases: blocked
574
+ };
575
+ }
576
+ function planPhase(phase, inspection) {
577
+ return {
578
+ id: phase.id,
579
+ label: phase.label,
580
+ requiredEvidence: {
581
+ schema: phase.evidence.schema,
582
+ required: phase.evidence.required,
583
+ liveReadbackProviders: phase.evidence.liveReadbackProviders
584
+ },
585
+ approvalGate: phase.approvalGate,
586
+ approval: approvalEvaluationJson(approvalEvaluationForPhase(phase, inspection)),
587
+ permittedMutations: phase.allowedMutations,
588
+ costEnvelope: costEnvelope(phase),
589
+ evidenceFreshness: inspection.evidenceFreshness[phase.id],
590
+ ...(phase.id === 'credential-ready' && inspection.credential.applicable
591
+ ? { credential: inspection.credential }
592
+ : {})
593
+ };
594
+ }
595
+ function costEnvelope(phase) {
596
+ const relevant = phase.approvalGate.kind === 'infrastructure-cost' ||
597
+ phase.allowedMutations.remote.some((entry) => entry.startsWith('azure-'));
598
+ return {
599
+ relevant,
600
+ gate: phase.approvalGate.kind,
601
+ reason: relevant
602
+ ? 'Infrastructure approval may constrain resource classes, destinations, and cost ceilings.'
603
+ : 'No infrastructure cost envelope is required for this phase.'
604
+ };
605
+ }
606
+ function renderPlanHuman(inspection, presentation) {
607
+ presentation.commandIdentity('governance plan', 'Read-only activation transition plan');
608
+ const ready = inspection.graph.graph.phases.filter((phase) => inspection.readiness.phases[phase.id].state === 'ready');
609
+ const blocked = inspection.graph.graph.phases.filter((phase) => inspection.readiness.phases[phase.id].state === 'blocked');
610
+ presentation.status('info', 'Read-only', 'No files, remotes, credentials, or cloud resources are changed.');
611
+ presentation.table('Ready phases', ['Phase', 'Evidence', 'Question', 'Approval', 'Mutations'], ready.map((phase) => {
612
+ const approval = approvalEvaluationForPhase(phase, inspection);
613
+ return [
614
+ phase.id,
615
+ phase.evidence.schema,
616
+ approval.questionKind ?? 'none',
617
+ phase.approvalGate.required ? phase.approvalGate.kind : 'none',
618
+ `local=${phase.allowedMutations.local.join(',')} remote=${phase.allowedMutations.remote.join(',')}`
619
+ ];
620
+ }));
621
+ presentation.table('Blocked phases', ['Phase', 'Reason'], blocked.slice(0, 12).map((phase) => [
622
+ phase.id,
623
+ inspection.readiness.phases[phase.id].blockers.join('; ')
624
+ ]));
625
+ }
626
+ function validatePolicyIdentity(inspection) {
627
+ const issues = [];
628
+ if (inspection.manifest.governance.profile !== 'none' && inspection.manifest.governance.profile !== 'unspecified') {
629
+ if (inspection.manifest.governance.policyVersion !== governanceActivationPolicyVersion) {
630
+ issues.push(`Manifest governance policyVersion ${inspection.manifest.governance.policyVersion ?? 'missing'} does not match ${governanceActivationPolicyVersion}.`);
631
+ }
632
+ }
633
+ return { id: 'manifest-policy-identity', status: issues.length === 0 ? 'passed' : 'failed', issues };
634
+ }
635
+ async function validateManagedPolicy(projectRoot, manifest) {
636
+ if (manifest.governance.profile === 'none' || manifest.governance.profile === 'unspecified') {
637
+ return { id: 'managed-policy', status: 'skipped', issues: [] };
638
+ }
639
+ const bytes = await readProjectFile(projectRoot, [...governanceArtifactPaths.policy]);
640
+ const issues = [];
641
+ if (bytes === undefined) {
642
+ issues.push(`${governanceArtifactPaths.policy.join('/')} is missing.`);
643
+ }
644
+ else {
645
+ try {
646
+ validateGovernancePolicy(bytes.toString('utf8'));
647
+ }
648
+ catch (error) {
649
+ issues.push(errorMessage(error));
650
+ }
651
+ }
652
+ return { id: 'managed-policy', status: issues.length === 0 ? 'passed' : 'failed', issues };
653
+ }
654
+ function validateStateEvidence(inspection) {
655
+ const issues = [];
656
+ if (inspection.stateSource === 'not-started') {
657
+ return { id: 'state-evidence', status: 'passed', issues };
658
+ }
659
+ for (const phaseId of phaseIds) {
660
+ const stored = inspection.state.phases[phaseId];
661
+ if (!terminalEvidenceStates.has(stored.state)) {
662
+ continue;
663
+ }
664
+ const records = inspection.evidence.filter((record) => record.header.phaseId === phaseId);
665
+ const current = selectLatestPhaseEvidence(records, inspection.contexts[phaseId]);
666
+ if (!current.selected || current.selected.header.result !== stored.state) {
667
+ issues.push(`Phase ${phaseId} is stored as ${stored.state} but has no current matching authoritative evidence.`);
668
+ }
669
+ }
670
+ return { id: 'state-evidence', status: issues.length === 0 ? 'passed' : 'failed', issues };
671
+ }
672
+ function validateEvidenceFreshnessCheck(inspection) {
673
+ const issues = phaseIds.flatMap((phaseId) => {
674
+ const freshness = inspection.evidenceFreshness[phaseId];
675
+ return freshness.status === 'fresh'
676
+ ? []
677
+ : freshness.issues.map((issue) => `${phaseId}: ${issue}`);
678
+ });
679
+ return { id: 'evidence-freshness', status: issues.length === 0 ? 'passed' : 'failed', issues };
680
+ }
681
+ function validateReadinessCheck(inspection) {
682
+ if (inspection.readiness.identityCompatible) {
683
+ return { id: 'readiness', status: 'passed', issues: [] };
684
+ }
685
+ return {
686
+ id: 'readiness',
687
+ status: 'failed',
688
+ issues: [inspection.readiness.identityBlocker ?? 'Activation identity is incompatible.']
689
+ };
690
+ }
691
+ async function activeTaskProjectionCheck(inspection) {
692
+ const activeChange = inspection.state.activeChange;
693
+ if (!activeChange || activeChange.kind !== 'openspec') {
694
+ return { id: 'task-projection', status: 'skipped', issues: [] };
695
+ }
696
+ const pathParts = validateArtifactPathParts(['openspec', 'changes', activeChange.id, 'tasks.md'], 'Active OpenSpec task path');
697
+ const bytes = await readProjectFile(inspection.projectRoot, pathParts);
698
+ if (bytes === undefined) {
699
+ return { id: 'task-projection', status: 'skipped', issues: [] };
700
+ }
701
+ const markdown = bytes.toString('utf8');
702
+ const mappings = extractPhaseTaskMappings(markdown);
703
+ if (mappings.length === 0) {
704
+ return { id: 'task-projection', status: 'skipped', issues: [] };
705
+ }
706
+ const projection = projectOpenSpecTaskCheckboxes(markdown, mappings, inspection.readiness.phases);
707
+ const issues = projection.changes.map((change) => `Task ${change.taskId} for ${change.phaseId} is ${change.fromChecked ? 'checked' : 'unchecked'} but authoritative phase state is ${change.state}.`);
708
+ return { id: 'task-projection', status: issues.length === 0 ? 'passed' : 'failed', issues };
709
+ }
710
+ function extractPhaseTaskMappings(markdown) {
711
+ const phaseIdSet = new Set(phaseIds);
712
+ const mappings = [];
713
+ const pattern = /^\s*[-*]\s+\[[ xX]\]\s+(\S+).*<!--\s*liftoff-phase:\s*([a-z0-9-]+)\s*-->/u;
714
+ for (const line of markdown.split(/\r?\n/u)) {
715
+ const match = line.match(pattern);
716
+ if (!match) {
717
+ continue;
718
+ }
719
+ const phaseId = match[2];
720
+ if (!phaseIdSet.has(phaseId)) {
721
+ throw new Error(`Task projection references unknown phase ${phaseId}.`);
722
+ }
723
+ mappings.push({ phaseId: phaseId, taskId: match[1] });
724
+ }
725
+ return mappings;
726
+ }
727
+ function activeChangeIdentityCheck(inspection) {
728
+ const activeChange = inspection.state.activeChange;
729
+ if (!activeChange) {
730
+ return { id: 'active-change-identity', status: 'skipped', issues: [] };
731
+ }
732
+ const issues = [];
733
+ try {
734
+ validateArtifactPathParts(activeChange.kind === 'openspec'
735
+ ? ['openspec', 'changes', activeChange.id]
736
+ : ['specs', activeChange.id], 'Active change path');
737
+ }
738
+ catch (error) {
739
+ issues.push(errorMessage(error));
740
+ }
741
+ return { id: 'active-change-identity', status: issues.length === 0 ? 'passed' : 'failed', issues };
742
+ }
743
+ function liveReadbackCheck(inspection) {
744
+ const issues = inspection.graph.graph.phases.flatMap((phase) => {
745
+ if (phase.evidence.liveReadbackProviders.length === 0) {
746
+ return [];
747
+ }
748
+ const freshness = inspection.evidenceFreshness[phase.id];
749
+ return freshness.status === 'fresh'
750
+ ? []
751
+ : freshness.issues.map((issue) => `${phase.id}: ${issue}`);
752
+ });
753
+ return { id: 'live-readback', status: issues.length === 0 ? 'passed' : 'failed', issues };
754
+ }
755
+ function credentialPolicyCheck(inspection) {
756
+ if (!inspection.credential.applicable) {
757
+ return { id: 'credential-policy', status: 'skipped', issues: [] };
758
+ }
759
+ return {
760
+ id: 'credential-policy',
761
+ status: inspection.credential.ready ? 'passed' : 'failed',
762
+ issues: inspection.credential.issues
763
+ };
764
+ }
765
+ function activeSourceOfTruthCheck(inspection) {
766
+ const source = inspection.sourceOfTruth;
767
+ if (source.status === 'selected' || source.status === 'none') {
768
+ if (source.status === 'selected' && source.reconciliation.status !== 'not-required') {
769
+ return {
770
+ id: 'active-source-of-truth',
771
+ status: 'failed',
772
+ issues: source.reconciliation.issues
773
+ };
774
+ }
775
+ return { id: 'active-source-of-truth', status: 'passed', issues: [] };
776
+ }
777
+ if (source.status === 'seed-blocked' || source.status === 'ambiguous' || source.status === 'incompatible') {
778
+ return {
779
+ id: 'active-source-of-truth',
780
+ status: 'failed',
781
+ issues: source.blockers
782
+ };
783
+ }
784
+ return { id: 'active-source-of-truth', status: 'failed', issues: ['Unknown active source-of-truth status.'] };
785
+ }
786
+ async function pathExists(filePath) {
787
+ try {
788
+ await access(filePath);
789
+ return true;
790
+ }
791
+ catch (error) {
792
+ if (errorCode(error) === 'ENOENT') {
793
+ return false;
794
+ }
795
+ throw error;
796
+ }
797
+ }
798
+ async function verifyChecks(inspection) {
799
+ const managedGraphPresent = await pathExists(await resolveProjectPath(inspection.projectRoot, [...managedPhaseGraphPathParts]));
800
+ return [
801
+ { id: 'phase-graph', status: 'passed', issues: [] },
802
+ {
803
+ id: 'graph-identity',
804
+ status: inspection.graph.hash === currentActivationIdentity.phaseGraphHash ? 'passed' : 'failed',
805
+ issues: inspection.graph.hash === currentActivationIdentity.phaseGraphHash
806
+ ? []
807
+ : [`Graph hash ${inspection.graph.hash} does not match activation identity.`]
808
+ },
809
+ {
810
+ id: 'managed-graph-source',
811
+ status: managedGraphPresent || inspection.graph.source === 'packaged' ? 'passed' : 'failed',
812
+ issues: []
813
+ },
814
+ validatePolicyIdentity(inspection),
815
+ await validateManagedPolicy(inspection.projectRoot, inspection.manifest),
816
+ {
817
+ id: 'activation-state',
818
+ status: inspection.stateSource === 'not-started' ? 'skipped' : 'passed',
819
+ issues: inspection.stateSource === 'not-started'
820
+ ? ['No user activation state exists; reporting deterministic not-started view.']
821
+ : []
822
+ },
823
+ activeChangeIdentityCheck(inspection),
824
+ activeSourceOfTruthCheck(inspection),
825
+ validateEvidenceFreshnessCheck(inspection),
826
+ validateStateEvidence(inspection),
827
+ credentialPolicyCheck(inspection),
828
+ liveReadbackCheck(inspection),
829
+ await activeTaskProjectionCheck(inspection),
830
+ validateReadinessCheck(inspection)
831
+ ];
832
+ }
833
+ async function verifyJson(inspection) {
834
+ const checks = await verifyChecks(inspection);
835
+ return {
836
+ schemaVersion: 1,
837
+ command: 'governance verify',
838
+ projectRoot: inspection.projectRoot,
839
+ readOnly: true,
840
+ ok: checks.every((check) => check.status !== 'failed'),
841
+ activationIdentity: inspection.state.identity,
842
+ graphHash: inspection.graph.hash,
843
+ activeChange: inspection.state.activeChange,
844
+ activeSourceOfTruth: inspection.sourceOfTruth,
845
+ nextReadyPhase: inspection.readiness.nextReadyPhase,
846
+ checks
847
+ };
848
+ }
849
+ async function renderVerifyHuman(inspection, presentation) {
850
+ const result = await verifyJson(inspection);
851
+ const checks = result.checks;
852
+ presentation.commandIdentity('governance verify', 'Read-only activation verification');
853
+ for (const check of checks) {
854
+ presentation.status(check.status === 'failed' ? 'error' : check.status === 'skipped' ? 'info' : 'success', check.id, check.issues[0]);
855
+ }
856
+ return result.ok === true ? 0 : 1;
857
+ }
858
+ function renderInspectionFailure(subcommand, projectRoot, error, presentation, jsonMode) {
859
+ const result = {
860
+ schemaVersion: 1,
861
+ command: `governance ${subcommand}`,
862
+ projectRoot,
863
+ readOnly: true,
864
+ ok: false,
865
+ checks: [{
866
+ id: 'inspection',
867
+ status: 'failed',
868
+ issues: [errorMessage(error)]
869
+ }]
870
+ };
871
+ if (jsonMode) {
872
+ json(presentation, result);
873
+ }
874
+ else {
875
+ presentation.error(errorMessage(error), 'Fix the malformed governance file or restore it from version control, then rerun verification.');
876
+ }
877
+ return 1;
878
+ }
879
+ function attachPresentation(inspection, presentation) {
880
+ inspection.presentation = presentation;
881
+ return inspection;
882
+ }
883
+ function transitionInspection(inspection) {
884
+ return {
885
+ projectRoot: inspection.projectRoot,
886
+ manifest: inspection.manifest,
887
+ graph: inspection.graph.graph,
888
+ graphHash: inspection.graph.hash,
889
+ ...(inspection.loadedState ? { loadedState: inspection.loadedState } : {}),
890
+ state: inspection.state,
891
+ approvals: inspection.approvals,
892
+ evidence: inspection.evidence,
893
+ contexts: inspection.contexts,
894
+ readiness: inspection.readiness,
895
+ sourceOfTruth: inspection.sourceOfTruth
896
+ };
897
+ }
898
+ function renderApplyNextHuman(result, presentation) {
899
+ presentation.commandIdentity('governance apply-next', 'Controlled activation transition');
900
+ presentation.status(result.applied ? 'success' : result.authorized ? 'pending' : 'error', result.reason, result.message);
901
+ presentation.table('Proposed operations', ['Adapter', 'Action', 'Mutation', 'Remote', 'Destructive'], result.proposedMutations.operations.map((op) => [
902
+ op.adapter,
903
+ op.actionId,
904
+ op.mutationClass,
905
+ String(op.remote),
906
+ String(op.destructive)
907
+ ]));
908
+ if ('executedOperations' in result) {
909
+ presentation.table('Executed operations', ['Adapter', 'Action', 'Mutation'], result.executedOperations.map((op) => [
910
+ op.adapter,
911
+ op.actionId,
912
+ op.mutationClass
913
+ ]));
914
+ if (result.savedPlan) {
915
+ presentation.status('info', 'Saved plan', `${result.savedPlan.pathParts.join('/')} (${result.savedPlan.digest})`);
916
+ }
917
+ if (result.evidence) {
918
+ presentation.status('info', 'Evidence', `${result.evidence.pathParts.join('/')} (${result.evidence.headerDigest})`);
919
+ }
920
+ if (result.stateHash) {
921
+ presentation.status('info', 'State hash', result.stateHash);
922
+ }
923
+ }
924
+ else {
925
+ presentation.status('info', 'Preview only', 'No writes occurred; rerun with --execute to execute at most one phase.');
926
+ }
927
+ }
928
+ export async function governanceCommand(parsed, context) {
929
+ const presentation = context.presentation;
930
+ const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
931
+ const subcommand = parseGovernanceSubcommand(parsed);
932
+ if (!subcommand) {
933
+ presentation.error('Missing governance subcommand.', 'Run `liftoff governance --help` and choose one of: status, plan, apply-next, resume, verify.');
934
+ return 1;
935
+ }
936
+ let projectRoot;
937
+ try {
938
+ projectRoot = await resolveGovernanceProjectRoot(parsed, context);
939
+ }
940
+ catch (error) {
941
+ presentation.error(errorMessage(error), 'Run `liftoff governance --help` to review accepted project arguments.');
942
+ return 1;
943
+ }
944
+ if (!projectRoot) {
945
+ const start = parsed.positional[0] ?? readStringFlag(parsed.flags, 'project') ?? context.cwd;
946
+ const failure = projectRootError(path.resolve(context.cwd, start));
947
+ presentation.error(failure.message, failure.remedy);
948
+ return 1;
949
+ }
950
+ let inspection;
951
+ try {
952
+ inspection = attachPresentation(await inspectGovernance(projectRoot), presentation);
953
+ }
954
+ catch (error) {
955
+ if (subcommand === 'verify') {
956
+ return renderInspectionFailure(subcommand, projectRoot, error, presentation, jsonMode);
957
+ }
958
+ throw error;
959
+ }
960
+ if (subcommand === 'status') {
961
+ if (jsonMode) {
962
+ json(presentation, statusJson(inspection, 'status'));
963
+ }
964
+ else {
965
+ renderStatusHuman(inspection, 'status');
966
+ }
967
+ return 0;
968
+ }
969
+ if (subcommand === 'plan') {
970
+ if (jsonMode) {
971
+ json(presentation, planJson(inspection));
972
+ }
973
+ else {
974
+ renderPlanHuman(inspection, presentation);
975
+ }
976
+ return 0;
977
+ }
978
+ if (subcommand === 'resume') {
979
+ const result = {
980
+ ...statusJson(inspection, 'resume'),
981
+ deterministicPreflights: ['phase-graph', 'activation-state', 'approvals', 'evidence-freshness', 'readiness'],
982
+ executedOperations: [],
983
+ noWrites: true
984
+ };
985
+ if (jsonMode) {
986
+ json(presentation, result);
987
+ }
988
+ else {
989
+ renderStatusHuman(inspection, 'resume');
990
+ presentation.status('info', 'Resume scope', 'Recalculated blockers and readiness only; no verified operation was rerun.');
991
+ }
992
+ return 0;
993
+ }
994
+ if (subcommand === 'verify') {
995
+ if (jsonMode) {
996
+ const result = await verifyJson(inspection);
997
+ json(presentation, result);
998
+ return result.ok === true ? 0 : 1;
999
+ }
1000
+ return await renderVerifyHuman(inspection, presentation);
1001
+ }
1002
+ const execute = readBooleanFlag(parsed.flags, 'execute') ?? false;
1003
+ const transitionInput = transitionInspection(inspection);
1004
+ const result = execute
1005
+ ? await executeApplyNext({
1006
+ inspection: transitionInput,
1007
+ runner: context.runner,
1008
+ reinspect: async () => transitionInspection(attachPresentation(await inspectGovernance(projectRoot), presentation))
1009
+ })
1010
+ : await previewApplyNext({
1011
+ inspection: transitionInput,
1012
+ runner: context.runner,
1013
+ execute: false
1014
+ });
1015
+ if (jsonMode) {
1016
+ json(presentation, result);
1017
+ }
1018
+ else {
1019
+ renderApplyNextHuman(result, presentation);
1020
+ }
1021
+ return result.applied || result.reason === 'execute-required' ? 0 : 1;
1022
+ }
1023
+ //# sourceMappingURL=commands.js.map