@msn-control/liftoff 0.11.1 → 0.11.2

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 (132) hide show
  1. package/DEVELOPER.md +28 -10
  2. package/README.md +11 -6
  3. package/dist/adapters/filesystem/project-transaction.d.ts +2 -1
  4. package/dist/adapters/filesystem/project-transaction.js +25 -6
  5. package/dist/adapters/filesystem/project-transaction.js.map +1 -1
  6. package/dist/adapters/filesystem/reviewed-update-transaction.d.ts +53 -0
  7. package/dist/adapters/filesystem/reviewed-update-transaction.js +864 -0
  8. package/dist/adapters/filesystem/reviewed-update-transaction.js.map +1 -0
  9. package/dist/adapters/filesystem/update-previews.d.ts +50 -0
  10. package/dist/adapters/filesystem/update-previews.js +613 -0
  11. package/dist/adapters/filesystem/update-previews.js.map +1 -0
  12. package/dist/adapters/process/shell-command.d.ts +1 -0
  13. package/dist/adapters/process/shell-command.js +7 -1
  14. package/dist/adapters/process/shell-command.js.map +1 -1
  15. package/dist/application/context.d.ts +5 -0
  16. package/dist/application/diagnose/doctor.js +9 -15
  17. package/dist/application/diagnose/doctor.js.map +1 -1
  18. package/dist/application/update/approval.d.ts +39 -0
  19. package/dist/application/update/approval.js +54 -0
  20. package/dist/application/update/approval.js.map +1 -0
  21. package/dist/application/update/command-guidance.d.ts +2 -0
  22. package/dist/application/update/command-guidance.js +14 -0
  23. package/dist/application/update/command-guidance.js.map +1 -0
  24. package/dist/application/update/inspection.d.ts +37 -0
  25. package/dist/application/update/inspection.js +235 -0
  26. package/dist/application/update/inspection.js.map +1 -0
  27. package/dist/application/update/migration-runtime.d.ts +14 -0
  28. package/dist/application/update/migration-runtime.js +178 -0
  29. package/dist/application/update/migration-runtime.js.map +1 -0
  30. package/dist/application/update/output.d.ts +134 -0
  31. package/dist/application/update/output.js +182 -0
  32. package/dist/application/update/output.js.map +1 -0
  33. package/dist/application/update/preview.d.ts +49 -0
  34. package/dist/application/update/preview.js +220 -0
  35. package/dist/application/update/preview.js.map +1 -0
  36. package/dist/application/update/protected-source.d.ts +25 -0
  37. package/dist/application/update/protected-source.js +81 -0
  38. package/dist/application/update/protected-source.js.map +1 -0
  39. package/dist/application/update/reporting.d.ts +4 -4
  40. package/dist/application/update/reporting.js +14 -13
  41. package/dist/application/update/reporting.js.map +1 -1
  42. package/dist/application/update/revalidation-plan.d.ts +22 -0
  43. package/dist/application/update/revalidation-plan.js +108 -0
  44. package/dist/application/update/revalidation-plan.js.map +1 -0
  45. package/dist/application/update/revalidation.d.ts +70 -0
  46. package/dist/application/update/revalidation.js +337 -0
  47. package/dist/application/update/revalidation.js.map +1 -0
  48. package/dist/application/update/review-plan.d.ts +25 -0
  49. package/dist/application/update/review-plan.js +89 -0
  50. package/dist/application/update/review-plan.js.map +1 -0
  51. package/dist/application/update/transaction-approval.d.ts +29 -0
  52. package/dist/application/update/transaction-approval.js +82 -0
  53. package/dist/application/update/transaction-approval.js.map +1 -0
  54. package/dist/application/update/use-case.d.ts +1 -0
  55. package/dist/application/update/use-case.js +240 -569
  56. package/dist/application/update/use-case.js.map +1 -1
  57. package/dist/application/update/write-plan.d.ts +15 -0
  58. package/dist/application/update/write-plan.js +115 -0
  59. package/dist/application/update/write-plan.js.map +1 -0
  60. package/dist/cli/args/definitions.js +5 -4
  61. package/dist/cli/args/definitions.js.map +1 -1
  62. package/dist/cli/args/help.js +13 -1
  63. package/dist/cli/args/help.js.map +1 -1
  64. package/dist/cli/args/parser.js +22 -7
  65. package/dist/cli/args/parser.js.map +1 -1
  66. package/dist/cli/commands/update.js +1 -0
  67. package/dist/cli/commands/update.js.map +1 -1
  68. package/dist/domain/governance/policy/identity.d.ts +1 -1
  69. package/dist/domain/governance/policy/identity.js +2 -2
  70. package/dist/domain/governance/policy/identity.js.map +1 -1
  71. package/dist/domain/project/infrastructure-layout.d.ts +1 -1
  72. package/dist/domain/project/infrastructure-layout.js +2 -1
  73. package/dist/domain/project/infrastructure-layout.js.map +1 -1
  74. package/dist/domain/project/reviewed-update-artifacts.d.ts +2 -0
  75. package/dist/domain/project/reviewed-update-artifacts.js +3 -0
  76. package/dist/domain/project/reviewed-update-artifacts.js.map +1 -0
  77. package/dist/generators/common/base.js +5 -1
  78. package/dist/generators/common/base.js.map +1 -1
  79. package/dist/governance-activation/activation-state.js +4 -1
  80. package/dist/governance-activation/activation-state.js.map +1 -1
  81. package/dist/governance-activation/commands.d.ts +5 -0
  82. package/dist/governance-activation/commands.js +65 -3
  83. package/dist/governance-activation/commands.js.map +1 -1
  84. package/dist/governance-activation/compatibility.d.ts +21 -4
  85. package/dist/governance-activation/compatibility.js +47 -8
  86. package/dist/governance-activation/compatibility.js.map +1 -1
  87. package/dist/governance-activation/doctor.js +32 -5
  88. package/dist/governance-activation/doctor.js.map +1 -1
  89. package/dist/governance-activation/historical-state.d.ts +81 -0
  90. package/dist/governance-activation/historical-state.js +817 -0
  91. package/dist/governance-activation/historical-state.js.map +1 -0
  92. package/dist/governance-activation/history-contracts.d.ts +109 -0
  93. package/dist/governance-activation/history-contracts.js +303 -0
  94. package/dist/governance-activation/history-contracts.js.map +1 -0
  95. package/dist/governance-activation/inputs.d.ts +2 -0
  96. package/dist/governance-activation/inputs.js +13 -1
  97. package/dist/governance-activation/inputs.js.map +1 -1
  98. package/dist/governance-activation/migration-history.d.ts +97 -0
  99. package/dist/governance-activation/migration-history.js +388 -0
  100. package/dist/governance-activation/migration-history.js.map +1 -0
  101. package/dist/governance-activation/migration.js +1 -1
  102. package/dist/governance-activation/migration.js.map +1 -1
  103. package/dist/governance-activation/proof-records.d.ts +3 -0
  104. package/dist/governance-activation/proof-records.js +78 -0
  105. package/dist/governance-activation/proof-records.js.map +1 -0
  106. package/dist/governance-activation/read-only.d.ts +4 -4
  107. package/dist/governance-activation/read-only.js +9 -69
  108. package/dist/governance-activation/read-only.js.map +1 -1
  109. package/dist/governance-activation/seed-lifecycle.d.ts +28 -6
  110. package/dist/governance-activation/seed-lifecycle.js +258 -25
  111. package/dist/governance-activation/seed-lifecycle.js.map +1 -1
  112. package/dist/governance-activation/transition-planning.d.ts +2 -0
  113. package/dist/governance-activation/transition-planning.js +12 -3
  114. package/dist/governance-activation/transition-planning.js.map +1 -1
  115. package/dist/governance-activation/transitions.d.ts +8 -2
  116. package/dist/governance-activation/transitions.js +36 -11
  117. package/dist/governance-activation/transitions.js.map +1 -1
  118. package/dist/governance-assessment/engine.js +30 -3
  119. package/dist/governance-assessment/engine.js.map +1 -1
  120. package/dist/governance-assessment/project.d.ts +7 -0
  121. package/dist/governance-assessment/project.js +44 -4
  122. package/dist/governance-assessment/project.js.map +1 -1
  123. package/dist/repository-governance.js +25 -12
  124. package/dist/repository-governance.js.map +1 -1
  125. package/docs/cli-reference.md +101 -28
  126. package/docs/configuration-and-manifests.md +27 -15
  127. package/docs/existing-repositories.md +19 -10
  128. package/docs/repository-governance.md +9 -5
  129. package/docs/safety-and-consent.md +27 -18
  130. package/docs/troubleshooting.md +9 -7
  131. package/docs/workloads.md +5 -4
  132. package/package.json +1 -1
@@ -1,600 +1,271 @@
1
1
  import path from 'node:path';
2
- import { applyProjectFileTransaction, captureProjectFileSnapshot } from '../../adapters/filesystem/project-transaction.js';
2
+ import { consumeUpdatePreviewReceipt, createUpdateTransactionApprovalStore, issueUpdatePreviewReceipt, loadUpdatePreviewReceipt, resolveUpdatePreviewLocation } from '../../adapters/filesystem/update-previews.js';
3
+ import { applyReviewedUpdateTransaction, inspectReviewedUpdateTransaction, recoverReviewedUpdateTransaction } from '../../adapters/filesystem/reviewed-update-transaction.js';
3
4
  import { findProjectRoot } from '../../adapters/filesystem/project-discovery.js';
4
- import { loadManifest, parseManifest } from '../project/manifest.js';
5
- import { manifestHadFilteredLegacyNonDurableOwnership } from '../../domain/project/manifest/reader.js';
6
5
  import { manifestDisplayPath } from '../../domain/project/paths.js';
7
- import { readProjectFile } from '../../adapters/filesystem/project-files.js';
8
- import { buildProjectPlan, loadConfigOptions } from '../project/planning.js';
9
- import { hasDrift, reconcileProject } from '../../reconcile.js';
10
- import { compareSemver } from '../../semver.js';
11
- import { buildManifest } from '../../templates.js';
12
- import { activationStateFilePathParts } from '../../governance-activation/activation-state.js';
13
- import { planHistoricalActivationStateMigration } from '../../governance-activation/migration.js';
14
- import { liftoffVersion } from '../../version.js';
15
- import { appendProvisionedProjectArtifacts, assertAuthorizedUpdateMutations, buildUpdateArtifacts, captureUpdateSnapshots, inspectProvisioningGroups, isUnownedUpdateConflict, planWithBlockedProvisioning, preflightUpdate, provisioningJson, requestedProvisioningGroups, sameWorkloadIntent, selectUpdatePreconditions } from './planning.js';
16
- import { activeChangeReconciliationReport, combineReconciliationReports, entryDisplay, entryMarker, isDirtyGitWorktree, manifestChanges, maybeInjectUpdateFailure, stateMigrationReconciliation, summarizeEntries } from './reporting.js';
17
- function preserveDiagnosticGovernanceIdentity(target, source) {
18
- if (target.governance.profile === 'none' ||
19
- target.governance.profile === 'unspecified' ||
20
- source.governance.profile === 'none' ||
21
- source.governance.profile === 'unspecified') {
22
- return;
23
- }
24
- target.governance.policyVersion = source.governance.policyVersion;
25
- if (source.governance.activationIdentity) {
26
- target.governance.activationIdentity = source.governance.activationIdentity;
6
+ import { loadManifest } from '../project/manifest.js';
7
+ import { requestUpdateApproval } from './approval.js';
8
+ import { formatUpdateCommand, formatUpdateValidationCommands } from './command-guidance.js';
9
+ import { inspectProjectUpdate, UpdatePlanError } from './inspection.js';
10
+ import { buildUpdateReport, renderUpdateApprovalScope, renderUpdatePreview, renderUpdateSkipped } from './output.js';
11
+ import { assertAuthorizedUpdateMutations, preflightUpdate } from './planning.js';
12
+ import { matchUpdatePreviewReceipt, UpdatePreviewError } from './preview.js';
13
+ import { prepareUpdateReview } from './review-plan.js';
14
+ import { entryDisplay, maybeInjectUpdateFailure } from './reporting.js';
15
+ import { describeUpdateMigration, describeUpdateRevalidation, materializeUpdateMutations, runUpdateRevalidation, verifyHistoryBeforeReplacement } from './migration-runtime.js';
16
+ function storeOptions(context, inspection) {
17
+ return {
18
+ ...context.updatePreview,
19
+ env: context.updatePreview?.env ?? context.env,
20
+ clock: context.updatePreview?.clock ?? context.updateNow,
21
+ ...(inspection?.repositoryRoot ? { repositoryRoot: inspection.repositoryRoot } : {})
22
+ };
23
+ }
24
+ function emit(context, jsonMode, report, inspection, review) {
25
+ if (jsonMode) {
26
+ context.presentation.rawStdout(`${JSON.stringify(buildUpdateReport(report, inspection, review?.writePlan), null, 2)}\n`);
27
27
  }
28
- else {
29
- delete target.governance.activationIdentity;
28
+ else if (report.message) {
29
+ if (report.status === 'blocked' || report.status === 'failed') {
30
+ context.presentation.error(report.message, report.remedy);
31
+ }
32
+ else {
33
+ context.presentation.status(report.status === 'partial' ? 'warning' : 'success', 'Project update', report.message);
34
+ if (report.remedy)
35
+ context.presentation.remedy(report.remedy);
36
+ }
30
37
  }
31
38
  }
32
39
  export async function updateProject(request, context) {
33
40
  const { presentation } = context;
34
- const { check, force, jsonMode, project: explicit } = request;
35
- presentation.commandIdentity('update', 'Reconcile Liftoff-managed core files');
36
- if (force && check) {
37
- presentation.error('--force cannot be combined with --check.', 'Run `liftoff update --check` to inspect drift or `liftoff update --force` to overwrite conflicts.');
38
- return 1;
39
- }
40
- const projectRoot = explicit ? path.resolve(context.cwd, explicit) : await findProjectRoot(context.cwd);
41
- if (!projectRoot) {
42
- presentation.error(`No liftoff.manifest.json found in ${context.cwd} or any parent directory.`, 'Run this command inside a Liftoff project or provide its path explicitly.');
43
- return 1;
44
- }
45
- const initialUpdateSnapshots = check
46
- ? []
47
- : await Promise.all([
48
- captureProjectFileSnapshot(projectRoot, ['liftoff.manifest.json']),
49
- captureProjectFileSnapshot(projectRoot, ['liftoff.config.json'])
50
- ]);
51
- const manifest = await loadManifest(projectRoot);
52
- const oldByName = new Map(manifest.managedArtifacts.map((artifact) => [artifact.logicalName, artifact]));
53
- if (compareSemver(manifest.liftoffVersion, liftoffVersion) > 0) {
54
- presentation.error(`This project was written by Liftoff ${manifest.liftoffVersion}, which is newer than this CLI (${liftoffVersion}).`, 'Upgrade the CLI first.');
55
- return 1;
56
- }
57
- const config = await loadConfigOptions('liftoff.config.json', projectRoot);
58
- const recordedWorkload = manifest.project.workload;
59
- if (config.cloud !== undefined && config.cloud !== recordedWorkload.cloud) {
60
- presentation.error(`Cloud changes (${recordedWorkload.cloud} -> ${config.cloud}) are a migration, not an update.`, 'Restore liftoff.config.json or perform the cloud change through a separately reviewed project migration.');
61
- return 1;
62
- }
63
- const plan = buildProjectPlan(config, { requireProjectName: true });
64
- if (plan.projectName !== manifest.project.name) {
65
- presentation.error(`Project name changes (${manifest.project.name} -> ${plan.projectName}) are a migration, not an update.`, 'Restore liftoff.config.json or perform the rename through a separately reviewed project migration.');
66
- return 1;
67
- }
68
- if (plan.workload !== recordedWorkload.kind) {
69
- presentation.error(`Project type changes (${recordedWorkload.kind} -> ${plan.workload}) are not supported by update.`, 'Initialize a fresh project and move production behavior through a reviewed project change.');
70
- return 1;
71
- }
72
- {
73
- if (plan.provider.id !== recordedWorkload.cloud) {
74
- presentation.error(`Cloud changes (${recordedWorkload.cloud} -> ${plan.provider.id}) are a migration, not an update.`, 'Restore liftoff.config.json or perform the cloud change through a separately reviewed project migration.');
75
- return 1;
76
- }
77
- if (plan.region.slug !== recordedWorkload.region) {
78
- presentation.error(`Region changes (${recordedWorkload.region} -> ${plan.region.slug}) are a migration, not an update.`, 'Restore liftoff.config.json or perform the region change through a separately reviewed project migration.');
79
- return 1;
41
+ const { check, force, jsonMode } = request;
42
+ presentation.commandIdentity('update', 'Preview and explicitly approve scoped project updates');
43
+ let projectRoot = request.project ? path.resolve(context.cwd, request.project) : context.cwd;
44
+ let inspection;
45
+ let selected;
46
+ let committed = false;
47
+ let migration;
48
+ let revalidation;
49
+ try {
50
+ if (check && (force || request.approvePlan !== undefined)) {
51
+ throw new UpdatePlanError('--check cannot be combined with --force or --approve-plan.', 'invalid-update-options', `Run ${formatUpdateCommand(projectRoot, 'check')} before explicitly approving an apply plan.`);
80
52
  }
81
- if (plan.apiStack.id !== recordedWorkload.apiStack) {
82
- presentation.error(`API stack changes (${recordedWorkload.apiStack} -> ${plan.apiStack.id}) are a migration, not an update.`, 'Initialize a fresh project and move production behavior through a reviewed project change.');
83
- return 1;
53
+ const discovered = request.project ? projectRoot : await findProjectRoot(context.cwd);
54
+ if (!discovered) {
55
+ throw new UpdatePlanError(`No liftoff.manifest.json found in ${context.cwd} or any parent directory.`, 'project-not-found', 'Run inside a Liftoff project or supply its path.');
84
56
  }
85
- const recordedPattern = recordedWorkload.kind === 'genai' ? recordedWorkload.pattern : undefined;
86
- const desiredPattern = plan.workload === 'genai' ? plan.pattern.id : undefined;
87
- if (desiredPattern !== recordedPattern) {
88
- presentation.error(`Pattern changes (${recordedPattern ?? 'none'} -> ${desiredPattern ?? 'none'}) are a migration, not an update.`, 'Use a separately reviewed project migration; the existing migrate command only adopts non-Liftoff sources into a fresh target.');
89
- return 1;
57
+ projectRoot = discovered;
58
+ await loadManifest(projectRoot);
59
+ const initialOptions = storeOptions(context);
60
+ const approvalStore = createUpdateTransactionApprovalStore(projectRoot, initialOptions);
61
+ const recovery = await inspectReviewedUpdateTransaction(projectRoot, { approvalStore });
62
+ if (recovery.status !== 'absent') {
63
+ if (check || recovery.status === 'blocked') {
64
+ emit(context, jsonMode, {
65
+ mode: check ? 'check' : 'apply', status: 'blocked',
66
+ reasonCode: 'transaction-recovery-required', projectRoot, committed: recovery.committed,
67
+ message: recovery.reason ?? 'An interrupted approved update requires bounded recovery.',
68
+ remedy: `Review the reported transaction and run ${formatUpdateCommand(projectRoot)} for recovery; then run ${formatUpdateCommand(projectRoot, 'check')}.`
69
+ });
70
+ return 1;
71
+ }
72
+ const outcome = await recoverReviewedUpdateTransaction(projectRoot, { approvalStore });
73
+ emit(context, jsonMode, {
74
+ mode: 'apply', status: outcome.status === 'blocked' ? 'failed' : 'partial',
75
+ reasonCode: 'transaction-recovery', projectRoot, committed: outcome.committed,
76
+ message: outcome.status === 'blocked'
77
+ ? outcome.rollbackFailures.join('; ')
78
+ : 'Recovered the previously approved transaction; no new update was started.',
79
+ warnings: outcome.cleanupFailures,
80
+ remedy: `Run ${formatUpdateCommand(projectRoot, 'check')} before approving new work.`
81
+ });
82
+ return outcome.status === 'blocked' ? 1 : 2;
90
83
  }
91
- }
92
- if (manifest.governance.profile !== 'none' &&
93
- manifest.governance.profile !== 'unspecified' &&
94
- plan.governanceProfile.id === 'none' &&
95
- await readProjectFile(projectRoot, [...activationStateFilePathParts]) !== undefined) {
96
- presentation.error('Repository governance cannot be disabled by update while governance/activation-state.json exists.', 'Restore liftoff.config.json or complete a separately supported deactivation and reconciliation workflow before disabling governance. This Liftoff release does not infer deactivation or the absence of live enforcement.');
97
- return 1;
98
- }
99
- if (plan.specWorkflow.id !== manifest.project.specWorkflow) {
100
- presentation.error(`Spec workflow changes (${manifest.project.specWorkflow} -> ${plan.specWorkflow.id}) require official framework initialization and are not supported by liftoff update.`, 'Restore the workflow recorded in liftoff.manifest.json or migrate into a fresh project.');
101
- return 1;
102
- }
103
- const configuredAgents = plan.agents.map((agent) => agent.id);
104
- if (manifest.framework.state === 'initialized' && (configuredAgents.length !== manifest.project.agents.length ||
105
- configuredAgents.some((agent, index) => agent !== manifest.project.agents[index]) ||
106
- plan.defaultAgent?.id !== manifest.project.defaultAgent)) {
107
- presentation.error('AI agent or default-agent changes require official framework initialization and are not supported by liftoff update.', 'Restore liftoff.config.json to the integrations recorded in liftoff.manifest.json.');
108
- return 1;
109
- }
110
- let desiredRenderPlan = manifest.framework.state === 'legacy'
111
- ? { ...plan, agents: [], defaultAgent: undefined }
112
- : plan;
113
- const stateMigration = await planHistoricalActivationStateMigration(projectRoot);
114
- const desiredRender = buildUpdateArtifacts(desiredRenderPlan, manifest);
115
- let provisioningPlans = await inspectProvisioningGroups(projectRoot, desiredRender, requestedProvisioningGroups(manifest, desiredRenderPlan));
116
- if (stateMigration.report.diagnosticOnly === true) {
117
- provisioningPlans = provisioningPlans.map((group) => ({
118
- ...group,
119
- entries: [],
120
- blocked: true,
121
- reason: 'Historical activation v1 is diagnostic-only. This update may maintain managed core but cannot provision project-owned components.'
122
- }));
123
- }
124
- const renderPlan = planWithBlockedProvisioning(desiredRenderPlan, recordedWorkload, provisioningPlans);
125
- const render = buildUpdateArtifacts(renderPlan, manifest);
126
- const scopedRender = [
127
- ...render.filter((artifact) => artifact.lifecycle === 'managed-core'),
128
- ...desiredRender.filter((artifact) => artifact.lifecycle === 'project' &&
129
- provisioningPlans.some((group) => !group.blocked && group.group === artifact.provisioningGroup))
130
- ];
131
- const updateSnapshots = check
132
- ? []
133
- : await captureUpdateSnapshots(projectRoot, manifest, scopedRender, initialUpdateSnapshots);
134
- const entries = await reconcileProject(manifest, render, projectRoot);
135
- const summary = summarizeEntries(entries);
136
- const ownershipMigrationPending = manifest.artifactVersion !== 7 ||
137
- manifestHadFilteredLegacyNonDurableOwnership(manifest);
138
- const plannedManifest = buildManifest(renderPlan, render, {
139
- frameworkState: manifest.framework.state,
140
- projectArtifacts: manifest.projectArtifacts
141
- });
142
- if (stateMigration.report.diagnosticOnly === true) {
143
- preserveDiagnosticGovernanceIdentity(plannedManifest, manifest);
144
- }
145
- const plannedManifestChanges = manifestChanges(manifest, plannedManifest);
146
- const reconciliation = combineReconciliationReports([
147
- stateMigrationReconciliation(stateMigration),
148
- await activeChangeReconciliationReport(projectRoot)
149
- ]);
150
- if (reconciliation.status === 'blocked') {
151
- const blockedReport = {
152
- schemaVersion: 2,
153
- mode: check ? 'check' : 'apply',
154
- scope: 'managed-core',
155
- status: 'blocked',
156
- cliVersion: liftoffVersion,
157
- projectVersion: manifest.liftoffVersion,
158
- entries: [],
159
- removed: [],
160
- provisioning: [],
161
- ownershipMigrationPending: false,
162
- manifestChanges: plannedManifestChanges,
163
- activationStateMigration: stateMigration.report,
164
- reconciliation,
165
- summary
84
+ inspection = await inspectProjectUpdate(projectRoot, { runner: context.runner });
85
+ projectRoot = inspection.projectRoot;
86
+ const options = storeOptions(context, inspection);
87
+ const reviewOptions = { runner: context.runner, now: context.updateNow?.() ?? new Date() };
88
+ const normal = await prepareUpdateReview(inspection, false, reviewOptions);
89
+ const forced = await prepareUpdateReview(inspection, true, reviewOptions);
90
+ const variants = [normal, forced];
91
+ const hasWork = inspection.hasDrift || variants.some((entry) => entry.requiresApproval);
92
+ const plans = hasWork ? variants.map((entry) => entry.summary) : [];
93
+ selected = force ? forced : normal;
94
+ migration = describeUpdateMigration(inspection);
95
+ committed = migration.status === 'committed';
96
+ revalidation = describeUpdateRevalidation(selected);
97
+ const base = {
98
+ mode: check ? 'check' : 'apply', projectRoot,
99
+ plans, selectedPlanFingerprint: selected.descriptor.fingerprint, migration, revalidation
166
100
  };
167
- if (jsonMode) {
168
- presentation.rawStdout(`${JSON.stringify(blockedReport, null, 2)}\n`);
101
+ if (!selected.summary.eligible && (!check || !variants.some((entry) => entry.summary.eligible))) {
102
+ emit(context, jsonMode, {
103
+ ...base, status: 'blocked', reasonCode: 'incompatible-update',
104
+ message: selected.summary.blockers.join('; '),
105
+ remedy: inspection.reconciliation.remedy
106
+ }, inspection, selected);
107
+ return 1;
169
108
  }
170
- else {
171
- presentation.status('error', 'Managed update blocked', reconciliation.issues.join('; '));
172
- if (reconciliation.remedy) {
173
- presentation.remedy(reconciliation.remedy);
109
+ if (check) {
110
+ const stored = hasWork
111
+ ? await issueUpdatePreviewReceipt(projectRoot, variants.filter((entry) => entry.summary.eligible).map((entry) => entry.descriptor), options)
112
+ : undefined;
113
+ if (!jsonMode) {
114
+ renderUpdatePreview(presentation, inspection, plans, migration, revalidation, stored?.location.receiptPath);
115
+ if (!hasWork) {
116
+ presentation.status('success', 'Liftoff core is current', `${inspection.summary.unchanged} managed-core artifacts match; project files are not compared`);
117
+ }
118
+ else {
119
+ presentation.status('warning', 'Liftoff core maintenance available', `${inspection.summary.conflict + inspection.summary.retiredConflict} core conflict(s); review the exact plans before approval`);
120
+ }
174
121
  }
122
+ emit(context, jsonMode, {
123
+ ...base,
124
+ status: hasWork ? 'update-available' : 'current',
125
+ reasonCode: hasWork ? 'review-required' : 'no-update',
126
+ receipt: stored ? { status: 'issued', path: stored.location.receiptPath } : { status: 'not-required' }
127
+ }, inspection, selected);
128
+ return hasWork ? 2 : 0;
175
129
  }
176
- return 1;
177
- }
178
- const workloadIntentChanged = !sameWorkloadIntent(plannedManifest.project.workload, manifest.project.workload);
179
- const manifestRewritePending = ownershipMigrationPending ||
180
- workloadIntentChanged;
181
- const drift = hasDrift(entries) ||
182
- manifestRewritePending ||
183
- provisioningPlans.length > 0 ||
184
- stateMigration.status === 'migrate';
185
- const visible = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash);
186
- if (!drift) {
187
- if (jsonMode) {
188
- presentation.rawStdout(`${JSON.stringify(check ? {
189
- schemaVersion: 2,
190
- mode: 'check',
191
- scope: 'managed-core',
192
- status: reconciliation.status === 'reconciliation-required' ? 'reconciliation-required' : 'current',
193
- cliVersion: liftoffVersion,
194
- projectVersion: manifest.liftoffVersion,
195
- entries: [],
196
- provisioning: [],
197
- ownershipMigrationPending,
198
- manifestChanges: plannedManifestChanges,
199
- activationIdentity: manifest.governance.profile !== 'none' && manifest.governance.profile !== 'unspecified'
200
- ? manifest.governance.activationIdentity ?? null
201
- : null,
202
- activationStateMigration: stateMigration.report,
203
- reconciliation,
204
- summary
205
- } : {
206
- schemaVersion: 2,
207
- mode: 'apply',
208
- scope: 'managed-core',
209
- status: reconciliation.status === 'reconciliation-required' ? 'reconciliation-required' : 'current',
210
- cliVersion: liftoffVersion,
211
- projectVersion: manifest.liftoffVersion,
212
- written: [],
213
- removed: [],
214
- skipped: [],
215
- provisioning: [],
216
- ownershipMigrationPending,
217
- manifestChanges: plannedManifestChanges,
218
- activationIdentity: manifest.governance.profile !== 'none' && manifest.governance.profile !== 'unspecified'
219
- ? manifest.governance.activationIdentity ?? null
220
- : null,
221
- activationStateMigration: stateMigration.report,
222
- reconciliation,
223
- summary
224
- }, null, 2)}\n`);
130
+ if (!selected.requiresApproval) {
131
+ if (!jsonMode)
132
+ renderUpdateSkipped(presentation, inspection, selected.writePlan);
133
+ emit(context, jsonMode, {
134
+ ...base, status: selected.writePlan.skipped.length ? 'partial' : 'current', reasonCode: 'no-update',
135
+ message: selected.writePlan.skipped.length
136
+ ? 'No safe update writes are required; listed conflicts remain protected.'
137
+ : 'Liftoff core is current; project files were not changed.'
138
+ }, inspection, selected);
225
139
  return 0;
226
140
  }
227
- presentation.definitions('Project versions', [
228
- { label: 'Liftoff CLI', value: liftoffVersion },
229
- { label: 'Project generated by', value: manifest.liftoffVersion }
230
- ]);
231
- presentation.status('success', 'Liftoff core is current', `${summary.unchanged} managed-core artifacts match; project files are not compared`);
232
- if (stateMigration.report.diagnosticOnly === true) {
233
- presentation.status('warning', 'Historical activation remains diagnostic-only', 'Managed-core is current; activation v1 state and evidence were preserved without migration, retagging, or execution authority.');
141
+ const location = await resolveUpdatePreviewLocation(projectRoot, options);
142
+ const stored = await loadUpdatePreviewReceipt(projectRoot, options);
143
+ matchUpdatePreviewReceipt(stored.receipt, selected.descriptor);
144
+ if (inspection.repositoryRoot) {
145
+ const warning = 'If this worktree has uncommitted changes, consider committing or copying them before applying. Liftoff does not commit automatically.';
146
+ if (jsonMode)
147
+ presentation.rawStderr(`Warning: ${warning}\n`);
148
+ else
149
+ presentation.warning(warning);
234
150
  }
235
- return 0;
236
- }
237
- if (check) {
238
- if (jsonMode) {
239
- presentation.rawStdout(`${JSON.stringify({
240
- schemaVersion: 2,
241
- mode: 'check',
242
- scope: 'managed-core',
243
- cliVersion: liftoffVersion,
244
- projectVersion: manifest.liftoffVersion,
245
- entries: visible.map((entry) => ({
246
- logicalName: entry.logicalName,
247
- status: entry.status,
248
- path: manifestDisplayPath(entry.pathParts),
249
- previousPath: entry.previousPathParts ? manifestDisplayPath(entry.previousPathParts) : undefined,
250
- fileDeleted: entry.status === 'retired' || entry.status === 'retired-conflict'
251
- ? entry.destinationOccupied !== false
252
- : undefined,
253
- reason: entry.reason
254
- })),
255
- provisioning: provisioningJson(provisioningPlans),
256
- ownershipMigrationPending,
257
- manifestChanges: plannedManifestChanges,
258
- activationIdentity: plannedManifest.governance.profile !== 'none' && plannedManifest.governance.profile !== 'unspecified'
259
- ? plannedManifest.governance.activationIdentity ?? null
260
- : null,
261
- activationStateMigration: stateMigration.report,
262
- reconciliation,
263
- summary
264
- }, null, 2)}\n`);
265
- return 2;
151
+ renderUpdateApprovalScope(presentation, jsonMode, selected.summary, selected.writePlan, migration, revalidation);
152
+ const approval = await requestUpdateApproval({
153
+ fingerprint: selected.descriptor.fingerprint, approvePlan: request.approvePlan
154
+ }, context);
155
+ if (approval.status !== 'approved') {
156
+ emit(context, jsonMode, {
157
+ ...base, status: 'blocked', reasonCode: `approval-${approval.status}`,
158
+ receipt: { status: 'matched', path: location.receiptPath }, approval,
159
+ message: approval.status === 'required'
160
+ ? 'Explicit approval of this exact update plan is required.'
161
+ : approval.status === 'mismatch'
162
+ ? 'The approved fingerprint does not match the current effective plan.'
163
+ : 'Update approval was declined or cancelled; no project files changed.',
164
+ remedy: `Review ${formatUpdateCommand(projectRoot, 'check')}, then approve interactively or use --approve-plan with its exact fingerprint.`
165
+ }, inspection, selected);
166
+ return 1;
266
167
  }
267
- presentation.definitions('Project versions', [
268
- { label: 'Liftoff CLI', value: liftoffVersion },
269
- { label: 'Project generated by', value: manifest.liftoffVersion }
168
+ presentation.stage('Apply safe Liftoff core changes', projectRoot);
169
+ await preflightUpdate(projectRoot, inspection.entries, force, inspection.oldByName);
170
+ maybeInjectUpdateFailure(context.env, 'after-preflight');
171
+ const materialized = materializeUpdateMutations(inspection, selected, context.updateNow?.() ?? new Date());
172
+ const migrationPaths = inspection.historyMigration.status === 'eligible'
173
+ ? [
174
+ ...inspection.historyMigration.index.files.map((file) => file.copyPathParts),
175
+ ...inspection.historyMigration.requiredRetirements.map((entry) => entry.pathParts),
176
+ ['governance', 'history', inspection.historyMigration.index.snapshotId, 'index.json'],
177
+ ['governance', 'activation-state.json'], ['governance', 'migration-state.json']
178
+ ]
179
+ : [];
180
+ assertAuthorizedUpdateMutations(materialized.mutations, inspection.entries, inspection.provisioningPlans, [
181
+ ...inspection.stateMigration.mutations.map((mutation) => mutation.pathParts),
182
+ ...migrationPaths
270
183
  ]);
271
- if (visible.length > 0) {
272
- if (presentation.stdout.layout === 'plain') {
273
- presentation.section('Liftoff core drift', visible.map((entry) => `${entryMarker(entry)} ${entryDisplay(entry)} ${entry.reason}`));
274
- }
275
- else {
276
- presentation.table('Liftoff core drift', ['Change', 'Artifact', 'Reason'], visible.map((entry) => [entryMarker(entry), entryDisplay(entry), entry.reason]));
184
+ const approvedFingerprint = selected.descriptor.fingerprint;
185
+ const validateReview = async () => {
186
+ const current = await prepareUpdateReview(await inspectProjectUpdate(projectRoot, { runner: context.runner }), force, { runner: context.runner, now: context.updateNow?.() ?? new Date() });
187
+ if (!current.summary.eligible || current.descriptor.fingerprint !== approvedFingerprint) {
188
+ throw new UpdatePlanError('The effective update plan changed after review.', 'preview-mismatch', `Run ${formatUpdateCommand(projectRoot, 'check')} again.`);
277
189
  }
278
- }
279
- for (const group of provisioningPlans) {
280
- presentation.bullets(`Project component provisioning: ${group.group}`, [
281
- ...(group.reason ? [group.reason] : []),
282
- ...group.entries.map((entry) => `${entry.status} ${manifestDisplayPath(entry.rendered.pathParts)} ${entry.reason}`)
283
- ]);
284
- }
285
- const manifestMaintenance = [
286
- ownershipMigrationPending
287
- ? 'release legacy project artifacts from Liftoff update authority in manifest schema v7; no production file will be written'
288
- : undefined,
289
- workloadIntentChanged
290
- ? 'record the requested project configuration intent after safe provisioning'
291
- : undefined
292
- ].filter((item) => item !== undefined);
293
- if (manifestMaintenance.length > 0) {
294
- presentation.bullets('Manifest maintenance', manifestMaintenance);
295
- }
296
- if (stateMigration.status === 'migrate') {
297
- presentation.bullets('Activation-state migration', [
298
- `${stateMigration.report.path}: explicit compatibility mapping preserves evidence bytes and stages ${stateMigration.report.reconciliationPath}`
299
- ]);
300
- }
301
- if (reconciliation.status === 'reconciliation-required') {
302
- presentation.bullets('Reconciliation required after update', [
303
- ...reconciliation.changedIdentityFields.map((field) => `${field.field}: ${JSON.stringify(field.from)} -> ${JSON.stringify(field.to)}`),
304
- `invalid phases: ${reconciliation.phaseImpact.invalidPhaseIds.join(', ') || 'none'}`,
305
- reconciliation.remedy ?? 'Run governance status/verify before executing affected phases.'
306
- ]);
307
- }
308
- const toWrite = summary.new +
309
- summary.missing +
310
- summary.upgrade +
311
- summary.moved +
312
- summary.refresh +
313
- provisioningPlans.reduce((count, group) => count + (group.blocked
314
- ? 0
315
- : group.entries.filter((entry) => entry.status === 'create').length), 0) +
316
- (manifestRewritePending ? 1 : 0) +
317
- stateMigration.mutations.length;
318
- const toRetire = summary.retired;
319
- presentation.status('warning', 'Liftoff core maintenance available', `${toWrite} to write, ${toRetire} retired alias ownership record(s) to remove (${summary.retiredRemoved} file deletion(s)), ${summary.conflict + summary.retiredConflict} core conflict(s), ${summary.orphan} core orphan(s), ${summary.unchanged} core unchanged`);
320
- if (toWrite > 0 || toRetire > 0) {
321
- presentation.command('liftoff update');
322
- }
323
- const unownedConflicts = entries.filter((entry) => isUnownedUpdateConflict(entry, oldByName));
324
- if (unownedConflicts.length > 0) {
325
- presentation.bullets('Unowned destinations remain protected', unownedConflicts.map((entry) => `${entryDisplay(entry)}: review and resolve this unowned destination manually; --force cannot overwrite it`));
326
- }
327
- if (entries.some((entry) => !isUnownedUpdateConflict(entry, oldByName) &&
328
- (entry.status === 'conflict' || entry.status === 'retired-conflict' ||
329
- entry.status === 'moved' && !entry.cleanMove))) {
330
- presentation.command('liftoff update --force');
331
- }
332
- return 2;
333
- }
334
- const blockedProvisioning = provisioningPlans.filter((group) => group.blocked);
335
- if (isDirtyGitWorktree(projectRoot)) {
336
- const warning = 'The project worktree has uncommitted changes; consider committing before applying.';
337
- if (jsonMode) {
338
- presentation.rawStderr(`Warning: ${warning}\n`);
339
- }
340
- else {
341
- presentation.warning(warning);
342
- }
343
- }
344
- presentation.stage('Apply safe Liftoff core changes', projectRoot);
345
- await preflightUpdate(projectRoot, entries, force, oldByName);
346
- maybeInjectUpdateFailure(context.env, 'after-preflight');
347
- const written = [];
348
- const retired = [];
349
- const skipped = [];
350
- const mutations = [];
351
- for (const entry of entries) {
352
- switch (entry.status) {
353
- case 'new':
354
- case 'missing':
355
- case 'upgrade':
356
- mutations.push({
357
- type: 'write',
358
- pathParts: entry.pathParts,
359
- content: entry.rendered.content
360
- });
361
- written.push(entry);
362
- break;
363
- case 'moved':
364
- if (!isUnownedUpdateConflict(entry, oldByName) && (entry.cleanMove || force)) {
365
- if (!entry.destinationMatches) {
366
- mutations.push({
367
- type: 'write',
368
- pathParts: entry.pathParts,
369
- content: entry.rendered.content
370
- });
371
- }
372
- mutations.push({ type: 'delete', pathParts: entry.previousPathParts });
373
- written.push(entry);
374
- }
375
- else {
376
- skipped.push(entry);
377
- }
378
- break;
379
- case 'retired':
380
- mutations.push({ type: 'delete', pathParts: entry.pathParts });
381
- retired.push(entry);
382
- break;
383
- case 'retired-conflict':
384
- if (force) {
385
- mutations.push({ type: 'delete', pathParts: entry.pathParts });
386
- retired.push(entry);
387
- }
388
- else {
389
- skipped.push(entry);
390
- }
391
- break;
392
- case 'conflict':
393
- if (force && !isUnownedUpdateConflict(entry, oldByName)) {
394
- mutations.push({
395
- type: 'write',
396
- pathParts: entry.pathParts,
397
- content: entry.rendered.content
398
- });
399
- if (entry.previousPathParts) {
400
- mutations.push({ type: 'delete', pathParts: entry.previousPathParts });
401
- }
402
- written.push(entry);
403
- }
404
- else {
405
- skipped.push(entry);
190
+ };
191
+ const cleanupFailures = [];
192
+ if (materialized.mutations.length) {
193
+ const activeInspection = inspection;
194
+ const outcome = await applyReviewedUpdateTransaction(projectRoot, materialized.mutations, {
195
+ planFingerprint: approvedFingerprint,
196
+ approvalStore: createUpdateTransactionApprovalStore(projectRoot, options),
197
+ preconditions: selected.preconditions,
198
+ validatePlan: validateReview,
199
+ onBeforeMutation: async (mutation, index) => {
200
+ maybeInjectUpdateFailure(context.env, `before-mutation:${index}`);
201
+ maybeInjectUpdateFailure(context.env, `before-path:${mutation.pathParts.join('/')}`);
202
+ await verifyHistoryBeforeReplacement(activeInspection, mutation);
406
203
  }
407
- break;
408
- default:
409
- break;
410
- }
411
- }
412
- const provisioned = [];
413
- for (const group of provisioningPlans) {
414
- if (group.blocked) {
415
- continue;
416
- }
417
- for (const entry of group.entries) {
418
- if (entry.status === 'create') {
419
- mutations.push({
420
- type: 'write',
421
- pathParts: entry.rendered.pathParts,
422
- content: entry.rendered.content
423
- });
204
+ });
205
+ committed = outcome.committed;
206
+ cleanupFailures.push(...outcome.cleanupFailures);
207
+ if (!committed) {
208
+ throw new UpdatePlanError(outcome.rollbackFailures.join('; ') || 'The approved update did not commit.', 'transaction-failed', 'Review the recovery details before running a new check.');
424
209
  }
425
- provisioned.push(entry);
210
+ if (migration.status === 'available')
211
+ migration = { ...migration, status: 'committed' };
426
212
  }
427
- }
428
- for (const mutation of stateMigration.mutations) {
429
- mutations.push(mutation);
430
- }
431
- const skippedByName = new Map(skipped.map((entry) => [entry.logicalName, entry]));
432
- const hasUnrecordedGovernanceConflict = skipped.some((entry) => entry.status === 'conflict' &&
433
- entry.rendered?.category === 'governance' &&
434
- !oldByName.has(entry.logicalName));
435
- const hasProtectedRetiredAlias = skipped.some((entry) => entry.status === 'retired-conflict');
436
- const nextProjectArtifacts = appendProvisionedProjectArtifacts(manifest.projectArtifacts, provisioningPlans);
437
- const nextManifest = buildManifest(renderPlan, render.filter((artifact) => artifact.logicalName !== 'manifest'), {
438
- frameworkState: manifest.framework.state,
439
- projectArtifacts: nextProjectArtifacts
440
- });
441
- nextManifest.framework = manifest.framework;
442
- nextManifest.project.specWorkflow = manifest.project.specWorkflow;
443
- nextManifest.project.agents = manifest.project.agents;
444
- if (manifest.project.defaultAgent) {
445
- nextManifest.project.defaultAgent = manifest.project.defaultAgent;
446
- }
447
- else {
448
- delete nextManifest.project.defaultAgent;
449
- }
450
- if ((hasUnrecordedGovernanceConflict || hasProtectedRetiredAlias) &&
451
- nextManifest.governance.profile !== 'none') {
452
- nextManifest.governance.state = 'handoff-partial';
453
- }
454
- if (stateMigration.report.diagnosticOnly === true) {
455
- preserveDiagnosticGovernanceIdentity(nextManifest, manifest);
456
- }
457
- nextManifest.managedArtifacts = nextManifest.managedArtifacts.flatMap((artifact) => {
458
- if (!skippedByName.has(artifact.logicalName)) {
459
- return [artifact];
213
+ if (selected.needsRevalidation && !cleanupFailures.length) {
214
+ revalidation = await runUpdateRevalidation(inspection, selected, context, materialized.mutations.length ? undefined : validateReview);
460
215
  }
461
- const previous = oldByName.get(artifact.logicalName);
462
- if (!previous) {
463
- return [];
464
- }
465
- return [{ ...artifact, pathParts: previous.pathParts, contentHash: previous.contentHash }];
466
- });
467
- for (const entry of entries) {
468
- if (entry.status === 'orphan') {
469
- const previous = oldByName.get(entry.logicalName);
470
- if (plan.governanceProfile.id === 'none' &&
471
- previous.category === 'governance') {
472
- continue;
216
+ await consumeUpdatePreviewReceipt(projectRoot, stored.receipt, options);
217
+ const revalidationBlocked = revalidation.status === 'blocked';
218
+ const partial = revalidationBlocked || selected.writePlan.skipped.length > 0 ||
219
+ inspection.provisioningPlans.some((group) => group.blocked);
220
+ emit(context, jsonMode, {
221
+ ...base, migration, revalidation,
222
+ status: cleanupFailures.length ? 'failed' : partial ? 'partial' : 'applied',
223
+ reasonCode: cleanupFailures.length ? 'transaction-cleanup' :
224
+ revalidationBlocked ? 'revalidation-blocked' : 'approved-update-applied',
225
+ receipt: { status: 'consumed', path: location.receiptPath }, approval, committed,
226
+ warnings: cleanupFailures
227
+ }, inspection, selected);
228
+ if (!jsonMode) {
229
+ if (selected.writePlan.written.length) {
230
+ presentation.bullets('Applied Liftoff core changes', selected.writePlan.written.map((entry) => `wrote ${entryDisplay(entry)}`));
473
231
  }
474
- nextManifest.managedArtifacts.push(previous);
475
- }
476
- if (entry.status === 'retired-conflict' && !force) {
477
- const previous = oldByName.get(entry.logicalName);
478
- if (plan.governanceProfile.id === 'none' &&
479
- previous.category === 'governance') {
480
- continue;
232
+ if (selected.writePlan.retired.length) {
233
+ presentation.bullets('Removed retired Liftoff aliases', selected.writePlan.retired.map((entry) => `removed ${manifestDisplayPath(entry.pathParts)}`));
234
+ }
235
+ renderUpdateSkipped(presentation, inspection, selected.writePlan);
236
+ if (migration.status === 'committed') {
237
+ presentation.status('success', 'Activation migration committed', 'Original v1 history remains preserved; active v2 readiness is reported separately.');
238
+ }
239
+ if (revalidationBlocked) {
240
+ presentation.bullets('V2 revalidation is blocked and resumable', [
241
+ ...revalidation.issues,
242
+ ...(revalidation.nextPhase ? [`Next incomplete phase: ${revalidation.nextPhase}`] : []),
243
+ `Repair the blocker, run ${formatUpdateCommand(projectRoot, 'check')}, and approve the remaining local work.`
244
+ ]);
245
+ }
246
+ for (const failure of cleanupFailures)
247
+ presentation.error(failure);
248
+ if (!cleanupFailures.length && !revalidationBlocked) {
249
+ presentation.completion('Updated project', `${selected.writePlan.written.length} core written, ${selected.writePlan.skipped.length} core skipped`, [], formatUpdateValidationCommands(projectRoot));
481
250
  }
482
- nextManifest.managedArtifacts.push(previous);
483
251
  }
252
+ return cleanupFailures.length ? 1 : revalidationBlocked ? 2 : 0;
253
+ }
254
+ catch (error) {
255
+ const reasonCode = error instanceof UpdatePlanError ? error.reasonCode :
256
+ error instanceof UpdatePreviewError ? error.code : 'update-failed';
257
+ const detail = error instanceof Error ? error.message : String(error);
258
+ const message = error instanceof UpdatePreviewError && !committed
259
+ ? `${detail} No new project update was performed.` : detail;
260
+ const remedy = error instanceof UpdatePlanError ? error.remedy :
261
+ error instanceof UpdatePreviewError
262
+ ? `Repair any reported preview-storage issue, then run ${formatUpdateCommand(projectRoot, 'check')} before applying.`
263
+ : `Review the reported failure. Preserve concurrent edits and run ${formatUpdateCommand(projectRoot, 'check')} after repair.`;
264
+ emit(context, jsonMode, {
265
+ mode: check ? 'check' : 'apply', status: 'failed', reasonCode, projectRoot,
266
+ committed, message, remedy, migration, revalidation
267
+ }, inspection, selected);
268
+ return 1;
484
269
  }
485
- parseManifest(JSON.parse(JSON.stringify(nextManifest)));
486
- mutations.push({
487
- type: 'write',
488
- pathParts: ['liftoff.manifest.json'],
489
- content: `${JSON.stringify(nextManifest, null, 2)}\n`
490
- });
491
- assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans, stateMigration.mutations.map((mutation) => mutation.pathParts));
492
- await applyProjectFileTransaction(projectRoot, mutations, {
493
- onBeforeMutation: async (mutation, index) => {
494
- maybeInjectUpdateFailure(context.env, `before-mutation:${index}`);
495
- maybeInjectUpdateFailure(context.env, `before-path:${mutation.pathParts.join('/')}`);
496
- },
497
- preconditions: [
498
- ...selectUpdatePreconditions(updateSnapshots, entries, mutations, provisioned.map((entry) => entry.rendered.pathParts)),
499
- ...stateMigration.preconditions
500
- ]
501
- });
502
- if (jsonMode) {
503
- presentation.rawStdout(`${JSON.stringify({
504
- schemaVersion: 2,
505
- mode: 'apply',
506
- scope: 'managed-core',
507
- status: blockedProvisioning.length > 0 || skipped.length > 0
508
- ? 'partial'
509
- : reconciliation.status === 'reconciliation-required'
510
- ? 'reconciliation-required'
511
- : 'applied',
512
- cliVersion: liftoffVersion,
513
- projectVersion: manifest.liftoffVersion,
514
- written: written.map((entry) => manifestDisplayPath(entry.pathParts)),
515
- removed: retired.map((entry) => ({
516
- logicalName: entry.logicalName,
517
- status: entry.status === 'retired-conflict'
518
- ? 'force-retired'
519
- : entry.destinationOccupied === false
520
- ? 'retired-absent'
521
- : 'retired',
522
- path: manifestDisplayPath(entry.pathParts),
523
- fileDeleted: entry.destinationOccupied !== false,
524
- reason: entry.reason
525
- })),
526
- stateWritten: stateMigration.mutations.map((mutation) => manifestDisplayPath(mutation.pathParts)),
527
- skipped: skipped.map((entry) => ({
528
- logicalName: entry.logicalName,
529
- status: entry.status,
530
- path: manifestDisplayPath(entry.pathParts),
531
- reason: entry.reason
532
- })),
533
- provisioning: provisioningJson(provisioningPlans),
534
- ownershipMigrationPending,
535
- manifestChanges: manifestChanges(manifest, nextManifest),
536
- activationIdentity: nextManifest.governance.profile !== 'none' && nextManifest.governance.profile !== 'unspecified'
537
- ? nextManifest.governance.activationIdentity ?? null
538
- : null,
539
- activationStateMigration: stateMigration.report,
540
- reconciliation,
541
- summary
542
- }, null, 2)}\n`);
543
- return 0;
544
- }
545
- if (written.length > 0) {
546
- presentation.bullets('Applied Liftoff core changes', written.map((entry) => `wrote ${entryDisplay(entry)}`));
547
- }
548
- const retiredDeleted = retired.filter((entry) => entry.destinationOccupied !== false);
549
- const retiredAbsent = retired.filter((entry) => entry.destinationOccupied === false);
550
- if (retiredDeleted.length > 0) {
551
- presentation.bullets('Removed retired Liftoff aliases', retiredDeleted.map((entry) => `removed ${entryDisplay(entry)} ${entry.reason}`));
552
- }
553
- if (retiredAbsent.length > 0) {
554
- presentation.bullets('Retired absent Liftoff alias ownership', retiredAbsent.map((entry) => `removed manifest ownership for ${entryDisplay(entry)} ${entry.reason}`));
555
- }
556
- if (stateMigration.status === 'migrate') {
557
- presentation.bullets('Migrated activation state', stateMigration.mutations.map((mutation) => `wrote ${manifestDisplayPath(mutation.pathParts)}; evidence bytes preserved`));
558
- }
559
- if (provisioned.length > 0) {
560
- presentation.bullets('Provisioned project components', provisioned.map((entry) => `${entry.status === 'create' ? 'wrote' : 'adopted'} ${manifestDisplayPath(entry.rendered.pathParts)}`));
561
- }
562
- if (blockedProvisioning.length > 0) {
563
- presentation.bullets('Blocked project component provisioning', blockedProvisioning.flatMap((group) => [
564
- ...(group.reason ? [`${group.group}: ${group.reason}`] : []),
565
- ...group.entries
566
- .filter((entry) => entry.status === 'conflict')
567
- .map((entry) => `${group.group}: preserved ${manifestDisplayPath(entry.rendered.pathParts)}; --force cannot overwrite project-owned bytes`)
568
- ]));
569
- }
570
- if (skipped.length > 0) {
571
- presentation.bullets('Skipped Liftoff core conflicts', skipped.map((entry) => isUnownedUpdateConflict(entry, oldByName)
572
- ? `protected unowned destination ${entryDisplay(entry)} ${entry.reason} (resolve manually; --force cannot overwrite it)`
573
- : entry.status === 'retired-conflict'
574
- ? `protected retired alias ${entryDisplay(entry)} ${entry.reason}${force ? '' : ' (delete it manually or use --force to remove)'}`
575
- : `skipped ${entryDisplay(entry)} ${entry.reason}${force ? '' : ' (use --force to overwrite)'}`));
576
- }
577
- const orphans = entries.filter((entry) => entry.status === 'orphan');
578
- if (orphans.length > 0) {
579
- presentation.bullets('Orphaned Liftoff core artifacts', orphans.map((entry) => `orphan ${entryDisplay(entry)} ${entry.reason}`));
580
- }
581
- if (reconciliation.status === 'reconciliation-required') {
582
- presentation.bullets('Reconciliation required', [
583
- ...reconciliation.changedIdentityFields.map((field) => `${field.field}: ${JSON.stringify(field.from)} -> ${JSON.stringify(field.to)}`),
584
- `invalid phases: ${reconciliation.phaseImpact.invalidPhaseIds.join(', ') || 'none'}`,
585
- reconciliation.remedy ?? 'Run governance status/verify before executing affected phases.'
586
- ]);
587
- }
588
- presentation.completion('Updated project', [
589
- `${written.length} core written`,
590
- `${retired.length} retired ownership removed`,
591
- `${retiredDeleted.length} retired file deleted`,
592
- `${provisioned.length} project provisioned`,
593
- ...(blockedProvisioning.length > 0
594
- ? [`${blockedProvisioning.length} provisioning group(s) blocked`]
595
- : []),
596
- `${skipped.length} core skipped`
597
- ].join(', '), [{ label: 'Manifest version', value: liftoffVersion }], 'liftoff validate && liftoff doctor');
598
- return 0;
599
270
  }
600
271
  //# sourceMappingURL=use-case.js.map