@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.
- package/DEVELOPER.md +28 -10
- package/README.md +11 -6
- package/dist/adapters/filesystem/project-transaction.d.ts +2 -1
- package/dist/adapters/filesystem/project-transaction.js +25 -6
- package/dist/adapters/filesystem/project-transaction.js.map +1 -1
- package/dist/adapters/filesystem/reviewed-update-transaction.d.ts +53 -0
- package/dist/adapters/filesystem/reviewed-update-transaction.js +864 -0
- package/dist/adapters/filesystem/reviewed-update-transaction.js.map +1 -0
- package/dist/adapters/filesystem/update-previews.d.ts +50 -0
- package/dist/adapters/filesystem/update-previews.js +613 -0
- package/dist/adapters/filesystem/update-previews.js.map +1 -0
- package/dist/adapters/process/shell-command.d.ts +1 -0
- package/dist/adapters/process/shell-command.js +7 -1
- package/dist/adapters/process/shell-command.js.map +1 -1
- package/dist/application/context.d.ts +5 -0
- package/dist/application/diagnose/doctor.js +9 -15
- package/dist/application/diagnose/doctor.js.map +1 -1
- package/dist/application/update/approval.d.ts +39 -0
- package/dist/application/update/approval.js +54 -0
- package/dist/application/update/approval.js.map +1 -0
- package/dist/application/update/command-guidance.d.ts +2 -0
- package/dist/application/update/command-guidance.js +14 -0
- package/dist/application/update/command-guidance.js.map +1 -0
- package/dist/application/update/inspection.d.ts +37 -0
- package/dist/application/update/inspection.js +235 -0
- package/dist/application/update/inspection.js.map +1 -0
- package/dist/application/update/migration-runtime.d.ts +14 -0
- package/dist/application/update/migration-runtime.js +178 -0
- package/dist/application/update/migration-runtime.js.map +1 -0
- package/dist/application/update/output.d.ts +134 -0
- package/dist/application/update/output.js +182 -0
- package/dist/application/update/output.js.map +1 -0
- package/dist/application/update/preview.d.ts +49 -0
- package/dist/application/update/preview.js +220 -0
- package/dist/application/update/preview.js.map +1 -0
- package/dist/application/update/protected-source.d.ts +25 -0
- package/dist/application/update/protected-source.js +81 -0
- package/dist/application/update/protected-source.js.map +1 -0
- package/dist/application/update/reporting.d.ts +4 -4
- package/dist/application/update/reporting.js +14 -13
- package/dist/application/update/reporting.js.map +1 -1
- package/dist/application/update/revalidation-plan.d.ts +22 -0
- package/dist/application/update/revalidation-plan.js +108 -0
- package/dist/application/update/revalidation-plan.js.map +1 -0
- package/dist/application/update/revalidation.d.ts +70 -0
- package/dist/application/update/revalidation.js +337 -0
- package/dist/application/update/revalidation.js.map +1 -0
- package/dist/application/update/review-plan.d.ts +25 -0
- package/dist/application/update/review-plan.js +89 -0
- package/dist/application/update/review-plan.js.map +1 -0
- package/dist/application/update/transaction-approval.d.ts +29 -0
- package/dist/application/update/transaction-approval.js +82 -0
- package/dist/application/update/transaction-approval.js.map +1 -0
- package/dist/application/update/use-case.d.ts +1 -0
- package/dist/application/update/use-case.js +240 -569
- package/dist/application/update/use-case.js.map +1 -1
- package/dist/application/update/write-plan.d.ts +15 -0
- package/dist/application/update/write-plan.js +115 -0
- package/dist/application/update/write-plan.js.map +1 -0
- package/dist/cli/args/definitions.js +5 -4
- package/dist/cli/args/definitions.js.map +1 -1
- package/dist/cli/args/help.js +13 -1
- package/dist/cli/args/help.js.map +1 -1
- package/dist/cli/args/parser.js +22 -7
- package/dist/cli/args/parser.js.map +1 -1
- package/dist/cli/commands/update.js +1 -0
- package/dist/cli/commands/update.js.map +1 -1
- package/dist/domain/governance/policy/identity.d.ts +1 -1
- package/dist/domain/governance/policy/identity.js +2 -2
- package/dist/domain/governance/policy/identity.js.map +1 -1
- package/dist/domain/project/infrastructure-layout.d.ts +1 -1
- package/dist/domain/project/infrastructure-layout.js +2 -1
- package/dist/domain/project/infrastructure-layout.js.map +1 -1
- package/dist/domain/project/reviewed-update-artifacts.d.ts +2 -0
- package/dist/domain/project/reviewed-update-artifacts.js +3 -0
- package/dist/domain/project/reviewed-update-artifacts.js.map +1 -0
- package/dist/generators/common/base.js +5 -1
- package/dist/generators/common/base.js.map +1 -1
- package/dist/governance-activation/activation-state.js +4 -1
- package/dist/governance-activation/activation-state.js.map +1 -1
- package/dist/governance-activation/commands.d.ts +5 -0
- package/dist/governance-activation/commands.js +65 -3
- package/dist/governance-activation/commands.js.map +1 -1
- package/dist/governance-activation/compatibility.d.ts +21 -4
- package/dist/governance-activation/compatibility.js +47 -8
- package/dist/governance-activation/compatibility.js.map +1 -1
- package/dist/governance-activation/doctor.js +32 -5
- package/dist/governance-activation/doctor.js.map +1 -1
- package/dist/governance-activation/historical-state.d.ts +81 -0
- package/dist/governance-activation/historical-state.js +817 -0
- package/dist/governance-activation/historical-state.js.map +1 -0
- package/dist/governance-activation/history-contracts.d.ts +109 -0
- package/dist/governance-activation/history-contracts.js +303 -0
- package/dist/governance-activation/history-contracts.js.map +1 -0
- package/dist/governance-activation/inputs.d.ts +2 -0
- package/dist/governance-activation/inputs.js +13 -1
- package/dist/governance-activation/inputs.js.map +1 -1
- package/dist/governance-activation/migration-history.d.ts +97 -0
- package/dist/governance-activation/migration-history.js +388 -0
- package/dist/governance-activation/migration-history.js.map +1 -0
- package/dist/governance-activation/migration.js +1 -1
- package/dist/governance-activation/migration.js.map +1 -1
- package/dist/governance-activation/proof-records.d.ts +3 -0
- package/dist/governance-activation/proof-records.js +78 -0
- package/dist/governance-activation/proof-records.js.map +1 -0
- package/dist/governance-activation/read-only.d.ts +4 -4
- package/dist/governance-activation/read-only.js +9 -69
- package/dist/governance-activation/read-only.js.map +1 -1
- package/dist/governance-activation/seed-lifecycle.d.ts +28 -6
- package/dist/governance-activation/seed-lifecycle.js +258 -25
- package/dist/governance-activation/seed-lifecycle.js.map +1 -1
- package/dist/governance-activation/transition-planning.d.ts +2 -0
- package/dist/governance-activation/transition-planning.js +12 -3
- package/dist/governance-activation/transition-planning.js.map +1 -1
- package/dist/governance-activation/transitions.d.ts +8 -2
- package/dist/governance-activation/transitions.js +36 -11
- package/dist/governance-activation/transitions.js.map +1 -1
- package/dist/governance-assessment/engine.js +30 -3
- package/dist/governance-assessment/engine.js.map +1 -1
- package/dist/governance-assessment/project.d.ts +7 -0
- package/dist/governance-assessment/project.js +44 -4
- package/dist/governance-assessment/project.js.map +1 -1
- package/dist/repository-governance.js +25 -12
- package/dist/repository-governance.js.map +1 -1
- package/docs/cli-reference.md +101 -28
- package/docs/configuration-and-manifests.md +27 -15
- package/docs/existing-repositories.md +19 -10
- package/docs/repository-governance.md +9 -5
- package/docs/safety-and-consent.md +27 -18
- package/docs/troubleshooting.md +9 -7
- package/docs/workloads.md +5 -4
- package/package.json +1 -1
|
@@ -1,600 +1,271 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import {
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
35
|
-
presentation.commandIdentity('update', '
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
await
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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 (
|
|
168
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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.
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
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
|
-
|
|
210
|
+
if (migration.status === 'available')
|
|
211
|
+
migration = { ...migration, status: 'committed' };
|
|
426
212
|
}
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
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
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
|
|
480
|
-
|
|
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
|