@msn-control/liftoff 0.9.9 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/DEVELOPER.md +142 -0
  2. package/README.md +26 -18
  3. package/assets/governance/single-maintainer-gitflow/policy.md +13 -11
  4. package/dist/args.js +21 -0
  5. package/dist/args.js.map +1 -1
  6. package/dist/artifact-lifecycle.d.ts +1 -1
  7. package/dist/artifact-lifecycle.js +5 -0
  8. package/dist/artifact-lifecycle.js.map +1 -1
  9. package/dist/commands.js +355 -13
  10. package/dist/commands.js.map +1 -1
  11. package/dist/file-system.js +100 -13
  12. package/dist/file-system.js.map +1 -1
  13. package/dist/governance-activation/activation-state.d.ts +32 -0
  14. package/dist/governance-activation/activation-state.js +165 -0
  15. package/dist/governance-activation/activation-state.js.map +1 -0
  16. package/dist/governance-activation/approvals.d.ts +188 -0
  17. package/dist/governance-activation/approvals.js +508 -0
  18. package/dist/governance-activation/approvals.js.map +1 -0
  19. package/dist/governance-activation/canonical-json.d.ts +3 -0
  20. package/dist/governance-activation/canonical-json.js +36 -0
  21. package/dist/governance-activation/canonical-json.js.map +1 -0
  22. package/dist/governance-activation/commands.d.ts +10 -0
  23. package/dist/governance-activation/commands.js +1023 -0
  24. package/dist/governance-activation/commands.js.map +1 -0
  25. package/dist/governance-activation/compatibility.d.ts +68 -0
  26. package/dist/governance-activation/compatibility.js +447 -0
  27. package/dist/governance-activation/compatibility.js.map +1 -0
  28. package/dist/governance-activation/credentials.d.ts +160 -0
  29. package/dist/governance-activation/credentials.js +403 -0
  30. package/dist/governance-activation/credentials.js.map +1 -0
  31. package/dist/governance-activation/doctor.d.ts +10 -0
  32. package/dist/governance-activation/doctor.js +379 -0
  33. package/dist/governance-activation/doctor.js.map +1 -0
  34. package/dist/governance-activation/evidence.d.ts +49 -0
  35. package/dist/governance-activation/evidence.js +246 -0
  36. package/dist/governance-activation/evidence.js.map +1 -0
  37. package/dist/governance-activation/graph.d.ts +397 -0
  38. package/dist/governance-activation/graph.js +441 -0
  39. package/dist/governance-activation/graph.js.map +1 -0
  40. package/dist/governance-activation/identity.d.ts +35 -0
  41. package/dist/governance-activation/identity.js +89 -0
  42. package/dist/governance-activation/identity.js.map +1 -0
  43. package/dist/governance-activation/index.d.ts +18 -0
  44. package/dist/governance-activation/index.js +19 -0
  45. package/dist/governance-activation/index.js.map +1 -0
  46. package/dist/governance-activation/migration.d.ts +44 -0
  47. package/dist/governance-activation/migration.js +268 -0
  48. package/dist/governance-activation/migration.js.map +1 -0
  49. package/dist/governance-activation/readiness.d.ts +23 -0
  50. package/dist/governance-activation/readiness.js +158 -0
  51. package/dist/governance-activation/readiness.js.map +1 -0
  52. package/dist/governance-activation/reconciliation.d.ts +11 -0
  53. package/dist/governance-activation/reconciliation.js +58 -0
  54. package/dist/governance-activation/reconciliation.js.map +1 -0
  55. package/dist/governance-activation/release-integrity.d.ts +11 -0
  56. package/dist/governance-activation/release-integrity.js +49 -0
  57. package/dist/governance-activation/release-integrity.js.map +1 -0
  58. package/dist/governance-activation/seed-lifecycle.d.ts +116 -0
  59. package/dist/governance-activation/seed-lifecycle.js +609 -0
  60. package/dist/governance-activation/seed-lifecycle.js.map +1 -0
  61. package/dist/governance-activation/source-of-truth.d.ts +164 -0
  62. package/dist/governance-activation/source-of-truth.js +986 -0
  63. package/dist/governance-activation/source-of-truth.js.map +1 -0
  64. package/dist/governance-activation/task-projection.d.ts +21 -0
  65. package/dist/governance-activation/task-projection.js +75 -0
  66. package/dist/governance-activation/task-projection.js.map +1 -0
  67. package/dist/governance-activation/transitions.d.ts +163 -0
  68. package/dist/governance-activation/transitions.js +2128 -0
  69. package/dist/governance-activation/transitions.js.map +1 -0
  70. package/dist/governance-activation/type-contract.d.ts +1 -0
  71. package/dist/governance-activation/type-contract.js +27 -0
  72. package/dist/governance-activation/type-contract.js.map +1 -0
  73. package/dist/governance-activation/types.d.ts +390 -0
  74. package/dist/governance-activation/types.js +108 -0
  75. package/dist/governance-activation/types.js.map +1 -0
  76. package/dist/governance-activation/validators.d.ts +17 -0
  77. package/dist/governance-activation/validators.js +1305 -0
  78. package/dist/governance-activation/validators.js.map +1 -0
  79. package/dist/init-filesystem.js +5 -1
  80. package/dist/init-filesystem.js.map +1 -1
  81. package/dist/planner.js +3 -3
  82. package/dist/planner.js.map +1 -1
  83. package/dist/power-apps-templates.js +14 -1
  84. package/dist/power-apps-templates.js.map +1 -1
  85. package/dist/process-runner.d.ts +3 -0
  86. package/dist/process-runner.js +68 -10
  87. package/dist/process-runner.js.map +1 -1
  88. package/dist/repository-governance.d.ts +13 -3
  89. package/dist/repository-governance.js +357 -51
  90. package/dist/repository-governance.js.map +1 -1
  91. package/dist/telemetry/contract.d.ts +1 -1
  92. package/dist/telemetry/contract.js +6 -0
  93. package/dist/telemetry/contract.js.map +1 -1
  94. package/dist/templates.js +320 -25
  95. package/dist/templates.js.map +1 -1
  96. package/dist/types.d.ts +14 -6
  97. package/docs/cli-reference.md +60 -12
  98. package/docs/configuration-and-manifests.md +40 -16
  99. package/docs/existing-repositories.md +22 -11
  100. package/docs/getting-started.md +46 -10
  101. package/docs/project-structure.md +34 -5
  102. package/docs/repository-governance.md +200 -245
  103. package/docs/safety-and-consent.md +24 -2
  104. package/docs/troubleshooting.md +55 -4
  105. package/docs/workloads.md +1 -1
  106. package/package.json +2 -1
package/dist/commands.js CHANGED
@@ -26,12 +26,19 @@ import { lookupStableRelease } from './stable-release.js';
26
26
  import { supportedStack } from './supported-stack.js';
27
27
  import { buildArtifacts, buildManifest, partitionGeneratedArtifacts } from './templates.js';
28
28
  import { PresentationSession } from './terminal.js';
29
+ import { governanceCommand } from './governance-activation/commands.js';
30
+ import { loadActivationState as loadCurrentActivationState } from './governance-activation/activation-state.js';
31
+ import { planHistoricalActivationStateMigration, updateFailureInjectionEnv } from './governance-activation/migration.js';
32
+ import { currentActivationIdentity } from './governance-activation/graph.js';
33
+ import { governanceChangeMetadataFileName, reconcileActiveGovernanceChange, validateGovernanceChangeMetadata } from './governance-activation/source-of-truth.js';
34
+ import { governanceDoctorChecks } from './governance-activation/doctor.js';
29
35
  import { liftoffVersion } from './version.js';
30
36
  import { blockingReadinessFailures, detectHostEnvironment, installRequirement, probeWorkstation, selectLiftoffRuntimeRequirements, selectWorkstationRequirements } from './workstation.js';
31
37
  export async function runCommand(parsed, context) {
32
38
  const helpRequested = parsed.command !== undefined && readBooleanFlag(parsed.flags, 'help') === true;
33
39
  const jsonMode = !helpRequested && (parsed.command === 'doctor' ||
34
40
  parsed.command === 'update' ||
41
+ parsed.command === 'governance' ||
35
42
  parsed.command === 'upgrade' ||
36
43
  parsed.command === 'validate') &&
37
44
  readBooleanFlag(parsed.flags, 'json') === true;
@@ -81,6 +88,8 @@ export async function runCommand(parsed, context) {
81
88
  return await migrateCommand(parsed, executionContext);
82
89
  case 'doctor':
83
90
  return await doctorCommand(parsed, executionContext);
91
+ case 'governance':
92
+ return await governanceCommand(parsed, executionContext);
84
93
  case 'dev':
85
94
  return helperCommand(parsed, executionContext, 'docker compose');
86
95
  case 'infra':
@@ -239,9 +248,11 @@ async function initCommand(parsed, context) {
239
248
  label: 'Repository governance',
240
249
  value: plan.governanceProfile.id === 'none'
241
250
  ? 'Disabled'
242
- : 'Handoff generated; commit and push before read-only Phase 0'
251
+ : 'Deterministic setup generated; run /liftoff-setup next'
243
252
  }
244
- ], `liftoff validate ${JSON.stringify(target.root)}`);
253
+ ], plan.governanceProfile.id === 'none'
254
+ ? `liftoff validate ${JSON.stringify(target.root)}`
255
+ : '/liftoff-setup');
245
256
  return 0;
246
257
  }
247
258
  finally {
@@ -718,7 +729,11 @@ async function executeMigration(parsed, context, sourceRoot) {
718
729
  await assertSafeInitTarget(target, parentDir);
719
730
  await assertNewOrEmptyDirectory(targetRoot);
720
731
  presentation.stage('Check workstation readiness');
721
- const readinessRoot = await mkdtemp(path.join(os.tmpdir(), 'liftoff-migrate-readiness-'));
732
+ const readinessParent = process.env.LIFTOFF_STAGING_ROOT
733
+ ? path.resolve(process.env.LIFTOFF_STAGING_ROOT)
734
+ : os.tmpdir();
735
+ await mkdir(readinessParent, { recursive: true });
736
+ const readinessRoot = await mkdtemp(path.join(readinessParent, 'liftoff-migrate-readiness-'));
722
737
  const readiness = await (async () => {
723
738
  try {
724
739
  return await ensureWorkstationReady(plan, options, context, runner, presentation, prompter, `liftoff migrate ${JSON.stringify(sourceRoot)}`, readinessRoot);
@@ -1098,9 +1113,10 @@ function selectUpdatePreconditions(snapshots, entries, mutations, additionalPath
1098
1113
  }
1099
1114
  return snapshots.filter((snapshot) => requiredKeys.has(updateSnapshotKey(snapshot.pathParts)));
1100
1115
  }
1101
- function assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans) {
1116
+ function assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans, additionalAuthorizedPaths = []) {
1102
1117
  const authorized = new Set([
1103
- updateSnapshotKey(['liftoff.manifest.json'])
1118
+ updateSnapshotKey(['liftoff.manifest.json']),
1119
+ ...additionalAuthorizedPaths.map((pathParts) => updateSnapshotKey(pathParts))
1104
1120
  ]);
1105
1121
  for (const entry of entries) {
1106
1122
  if (entry.rendered && entry.rendered.lifecycle !== 'managed-core') {
@@ -1127,6 +1143,232 @@ function assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans)
1127
1143
  }
1128
1144
  }
1129
1145
  }
1146
+ function identityFieldChanges(from, to = currentActivationIdentity) {
1147
+ return Object.keys(to)
1148
+ .filter((field) => from[field] !== undefined && from[field] !== to[field])
1149
+ .map((field) => ({
1150
+ field: `activationIdentity.${field}`,
1151
+ from: from[field],
1152
+ to: to[field]
1153
+ }));
1154
+ }
1155
+ function manifestChanges(manifest, plannedManifest) {
1156
+ const changes = [];
1157
+ if (manifest.artifactVersion !== plannedManifest.artifactVersion) {
1158
+ changes.push({
1159
+ field: 'artifactVersion',
1160
+ from: manifest.artifactVersion,
1161
+ to: plannedManifest.artifactVersion
1162
+ });
1163
+ }
1164
+ if (manifest.liftoffVersion !== plannedManifest.liftoffVersion) {
1165
+ changes.push({
1166
+ field: 'liftoffVersion',
1167
+ from: manifest.liftoffVersion,
1168
+ to: plannedManifest.liftoffVersion
1169
+ });
1170
+ }
1171
+ if (manifest.governance.profile !== 'none' &&
1172
+ manifest.governance.profile !== 'unspecified' &&
1173
+ plannedManifest.governance.profile !== 'none' &&
1174
+ plannedManifest.governance.profile !== 'unspecified') {
1175
+ if (manifest.governance.policyVersion !== plannedManifest.governance.policyVersion) {
1176
+ changes.push({
1177
+ field: 'governance.policyVersion',
1178
+ from: manifest.governance.policyVersion,
1179
+ to: plannedManifest.governance.policyVersion
1180
+ });
1181
+ }
1182
+ const fromIdentity = manifest.governance.activationIdentity;
1183
+ const toIdentity = plannedManifest.governance.activationIdentity;
1184
+ if (toIdentity) {
1185
+ if (!fromIdentity) {
1186
+ changes.push({
1187
+ field: 'governance.activationIdentity',
1188
+ from: null,
1189
+ to: toIdentity
1190
+ });
1191
+ }
1192
+ else {
1193
+ changes.push(...identityFieldChanges(fromIdentity, toIdentity));
1194
+ }
1195
+ }
1196
+ }
1197
+ return changes;
1198
+ }
1199
+ function stateMigrationReconciliation(stateMigration) {
1200
+ if (stateMigration.status === 'blocked') {
1201
+ return {
1202
+ status: 'blocked',
1203
+ changedIdentityFields: [],
1204
+ phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1205
+ issues: stateMigration.report.issues,
1206
+ remedy: 'Preserve governance/activation-state.json byte-for-byte and provide an explicit versioned import mapping; checkboxes, filenames, and prose cannot become evidence.'
1207
+ };
1208
+ }
1209
+ if (stateMigration.status === 'migrate') {
1210
+ return {
1211
+ status: 'reconciliation-required',
1212
+ changedIdentityFields: identityFieldChanges(stateMigration.report.fromIdentity, stateMigration.report.toIdentity),
1213
+ phaseImpact: {
1214
+ preservedPhaseIds: stateMigration.report.preservedPhaseIds,
1215
+ invalidPhaseIds: stateMigration.report.invalidPhaseIds
1216
+ },
1217
+ issues: [
1218
+ `Historical activation state will be migrated with the managed update transaction; evidence bytes are preserved and ${stateMigration.report.reconciliationPath} records the explicit graph mapping.`
1219
+ ],
1220
+ remedy: 'Run governance status/verify after update and acknowledge the reconciliation record before executing affected phases.'
1221
+ };
1222
+ }
1223
+ return {
1224
+ status: 'not-required',
1225
+ changedIdentityFields: [],
1226
+ phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1227
+ issues: []
1228
+ };
1229
+ }
1230
+ function activeChangePathParts(stateKind, changeId) {
1231
+ return stateKind === 'openspec'
1232
+ ? ['openspec', 'changes', changeId]
1233
+ : ['specs', changeId];
1234
+ }
1235
+ async function activeChangeReconciliationReport(projectRoot) {
1236
+ let loaded;
1237
+ try {
1238
+ loaded = await loadCurrentActivationState(projectRoot);
1239
+ }
1240
+ catch {
1241
+ return {
1242
+ status: 'not-required',
1243
+ changedIdentityFields: [],
1244
+ phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1245
+ issues: []
1246
+ };
1247
+ }
1248
+ const activeChange = loaded?.state.activeChange;
1249
+ if (!activeChange) {
1250
+ return {
1251
+ status: 'not-required',
1252
+ changedIdentityFields: [],
1253
+ phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1254
+ issues: []
1255
+ };
1256
+ }
1257
+ const metadataPathParts = [
1258
+ ...activeChangePathParts(activeChange.kind, activeChange.id),
1259
+ governanceChangeMetadataFileName
1260
+ ];
1261
+ const bytes = await readProjectFile(projectRoot, metadataPathParts);
1262
+ if (bytes === undefined) {
1263
+ return {
1264
+ status: 'blocked',
1265
+ changedIdentityFields: [],
1266
+ phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1267
+ issues: [
1268
+ `${metadataPathParts.join('/')} is missing for active change ${activeChange.id}; no update mode can infer it from tasks or prose.`
1269
+ ],
1270
+ remedy: 'Restore schema-valid governance metadata or record an explicit supersession before continuing setup.'
1271
+ };
1272
+ }
1273
+ let parsed;
1274
+ try {
1275
+ parsed = JSON.parse(bytes.toString('utf8'));
1276
+ }
1277
+ catch (error) {
1278
+ return {
1279
+ status: 'blocked',
1280
+ changedIdentityFields: [],
1281
+ phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1282
+ issues: [`Unable to parse ${metadataPathParts.join('/')}: ${error instanceof Error ? error.message : String(error)}`],
1283
+ remedy: 'Restore schema-valid governance metadata; no update mode can infer active governance from checkboxes, filenames, or prose.'
1284
+ };
1285
+ }
1286
+ let reconciliation;
1287
+ try {
1288
+ const metadata = validateGovernanceChangeMetadata(parsed);
1289
+ reconciliation = reconcileActiveGovernanceChange({ metadata, evidence: [] });
1290
+ }
1291
+ catch (error) {
1292
+ return {
1293
+ status: 'blocked',
1294
+ changedIdentityFields: [],
1295
+ phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1296
+ issues: [`Invalid ${metadataPathParts.join('/')}: ${error instanceof Error ? error.message : String(error)}`],
1297
+ remedy: 'Restore schema-valid governance metadata; no update mode can infer active governance from checkboxes, filenames, or prose.'
1298
+ };
1299
+ }
1300
+ if (reconciliation.status === 'not-required') {
1301
+ return {
1302
+ status: 'not-required',
1303
+ changedIdentityFields: [],
1304
+ phaseImpact: {
1305
+ preservedPhaseIds: reconciliation.preservedPhaseIds,
1306
+ invalidPhaseIds: reconciliation.invalidPhaseIds
1307
+ },
1308
+ issues: []
1309
+ };
1310
+ }
1311
+ if (reconciliation.status === 'blocked') {
1312
+ return {
1313
+ status: 'blocked',
1314
+ changedIdentityFields: [],
1315
+ phaseImpact: {
1316
+ preservedPhaseIds: reconciliation.preservedPhaseIds,
1317
+ invalidPhaseIds: reconciliation.invalidPhaseIds
1318
+ },
1319
+ issues: reconciliation.issues,
1320
+ remedy: 'Upgrade Liftoff or add an explicit compatibility mapping before executing affected governance phases.'
1321
+ };
1322
+ }
1323
+ return {
1324
+ status: 'reconciliation-required',
1325
+ changedIdentityFields: identityFieldChanges(reconciliation.fromIdentity, reconciliation.toIdentity),
1326
+ phaseImpact: {
1327
+ preservedPhaseIds: reconciliation.preservedPhaseIds,
1328
+ invalidPhaseIds: reconciliation.invalidPhaseIds
1329
+ },
1330
+ issues: reconciliation.issues,
1331
+ remedy: 'Managed definitions and manifest may be updated, but governance status/verify will block affected execution until the active change acknowledges the installed identity.'
1332
+ };
1333
+ }
1334
+ function combineReconciliationReports(reports) {
1335
+ if (reports.some((report) => report.status === 'blocked')) {
1336
+ return {
1337
+ status: 'blocked',
1338
+ changedIdentityFields: reports.flatMap((report) => report.changedIdentityFields),
1339
+ phaseImpact: {
1340
+ preservedPhaseIds: [],
1341
+ invalidPhaseIds: [...new Set(reports.flatMap((report) => report.phaseImpact.invalidPhaseIds))]
1342
+ },
1343
+ issues: reports.flatMap((report) => report.issues),
1344
+ remedy: reports.find((report) => report.remedy)?.remedy
1345
+ };
1346
+ }
1347
+ if (reports.some((report) => report.status === 'reconciliation-required')) {
1348
+ return {
1349
+ status: 'reconciliation-required',
1350
+ changedIdentityFields: reports.flatMap((report) => report.changedIdentityFields),
1351
+ phaseImpact: {
1352
+ preservedPhaseIds: [...new Set(reports.flatMap((report) => report.phaseImpact.preservedPhaseIds))],
1353
+ invalidPhaseIds: [...new Set(reports.flatMap((report) => report.phaseImpact.invalidPhaseIds))]
1354
+ },
1355
+ issues: reports.flatMap((report) => report.issues),
1356
+ remedy: reports.find((report) => report.remedy)?.remedy
1357
+ };
1358
+ }
1359
+ return {
1360
+ status: 'not-required',
1361
+ changedIdentityFields: [],
1362
+ phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1363
+ issues: []
1364
+ };
1365
+ }
1366
+ function maybeInjectUpdateFailure(env, stage) {
1367
+ const requested = env?.[updateFailureInjectionEnv] ?? process.env[updateFailureInjectionEnv];
1368
+ if (requested === stage) {
1369
+ throw new Error(`Injected managed-update failure at ${stage}.`);
1370
+ }
1371
+ }
1130
1372
  async function updateCommand(parsed, context) {
1131
1373
  const { presentation } = context;
1132
1374
  const check = readBooleanFlag(parsed.flags, 'check') ?? false;
@@ -1224,18 +1466,52 @@ async function updateCommand(parsed, context) {
1224
1466
  : await captureUpdateSnapshots(projectRoot, manifest, scopedRender, initialUpdateSnapshots);
1225
1467
  const entries = await reconcileProject(manifest, render, projectRoot);
1226
1468
  const summary = summarizeEntries(entries);
1227
- const ownershipMigrationPending = manifest.artifactVersion !== 6 ||
1469
+ const ownershipMigrationPending = manifest.artifactVersion !== 7 ||
1228
1470
  manifestHadFilteredLegacyNonDurableOwnership(manifest);
1229
1471
  const plannedManifest = buildManifest(renderPlan, render, {
1230
1472
  frameworkState: manifest.framework.state,
1231
1473
  projectArtifacts: manifest.projectArtifacts
1232
1474
  });
1475
+ const plannedManifestChanges = manifestChanges(manifest, plannedManifest);
1476
+ const stateMigration = await planHistoricalActivationStateMigration(projectRoot);
1477
+ const reconciliation = combineReconciliationReports([
1478
+ stateMigrationReconciliation(stateMigration),
1479
+ await activeChangeReconciliationReport(projectRoot)
1480
+ ]);
1481
+ if (reconciliation.status === 'blocked') {
1482
+ const blockedReport = {
1483
+ schemaVersion: 2,
1484
+ mode: check ? 'check' : 'apply',
1485
+ scope: 'managed-core',
1486
+ status: 'blocked',
1487
+ cliVersion: liftoffVersion,
1488
+ projectVersion: manifest.liftoffVersion,
1489
+ entries: [],
1490
+ provisioning: [],
1491
+ ownershipMigrationPending: false,
1492
+ manifestChanges: plannedManifestChanges,
1493
+ activationStateMigration: stateMigration.report,
1494
+ reconciliation,
1495
+ summary
1496
+ };
1497
+ if (jsonMode) {
1498
+ presentation.rawStdout(`${JSON.stringify(blockedReport, null, 2)}\n`);
1499
+ }
1500
+ else {
1501
+ presentation.status('error', 'Managed update blocked', reconciliation.issues.join('; '));
1502
+ if (reconciliation.remedy) {
1503
+ presentation.remedy(reconciliation.remedy);
1504
+ }
1505
+ }
1506
+ return 1;
1507
+ }
1233
1508
  const workloadIntentChanged = !sameWorkloadIntent(plannedManifest.project.workload, manifest.project.workload);
1234
1509
  const manifestRewritePending = ownershipMigrationPending ||
1235
1510
  workloadIntentChanged;
1236
1511
  const drift = hasDrift(entries) ||
1237
1512
  manifestRewritePending ||
1238
- provisioningPlans.length > 0;
1513
+ provisioningPlans.length > 0 ||
1514
+ stateMigration.status === 'migrate';
1239
1515
  const visible = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash);
1240
1516
  if (!drift) {
1241
1517
  if (jsonMode) {
@@ -1243,22 +1519,36 @@ async function updateCommand(parsed, context) {
1243
1519
  schemaVersion: 2,
1244
1520
  mode: 'check',
1245
1521
  scope: 'managed-core',
1522
+ status: reconciliation.status === 'reconciliation-required' ? 'reconciliation-required' : 'current',
1246
1523
  cliVersion: liftoffVersion,
1247
1524
  projectVersion: manifest.liftoffVersion,
1248
1525
  entries: [],
1249
1526
  provisioning: [],
1250
1527
  ownershipMigrationPending,
1528
+ manifestChanges: plannedManifestChanges,
1529
+ activationIdentity: manifest.governance.profile !== 'none' && manifest.governance.profile !== 'unspecified'
1530
+ ? manifest.governance.activationIdentity ?? null
1531
+ : null,
1532
+ activationStateMigration: stateMigration.report,
1533
+ reconciliation,
1251
1534
  summary
1252
1535
  } : {
1253
1536
  schemaVersion: 2,
1254
1537
  mode: 'apply',
1255
1538
  scope: 'managed-core',
1539
+ status: reconciliation.status === 'reconciliation-required' ? 'reconciliation-required' : 'current',
1256
1540
  cliVersion: liftoffVersion,
1257
1541
  projectVersion: manifest.liftoffVersion,
1258
1542
  written: [],
1259
1543
  skipped: [],
1260
1544
  provisioning: [],
1261
1545
  ownershipMigrationPending,
1546
+ manifestChanges: plannedManifestChanges,
1547
+ activationIdentity: manifest.governance.profile !== 'none' && manifest.governance.profile !== 'unspecified'
1548
+ ? manifest.governance.activationIdentity ?? null
1549
+ : null,
1550
+ activationStateMigration: stateMigration.report,
1551
+ reconciliation,
1262
1552
  summary
1263
1553
  }, null, 2)}\n`);
1264
1554
  return 0;
@@ -1287,6 +1577,12 @@ async function updateCommand(parsed, context) {
1287
1577
  })),
1288
1578
  provisioning: provisioningJson(provisioningPlans),
1289
1579
  ownershipMigrationPending,
1580
+ manifestChanges: plannedManifestChanges,
1581
+ activationIdentity: plannedManifest.governance.profile !== 'none' && plannedManifest.governance.profile !== 'unspecified'
1582
+ ? plannedManifest.governance.activationIdentity ?? null
1583
+ : null,
1584
+ activationStateMigration: stateMigration.report,
1585
+ reconciliation,
1290
1586
  summary
1291
1587
  }, null, 2)}\n`);
1292
1588
  return 2;
@@ -1308,7 +1604,7 @@ async function updateCommand(parsed, context) {
1308
1604
  }
1309
1605
  const manifestMaintenance = [
1310
1606
  ownershipMigrationPending
1311
- ? 'release legacy project artifacts from Liftoff update authority in manifest schema v6; no production file will be written'
1607
+ ? 'release legacy project artifacts from Liftoff update authority in manifest schema v7; no production file will be written'
1312
1608
  : undefined,
1313
1609
  workloadIntentChanged
1314
1610
  ? 'record the requested project configuration intent after safe provisioning'
@@ -1317,6 +1613,18 @@ async function updateCommand(parsed, context) {
1317
1613
  if (manifestMaintenance.length > 0) {
1318
1614
  presentation.bullets('Manifest maintenance', manifestMaintenance);
1319
1615
  }
1616
+ if (stateMigration.status === 'migrate') {
1617
+ presentation.bullets('Activation-state migration', [
1618
+ `${stateMigration.report.path}: explicit compatibility mapping preserves evidence bytes and stages ${stateMigration.report.reconciliationPath}`
1619
+ ]);
1620
+ }
1621
+ if (reconciliation.status === 'reconciliation-required') {
1622
+ presentation.bullets('Reconciliation required after update', [
1623
+ ...reconciliation.changedIdentityFields.map((field) => `${field.field}: ${JSON.stringify(field.from)} -> ${JSON.stringify(field.to)}`),
1624
+ `invalid phases: ${reconciliation.phaseImpact.invalidPhaseIds.join(', ') || 'none'}`,
1625
+ reconciliation.remedy ?? 'Run governance status/verify before executing affected phases.'
1626
+ ]);
1627
+ }
1320
1628
  const toWrite = summary.new +
1321
1629
  summary.missing +
1322
1630
  summary.upgrade +
@@ -1325,7 +1633,8 @@ async function updateCommand(parsed, context) {
1325
1633
  provisioningPlans.reduce((count, group) => count + (group.blocked
1326
1634
  ? 0
1327
1635
  : group.entries.filter((entry) => entry.status === 'create').length), 0) +
1328
- (manifestRewritePending ? 1 : 0);
1636
+ (manifestRewritePending ? 1 : 0) +
1637
+ stateMigration.mutations.length;
1329
1638
  presentation.status('warning', 'Liftoff core maintenance available', `${toWrite} to write, ${summary.conflict} core conflict(s), ${summary.orphan} core orphan(s), ${summary.unchanged} core unchanged`);
1330
1639
  if (toWrite > 0) {
1331
1640
  presentation.command('liftoff update');
@@ -1347,6 +1656,7 @@ async function updateCommand(parsed, context) {
1347
1656
  }
1348
1657
  presentation.stage('Apply safe Liftoff core changes', projectRoot);
1349
1658
  await preflightUpdate(projectRoot, entries, force);
1659
+ maybeInjectUpdateFailure(context.env, 'after-preflight');
1350
1660
  const written = [];
1351
1661
  const skipped = [];
1352
1662
  const mutations = [];
@@ -1414,6 +1724,9 @@ async function updateCommand(parsed, context) {
1414
1724
  provisioned.push(entry);
1415
1725
  }
1416
1726
  }
1727
+ for (const mutation of stateMigration.mutations) {
1728
+ mutations.push(mutation);
1729
+ }
1417
1730
  const oldByName = new Map(manifest.managedArtifacts.map((artifact) => [artifact.logicalName, artifact]));
1418
1731
  const skippedByName = new Map(skipped.map((entry) => [entry.logicalName, entry]));
1419
1732
  const hasUnrecordedGovernanceConflict = skipped.some((entry) => entry.status === 'conflict' &&
@@ -1462,22 +1775,40 @@ async function updateCommand(parsed, context) {
1462
1775
  pathParts: ['liftoff.manifest.json'],
1463
1776
  content: `${JSON.stringify(nextManifest, null, 2)}\n`
1464
1777
  });
1465
- assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans);
1778
+ assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans, stateMigration.mutations.map((mutation) => mutation.pathParts));
1466
1779
  await applyProjectFileTransaction(projectRoot, mutations, {
1467
- preconditions: selectUpdatePreconditions(updateSnapshots, entries, mutations, provisioned.map((entry) => entry.rendered.pathParts))
1780
+ onBeforeMutation: async (mutation, index) => {
1781
+ maybeInjectUpdateFailure(context.env, `before-mutation:${index}`);
1782
+ maybeInjectUpdateFailure(context.env, `before-path:${mutation.pathParts.join('/')}`);
1783
+ },
1784
+ preconditions: [
1785
+ ...selectUpdatePreconditions(updateSnapshots, entries, mutations, provisioned.map((entry) => entry.rendered.pathParts)),
1786
+ ...stateMigration.preconditions
1787
+ ]
1468
1788
  });
1469
1789
  if (jsonMode) {
1470
1790
  presentation.rawStdout(`${JSON.stringify({
1471
1791
  schemaVersion: 2,
1472
1792
  mode: 'apply',
1473
1793
  scope: 'managed-core',
1474
- status: blockedProvisioning.length > 0 ? 'partial' : 'applied',
1794
+ status: blockedProvisioning.length > 0
1795
+ ? 'partial'
1796
+ : reconciliation.status === 'reconciliation-required'
1797
+ ? 'reconciliation-required'
1798
+ : 'applied',
1475
1799
  cliVersion: liftoffVersion,
1476
1800
  projectVersion: manifest.liftoffVersion,
1477
1801
  written: written.map((entry) => manifestDisplayPath(entry.pathParts)),
1802
+ stateWritten: stateMigration.mutations.map((mutation) => manifestDisplayPath(mutation.pathParts)),
1478
1803
  skipped: skipped.map((entry) => ({ path: manifestDisplayPath(entry.pathParts), reason: entry.reason })),
1479
1804
  provisioning: provisioningJson(provisioningPlans),
1480
1805
  ownershipMigrationPending,
1806
+ manifestChanges: manifestChanges(manifest, nextManifest),
1807
+ activationIdentity: nextManifest.governance.profile !== 'none' && nextManifest.governance.profile !== 'unspecified'
1808
+ ? nextManifest.governance.activationIdentity ?? null
1809
+ : null,
1810
+ activationStateMigration: stateMigration.report,
1811
+ reconciliation,
1481
1812
  summary
1482
1813
  }, null, 2)}\n`);
1483
1814
  return 0;
@@ -1485,6 +1816,9 @@ async function updateCommand(parsed, context) {
1485
1816
  if (written.length > 0) {
1486
1817
  presentation.bullets('Applied Liftoff core changes', written.map((entry) => `wrote ${entryDisplay(entry)}`));
1487
1818
  }
1819
+ if (stateMigration.status === 'migrate') {
1820
+ presentation.bullets('Migrated activation state', stateMigration.mutations.map((mutation) => `wrote ${manifestDisplayPath(mutation.pathParts)}; evidence bytes preserved`));
1821
+ }
1488
1822
  if (provisioned.length > 0) {
1489
1823
  presentation.bullets('Provisioned project components', provisioned.map((entry) => `${entry.status === 'create' ? 'wrote' : 'adopted'} ${manifestDisplayPath(entry.rendered.pathParts)}`));
1490
1824
  }
@@ -1500,6 +1834,13 @@ async function updateCommand(parsed, context) {
1500
1834
  if (orphans.length > 0) {
1501
1835
  presentation.bullets('Orphaned Liftoff core artifacts', orphans.map((entry) => `orphan ${entryDisplay(entry)} ${entry.reason}`));
1502
1836
  }
1837
+ if (reconciliation.status === 'reconciliation-required') {
1838
+ presentation.bullets('Reconciliation required', [
1839
+ ...reconciliation.changedIdentityFields.map((field) => `${field.field}: ${JSON.stringify(field.from)} -> ${JSON.stringify(field.to)}`),
1840
+ `invalid phases: ${reconciliation.phaseImpact.invalidPhaseIds.join(', ') || 'none'}`,
1841
+ reconciliation.remedy ?? 'Run governance status/verify before executing affected phases.'
1842
+ ]);
1843
+ }
1503
1844
  presentation.completion('Updated project', [
1504
1845
  `${written.length} core written`,
1505
1846
  `${provisioned.length} project provisioned`,
@@ -1961,6 +2302,7 @@ async function projectLayer(projectRoot, manifest) {
1961
2302
  detail,
1962
2303
  ...(remedy ? { remedy } : {})
1963
2304
  });
2305
+ checks.push(...await governanceDoctorChecks(projectRoot, manifest));
1964
2306
  }
1965
2307
  checks.push(...await frameworkDoctorChecks(projectRoot, manifest));
1966
2308
  if (manifest.project.workload.kind === 'power-apps-code-app') {
@@ -1985,7 +2327,7 @@ async function projectLayer(projectRoot, manifest) {
1985
2327
  const provisioningPlans = await inspectProvisioningGroups(projectRoot, render, requestedProvisioningGroups(manifest, plan));
1986
2328
  const driftCount = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash).length +
1987
2329
  provisioningPlans.length +
1988
- (manifest.artifactVersion === 6 ? 0 : 1);
2330
+ (manifest.artifactVersion === 7 ? 0 : 1);
1989
2331
  if (driftCount > 0) {
1990
2332
  checks.push({
1991
2333
  label: 'managed core',