@msn-control/liftoff 0.11.3 → 0.12.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 (326) hide show
  1. package/DEVELOPER.md +19 -18
  2. package/README.md +18 -18
  3. package/assets/governance/single-maintainer-gitflow/activation-v2-graph.json +1583 -0
  4. package/dist/adapters/credentials/github-enrollment.d.ts +51 -0
  5. package/dist/adapters/credentials/github-enrollment.js +226 -0
  6. package/dist/adapters/credentials/github-enrollment.js.map +1 -0
  7. package/dist/adapters/credentials/protected-input.d.ts +7 -0
  8. package/dist/adapters/credentials/protected-input.js +68 -0
  9. package/dist/adapters/credentials/protected-input.js.map +1 -0
  10. package/dist/adapters/filesystem/executables.d.ts +2 -0
  11. package/dist/adapters/filesystem/executables.js +70 -0
  12. package/dist/adapters/filesystem/executables.js.map +1 -0
  13. package/dist/adapters/filesystem/update-previews.d.ts +9 -0
  14. package/dist/adapters/filesystem/update-previews.js +44 -0
  15. package/dist/adapters/filesystem/update-previews.js.map +1 -1
  16. package/dist/adapters/filesystem/workstation-attempts.d.ts +13 -0
  17. package/dist/adapters/filesystem/workstation-attempts.js +78 -0
  18. package/dist/adapters/filesystem/workstation-attempts.js.map +1 -0
  19. package/dist/adapters/github/activation-rest.d.ts +40 -0
  20. package/dist/adapters/github/activation-rest.js +272 -0
  21. package/dist/adapters/github/activation-rest.js.map +1 -0
  22. package/dist/adapters/state/azure-blob.d.ts +43 -0
  23. package/dist/adapters/state/azure-blob.js +335 -0
  24. package/dist/adapters/state/azure-blob.js.map +1 -0
  25. package/dist/adapters/state/darwin-capabilities.d.ts +33 -0
  26. package/dist/adapters/state/darwin-capabilities.js +146 -0
  27. package/dist/adapters/state/darwin-capabilities.js.map +1 -0
  28. package/dist/adapters/state/darwin-native-host.d.ts +32 -0
  29. package/dist/adapters/state/darwin-native-host.js +294 -0
  30. package/dist/adapters/state/darwin-native-host.js.map +1 -0
  31. package/dist/adapters/state/darwin-profile.d.ts +6 -0
  32. package/dist/adapters/state/darwin-profile.js +56 -0
  33. package/dist/adapters/state/darwin-profile.js.map +1 -0
  34. package/dist/adapters/state/darwin-system-program.d.ts +1 -0
  35. package/dist/adapters/state/darwin-system-program.js +94 -0
  36. package/dist/adapters/state/darwin-system-program.js.map +1 -0
  37. package/dist/adapters/state/local-qualification.d.ts +16 -0
  38. package/dist/adapters/state/local-qualification.js +299 -0
  39. package/dist/adapters/state/local-qualification.js.map +1 -0
  40. package/dist/adapters/state/local.d.ts +25 -0
  41. package/dist/adapters/state/local.js +145 -0
  42. package/dist/adapters/state/local.js.map +1 -0
  43. package/dist/adapters/state/native-command.d.ts +21 -0
  44. package/dist/adapters/state/native-command.js +82 -0
  45. package/dist/adapters/state/native-command.js.map +1 -0
  46. package/dist/adapters/state/native-files.d.ts +2 -0
  47. package/dist/adapters/state/native-files.js +47 -0
  48. package/dist/adapters/state/native-files.js.map +1 -0
  49. package/dist/adapters/state/native-system.d.ts +38 -0
  50. package/dist/adapters/state/native-system.js +86 -0
  51. package/dist/adapters/state/native-system.js.map +1 -0
  52. package/dist/adapters/state/opentofu.d.ts +35 -0
  53. package/dist/adapters/state/opentofu.js +350 -0
  54. package/dist/adapters/state/opentofu.js.map +1 -0
  55. package/dist/adapters/state/owned-process.d.ts +43 -0
  56. package/dist/adapters/state/owned-process.js +252 -0
  57. package/dist/adapters/state/owned-process.js.map +1 -0
  58. package/dist/adapters/state/posix-lock-program.d.ts +1 -0
  59. package/dist/adapters/state/posix-lock-program.js +185 -0
  60. package/dist/adapters/state/posix-lock-program.js.map +1 -0
  61. package/dist/adapters/state/posix-native-lock.d.ts +15 -0
  62. package/dist/adapters/state/posix-native-lock.js +145 -0
  63. package/dist/adapters/state/posix-native-lock.js.map +1 -0
  64. package/dist/adapters/state/protected-workspace.d.ts +57 -0
  65. package/dist/adapters/state/protected-workspace.js +412 -0
  66. package/dist/adapters/state/protected-workspace.js.map +1 -0
  67. package/dist/application/context.d.ts +3 -0
  68. package/dist/application/diagnose/doctor.d.ts +5 -0
  69. package/dist/application/diagnose/doctor.js +21 -5
  70. package/dist/application/diagnose/doctor.js.map +1 -1
  71. package/dist/application/diagnose/generated-project.js +2 -7
  72. package/dist/application/diagnose/generated-project.js.map +1 -1
  73. package/dist/application/initialize/fixture.js +8 -1
  74. package/dist/application/initialize/fixture.js.map +1 -1
  75. package/dist/application/initialize/use-case.js +70 -36
  76. package/dist/application/initialize/use-case.js.map +1 -1
  77. package/dist/application/project/manifest.js +5 -4
  78. package/dist/application/project/manifest.js.map +1 -1
  79. package/dist/application/state-migration/execution.d.ts +32 -0
  80. package/dist/application/state-migration/execution.js +440 -0
  81. package/dist/application/state-migration/execution.js.map +1 -0
  82. package/dist/application/state-migration/index.d.ts +22 -0
  83. package/dist/application/state-migration/index.js +17 -0
  84. package/dist/application/state-migration/index.js.map +1 -0
  85. package/dist/application/state-migration/lifecycle.d.ts +3 -0
  86. package/dist/application/state-migration/lifecycle.js +149 -0
  87. package/dist/application/state-migration/lifecycle.js.map +1 -0
  88. package/dist/application/state-migration/planning.d.ts +7 -0
  89. package/dist/application/state-migration/planning.js +150 -0
  90. package/dist/application/state-migration/planning.js.map +1 -0
  91. package/dist/application/state-migration/recovery.d.ts +5 -0
  92. package/dist/application/state-migration/recovery.js +145 -0
  93. package/dist/application/state-migration/recovery.js.map +1 -0
  94. package/dist/application/state-migration/runtime.d.ts +32 -0
  95. package/dist/application/state-migration/runtime.js +188 -0
  96. package/dist/application/state-migration/runtime.js.map +1 -0
  97. package/dist/application/state-migration/service.d.ts +2 -0
  98. package/dist/application/state-migration/service.js +20 -0
  99. package/dist/application/state-migration/service.js.map +1 -0
  100. package/dist/application/update/inspection.d.ts +16 -2
  101. package/dist/application/update/inspection.js +76 -23
  102. package/dist/application/update/inspection.js.map +1 -1
  103. package/dist/application/update/migration-runtime.js +24 -11
  104. package/dist/application/update/migration-runtime.js.map +1 -1
  105. package/dist/application/update/output.d.ts +2 -0
  106. package/dist/application/update/output.js +11 -1
  107. package/dist/application/update/output.js.map +1 -1
  108. package/dist/application/update/revalidation-plan.d.ts +1 -0
  109. package/dist/application/update/revalidation-plan.js +25 -12
  110. package/dist/application/update/revalidation-plan.js.map +1 -1
  111. package/dist/application/update/revalidation.d.ts +4 -1
  112. package/dist/application/update/revalidation.js +62 -21
  113. package/dist/application/update/revalidation.js.map +1 -1
  114. package/dist/application/update/use-case.js +25 -17
  115. package/dist/application/update/use-case.js.map +1 -1
  116. package/dist/cli/args/definitions.js +11 -3
  117. package/dist/cli/args/definitions.js.map +1 -1
  118. package/dist/cli/args/parser.js +29 -3
  119. package/dist/cli/args/parser.js.map +1 -1
  120. package/dist/domain/governance/activation/approvals.d.ts +264 -7
  121. package/dist/domain/governance/activation/approvals.js +166 -23
  122. package/dist/domain/governance/activation/approvals.js.map +1 -1
  123. package/dist/domain/governance/activation/capabilities.js +5 -2
  124. package/dist/domain/governance/activation/capabilities.js.map +1 -1
  125. package/dist/domain/governance/activation/evidence.d.ts +2 -1
  126. package/dist/domain/governance/activation/evidence.js +64 -11
  127. package/dist/domain/governance/activation/evidence.js.map +1 -1
  128. package/dist/domain/governance/activation/graph.d.ts +11 -384
  129. package/dist/domain/governance/activation/graph.js +424 -379
  130. package/dist/domain/governance/activation/graph.js.map +1 -1
  131. package/dist/domain/governance/activation/inputs.d.ts +3 -1
  132. package/dist/domain/governance/activation/inputs.js +47 -6
  133. package/dist/domain/governance/activation/inputs.js.map +1 -1
  134. package/dist/domain/governance/activation/operations.d.ts +4 -1
  135. package/dist/domain/governance/activation/operations.js +110 -22
  136. package/dist/domain/governance/activation/operations.js.map +1 -1
  137. package/dist/domain/governance/activation/readiness.d.ts +10 -0
  138. package/dist/domain/governance/activation/readiness.js +77 -38
  139. package/dist/domain/governance/activation/readiness.js.map +1 -1
  140. package/dist/domain/governance/activation/types.d.ts +151 -9
  141. package/dist/domain/governance/activation/types.js +23 -1
  142. package/dist/domain/governance/activation/types.js.map +1 -1
  143. package/dist/domain/governance/activation/validators.d.ts +4 -1
  144. package/dist/domain/governance/activation/validators.js +419 -26
  145. package/dist/domain/governance/activation/validators.js.map +1 -1
  146. package/dist/domain/governance/policy/identity.d.ts +55 -13
  147. package/dist/domain/governance/policy/identity.js +28 -8
  148. package/dist/domain/governance/policy/identity.js.map +1 -1
  149. package/dist/domain/project/artifact-lifecycle.d.ts +2 -1
  150. package/dist/domain/project/artifact-lifecycle.js +7 -1
  151. package/dist/domain/project/artifact-lifecycle.js.map +1 -1
  152. package/dist/domain/project/catalog.d.ts +50 -1
  153. package/dist/domain/project/catalog.js +104 -4
  154. package/dist/domain/project/catalog.js.map +1 -1
  155. package/dist/domain/project/contracts.d.ts +9 -1
  156. package/dist/domain/project/infrastructure-layout.d.ts +1 -1
  157. package/dist/domain/project/infrastructure-layout.js +2 -1
  158. package/dist/domain/project/infrastructure-layout.js.map +1 -1
  159. package/dist/domain/project/manifest/governance.d.ts +1 -1
  160. package/dist/domain/project/manifest/governance.js +9 -10
  161. package/dist/domain/project/manifest/governance.js.map +1 -1
  162. package/dist/domain/project/planning.js +4 -5
  163. package/dist/domain/project/planning.js.map +1 -1
  164. package/dist/domain/repair/stateful-bounded.d.ts +1 -0
  165. package/dist/domain/repair/stateful-bounded.js +37 -0
  166. package/dist/domain/repair/stateful-bounded.js.map +1 -0
  167. package/dist/domain/repair/stateful-invariants.d.ts +28 -0
  168. package/dist/domain/repair/stateful-invariants.js +278 -0
  169. package/dist/domain/repair/stateful-invariants.js.map +1 -0
  170. package/dist/domain/repair/stateful.d.ts +690 -0
  171. package/dist/domain/repair/stateful.js +26 -0
  172. package/dist/domain/repair/stateful.js.map +1 -0
  173. package/dist/domain/workstation/constraints.d.ts +1 -0
  174. package/dist/domain/workstation/constraints.js.map +1 -1
  175. package/dist/domain/workstation/contracts.d.ts +42 -0
  176. package/dist/domain/workstation/contracts.js +2 -0
  177. package/dist/domain/workstation/contracts.js.map +1 -0
  178. package/dist/domain/workstation/executables.d.ts +19 -0
  179. package/dist/domain/workstation/executables.js +68 -0
  180. package/dist/domain/workstation/executables.js.map +1 -0
  181. package/dist/domain/workstation/versions.d.ts +8 -0
  182. package/dist/domain/workstation/versions.js +145 -0
  183. package/dist/domain/workstation/versions.js.map +1 -0
  184. package/dist/framework-adapters.d.ts +2 -0
  185. package/dist/framework-adapters.js +148 -22
  186. package/dist/framework-adapters.js.map +1 -1
  187. package/dist/framework-validation.d.ts +6 -0
  188. package/dist/framework-validation.js +89 -24
  189. package/dist/framework-validation.js.map +1 -1
  190. package/dist/generators/common/base.js +57 -17
  191. package/dist/generators/common/base.js.map +1 -1
  192. package/dist/governance-activation/activation-state.js +39 -7
  193. package/dist/governance-activation/activation-state.js.map +1 -1
  194. package/dist/governance-activation/authority-records.d.ts +4 -0
  195. package/dist/governance-activation/authority-records.js +24 -0
  196. package/dist/governance-activation/authority-records.js.map +1 -0
  197. package/dist/governance-activation/commands.d.ts +9 -1
  198. package/dist/governance-activation/commands.js +392 -97
  199. package/dist/governance-activation/commands.js.map +1 -1
  200. package/dist/governance-activation/compatibility.d.ts +11 -8
  201. package/dist/governance-activation/compatibility.js +37 -23
  202. package/dist/governance-activation/compatibility.js.map +1 -1
  203. package/dist/governance-activation/doctor.js +40 -16
  204. package/dist/governance-activation/doctor.js.map +1 -1
  205. package/dist/governance-activation/github-config.d.ts +17 -0
  206. package/dist/governance-activation/github-config.js +88 -0
  207. package/dist/governance-activation/github-config.js.map +1 -0
  208. package/dist/governance-activation/github-discovery.d.ts +18 -0
  209. package/dist/governance-activation/github-discovery.js +229 -0
  210. package/dist/governance-activation/github-discovery.js.map +1 -0
  211. package/dist/governance-activation/github-ports.d.ts +12 -0
  212. package/dist/governance-activation/github-ports.js +4 -0
  213. package/dist/governance-activation/github-ports.js.map +1 -0
  214. package/dist/governance-activation/github-publication.d.ts +3 -0
  215. package/dist/governance-activation/github-publication.js +190 -0
  216. package/dist/governance-activation/github-publication.js.map +1 -0
  217. package/dist/governance-activation/historical-common.d.ts +98 -0
  218. package/dist/governance-activation/historical-common.js +269 -0
  219. package/dist/governance-activation/historical-common.js.map +1 -0
  220. package/dist/governance-activation/historical-credential-policy.d.ts +3 -0
  221. package/dist/governance-activation/historical-credential-policy.js +91 -0
  222. package/dist/governance-activation/historical-credential-policy.js.map +1 -0
  223. package/dist/governance-activation/historical-inputs.d.ts +6 -0
  224. package/dist/governance-activation/historical-inputs.js +57 -0
  225. package/dist/governance-activation/historical-inputs.js.map +1 -0
  226. package/dist/governance-activation/historical-safety.d.ts +9 -0
  227. package/dist/governance-activation/historical-safety.js +102 -0
  228. package/dist/governance-activation/historical-safety.js.map +1 -0
  229. package/dist/governance-activation/historical-source-metadata.d.ts +31 -0
  230. package/dist/governance-activation/historical-source-metadata.js +55 -0
  231. package/dist/governance-activation/historical-source-metadata.js.map +1 -0
  232. package/dist/governance-activation/historical-state.d.ts +12 -50
  233. package/dist/governance-activation/historical-state.js +195 -308
  234. package/dist/governance-activation/historical-state.js.map +1 -1
  235. package/dist/governance-activation/historical-v1-auxiliary.d.ts +1 -0
  236. package/dist/governance-activation/historical-v1-auxiliary.js +43 -0
  237. package/dist/governance-activation/historical-v1-auxiliary.js.map +1 -0
  238. package/dist/governance-activation/historical-v1-phase-contracts.d.ts +31 -0
  239. package/dist/governance-activation/historical-v1-phase-contracts.js +46 -0
  240. package/dist/governance-activation/historical-v1-phase-contracts.js.map +1 -0
  241. package/dist/governance-activation/historical-v2.d.ts +53 -0
  242. package/dist/governance-activation/historical-v2.js +313 -0
  243. package/dist/governance-activation/historical-v2.js.map +1 -0
  244. package/dist/governance-activation/history-contracts.d.ts +21 -12
  245. package/dist/governance-activation/history-contracts.js +99 -27
  246. package/dist/governance-activation/history-contracts.js.map +1 -1
  247. package/dist/governance-activation/inputs.d.ts +8 -2
  248. package/dist/governance-activation/inputs.js +41 -6
  249. package/dist/governance-activation/inputs.js.map +1 -1
  250. package/dist/governance-activation/migration-history.d.ts +43 -5
  251. package/dist/governance-activation/migration-history.js +231 -50
  252. package/dist/governance-activation/migration-history.js.map +1 -1
  253. package/dist/governance-activation/migration.d.ts +7 -4
  254. package/dist/governance-activation/migration.js +53 -252
  255. package/dist/governance-activation/migration.js.map +1 -1
  256. package/dist/governance-activation/phase-azure.d.ts +3 -0
  257. package/dist/governance-activation/phase-azure.js +205 -0
  258. package/dist/governance-activation/phase-azure.js.map +1 -0
  259. package/dist/governance-activation/phase-bootstrap-state.js +3 -0
  260. package/dist/governance-activation/phase-bootstrap-state.js.map +1 -1
  261. package/dist/governance-activation/phase-github.d.ts +3 -0
  262. package/dist/governance-activation/phase-github.js +192 -0
  263. package/dist/governance-activation/phase-github.js.map +1 -0
  264. package/dist/governance-activation/phase-governance.js +8 -2
  265. package/dist/governance-activation/phase-governance.js.map +1 -1
  266. package/dist/governance-activation/public-plans.d.ts +36 -0
  267. package/dist/governance-activation/public-plans.js +100 -0
  268. package/dist/governance-activation/public-plans.js.map +1 -0
  269. package/dist/governance-activation/read-only.d.ts +3 -1
  270. package/dist/governance-activation/read-only.js +7 -3
  271. package/dist/governance-activation/read-only.js.map +1 -1
  272. package/dist/governance-activation/reconciliation.js +10 -0
  273. package/dist/governance-activation/reconciliation.js.map +1 -1
  274. package/dist/governance-activation/seed-lifecycle.js +14 -25
  275. package/dist/governance-activation/seed-lifecycle.js.map +1 -1
  276. package/dist/governance-activation/source-of-truth.d.ts +21 -5
  277. package/dist/governance-activation/source-of-truth.js +102 -13
  278. package/dist/governance-activation/source-of-truth.js.map +1 -1
  279. package/dist/governance-activation/task-projection.d.ts +4 -1
  280. package/dist/governance-activation/task-projection.js +32 -3
  281. package/dist/governance-activation/task-projection.js.map +1 -1
  282. package/dist/governance-activation/task-writes.d.ts +42 -0
  283. package/dist/governance-activation/task-writes.js +250 -0
  284. package/dist/governance-activation/task-writes.js.map +1 -0
  285. package/dist/governance-activation/transition-files.d.ts +12 -0
  286. package/dist/governance-activation/transition-files.js +109 -0
  287. package/dist/governance-activation/transition-files.js.map +1 -0
  288. package/dist/governance-activation/transition-planning.d.ts +3 -1
  289. package/dist/governance-activation/transition-planning.js +158 -20
  290. package/dist/governance-activation/transition-planning.js.map +1 -1
  291. package/dist/governance-activation/transition-ports.d.ts +31 -3
  292. package/dist/governance-activation/transition-records.d.ts +11 -1
  293. package/dist/governance-activation/transition-records.js +52 -6
  294. package/dist/governance-activation/transition-records.js.map +1 -1
  295. package/dist/governance-activation/transitions.d.ts +5 -0
  296. package/dist/governance-activation/transitions.js +271 -37
  297. package/dist/governance-activation/transitions.js.map +1 -1
  298. package/dist/governance-assessment/engine.js +22 -15
  299. package/dist/governance-assessment/engine.js.map +1 -1
  300. package/dist/governance-assessment/project.d.ts +2 -0
  301. package/dist/governance-assessment/project.js +2 -1
  302. package/dist/governance-assessment/project.js.map +1 -1
  303. package/dist/interactive.d.ts +0 -1
  304. package/dist/interactive.js +3 -22
  305. package/dist/interactive.js.map +1 -1
  306. package/dist/openspec-profile.d.ts +2 -1
  307. package/dist/openspec-profile.js +11 -41
  308. package/dist/openspec-profile.js.map +1 -1
  309. package/dist/repository-governance.d.ts +10 -5
  310. package/dist/repository-governance.js +178 -92
  311. package/dist/repository-governance.js.map +1 -1
  312. package/dist/telemetry/contract.d.ts +1 -1
  313. package/dist/telemetry/contract.js +3 -0
  314. package/dist/telemetry/contract.js.map +1 -1
  315. package/dist/workstation-catalog.d.ts +13 -1
  316. package/dist/workstation-catalog.js +122 -0
  317. package/dist/workstation-catalog.js.map +1 -1
  318. package/dist/workstation.d.ts +69 -14
  319. package/dist/workstation.js +600 -215
  320. package/dist/workstation.js.map +1 -1
  321. package/docs/cli-reference.md +59 -21
  322. package/docs/configuration-and-manifests.md +9 -8
  323. package/docs/prerequisites.md +93 -18
  324. package/docs/repository-governance.md +68 -40
  325. package/docs/spec-workflows-and-agents.md +132 -13
  326. package/package.json +1 -1
@@ -1,20 +1,26 @@
1
1
  import { activationCompatibility, canonicalPhaseGraph, canonicalPhaseGraphHash, currentActivationIdentity } from './graph.js';
2
2
  import { activationContractVersion, activationStateSchemaVersion, approvalEnvelopeSchemaVersion, credentialPolicySchemaVersion, evidenceHeaderSchemaVersion, governanceActivationPolicyVersion, liftoffActivationPackageVersion, liftoffManifestArtifactVersion, phaseGraphSchemaVersion, resolveActivationCompatibility, supersessionSchemaVersion, isHistoricalActivationIdentity } from '../policy/identity.js';
3
3
  import { normalizeApprovalCostCeiling, normalizeApprovalDestinations, normalizeApprovalDestructiveScope, normalizeApprovalPermissions, normalizeApprovalPolicyExceptions, normalizeApprovalResources } from './approvals.js';
4
- import { approvalGateKinds, invalidationInputKinds, mutationClasses, phaseIds, phaseStates, rollbackKinds, runnerPreflightDisplayNameTemplate, runnerPreflightOrganizationPermissions, runnerPreflightPatLifetimeDays, runnerPreflightRepositoryPermissions, runnerPreflightRotationLeadDays, runnerPreflightSecretName } from './types.js';
4
+ import { sha256Hex } from './canonical-json.js';
5
+ import { validateArtifactPathParts } from '../../project/paths.js';
6
+ import { approvalGateKinds, governanceScopes, invalidationInputKinds, mutationClasses, phaseIds, phaseStates, phaseScope, rollbackKinds, runnerPreflightDisplayNameTemplate, runnerPreflightOrganizationPermissions, runnerPreflightPatLifetimeDays, runnerPreflightRepositoryPermissions, runnerPreflightRotationLeadDays, runnerPreflightSecretName } from './types.js';
5
7
  const phaseIdSet = new Set(phaseIds);
6
8
  const terminalPhaseStateSet = new Set(['approved', 'verified', 'failed', 'inapplicable', 'retained', 'disposed']);
7
9
  const resultStateSet = new Set(['verified', 'failed', 'inapplicable', 'retained', 'disposed']);
8
10
  const hex64Pattern = /^[a-f0-9]{64}$/;
9
11
  const isoLikePattern = /^\d{4}-\d{2}-\d{2}T/;
10
12
  const liveReadbackProviderSet = new Set(['github', 'azure']);
11
- const githubRemoteWriteMutations = new Set(['github-write', 'github-secret-write', 'github-ruleset-write']);
13
+ const githubRemoteWriteMutations = new Set([
14
+ 'git-push', 'github-write', 'github-repository-create', 'github-workflow-dispatch',
15
+ 'github-secret-write', 'github-ruleset-write'
16
+ ]);
12
17
  const azureRemoteWriteMutations = new Set([
13
18
  'azure-provider-register',
14
19
  'azure-network-provision',
15
20
  'azure-state-import',
16
21
  'azure-resource-provision'
17
22
  ]);
23
+ const governanceScopeSet = new Set(governanceScopes);
18
24
  const transitionAdapterIds = new Set([
19
25
  'local-evidence',
20
26
  'selected-spec-workflow',
@@ -105,7 +111,163 @@ function pathPartsArray(value, path) {
105
111
  if (!Array.isArray(value)) {
106
112
  throw new Error(`${path} must be an array.`);
107
113
  }
108
- return value.map((entry, index) => stringArray(entry, `${path}[${index}]`));
114
+ return value.map((entry, index) => safePathParts(entry, `${path}[${index}]`));
115
+ }
116
+ function safePathParts(value, path) {
117
+ return validateArtifactPathParts(value, path);
118
+ }
119
+ function publicJson(value, path, depth = 0) {
120
+ if (depth > 20)
121
+ throw new Error(`${path} exceeds the supported JSON nesting depth.`);
122
+ if (value === null || typeof value === 'boolean' || (typeof value === 'number' && Number.isFinite(value)))
123
+ return value;
124
+ if (typeof value === 'string') {
125
+ if (/https?:\/\/[^/\s]*@|[?&](?:token|key|secret|sig)=|(?:gh[pousr]_|github_pat_)[A-Za-z0-9_]+|-----BEGIN [A-Z ]*PRIVATE KEY-----/iu.test(value)) {
126
+ throw new Error(`${path} contains credential material; use protected credential enrollment instead.`);
127
+ }
128
+ return value;
129
+ }
130
+ if (Array.isArray(value))
131
+ return value.map((entry, index) => publicJson(entry, `${path}[${index}]`, depth + 1));
132
+ const object = record(value, path);
133
+ const result = {};
134
+ for (const [key, entry] of Object.entries(object)) {
135
+ const tokenMetadata = key === 'token' && typeof entry === 'object' && entry !== null && !Array.isArray(entry)
136
+ ? record(entry, `${path}.${key}`) : null;
137
+ const publicAppTokenMetadata = tokenMetadata !== null &&
138
+ Object.keys(tokenMetadata).sort().join(',') === 'generatedBy,strategy,ttlSeconds' &&
139
+ tokenMetadata.generatedBy === 'github-app' && tokenMetadata.strategy === 'installation-token' &&
140
+ Number.isSafeInteger(tokenMetadata.ttlSeconds) && Number(tokenMetadata.ttlSeconds) > 0 && Number(tokenMetadata.ttlSeconds) <= 3600;
141
+ if (['__proto__', 'prototype', 'constructor'].includes(key) ||
142
+ /^(?:accessToken|refreshToken|token|password|secret|clientSecret|privateKey|accountKey|connectionString|sasToken)$/iu.test(key) && !publicAppTokenMetadata) {
143
+ throw new Error(`${path}.${key} is not permitted in public activation inputs.`);
144
+ }
145
+ result[key] = publicJson(entry, `${path}.${key}`, depth + 1);
146
+ }
147
+ return result;
148
+ }
149
+ export function validateActivationConfiguration(value) {
150
+ const config = exactWithOptional(value, ['schemaVersion', 'phases'], ['repository', 'azure', 'budget'], 'activationInputs');
151
+ requireVersion(config.schemaVersion, 1, 'activationInputs.schemaVersion');
152
+ const phases = {};
153
+ for (const [id, inputs] of Object.entries(record(config.phases, 'activationInputs.phases'))) {
154
+ const phaseId = enumValue(id, phaseIdSet, 'activationInputs.phases');
155
+ phases[phaseId] = record(publicJson(inputs, `activationInputs.phases.${id}`), `activationInputs.phases.${id}`);
156
+ }
157
+ let repository;
158
+ if (config.repository !== undefined) {
159
+ const target = exactWithOptional(config.repository, ['name'], ['defaultBranch', 'visibility', 'create'], 'activationInputs.repository');
160
+ const name = stringField(target, 'name', 'activationInputs.repository');
161
+ if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(name)) {
162
+ throw new Error('activationInputs.repository.name must be owner/repository.');
163
+ }
164
+ const defaultBranch = target.defaultBranch === undefined ? undefined : stringField(target, 'defaultBranch', 'activationInputs.repository');
165
+ if (defaultBranch !== undefined && (!/^[A-Za-z0-9][A-Za-z0-9_./-]*$/u.test(defaultBranch) ||
166
+ defaultBranch.includes('..') || defaultBranch.includes('//') || defaultBranch.endsWith('/') || defaultBranch.endsWith('.lock'))) {
167
+ throw new Error('activationInputs.repository.defaultBranch must be a safe Git branch name.');
168
+ }
169
+ repository = {
170
+ name,
171
+ ...(defaultBranch === undefined ? {} : { defaultBranch }),
172
+ ...(target.visibility === undefined ? {} : { visibility: enumValue(target.visibility, new Set(['private', 'public']), 'activationInputs.repository.visibility') }),
173
+ ...(target.create === undefined ? {} : { create: booleanField(target, 'create', 'activationInputs.repository') })
174
+ };
175
+ }
176
+ let azure;
177
+ if (config.azure !== undefined) {
178
+ const target = exact(config.azure, ['subscriptionId', 'tenantId', 'region'], 'activationInputs.azure');
179
+ const subscriptionId = stringField(target, 'subscriptionId', 'activationInputs.azure');
180
+ const tenantId = stringField(target, 'tenantId', 'activationInputs.azure');
181
+ const region = stringField(target, 'region', 'activationInputs.azure');
182
+ const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/iu;
183
+ if (!uuid.test(subscriptionId) || !uuid.test(tenantId) || !/^[a-z][a-z0-9]+$/u.test(region)) {
184
+ throw new Error('activationInputs.azure requires concrete subscription/tenant GUIDs and an Azure region name.');
185
+ }
186
+ azure = { subscriptionId, tenantId, region };
187
+ }
188
+ let budget;
189
+ if (config.budget !== undefined) {
190
+ const cost = exact(config.budget, ['currency', 'fixedMonthlyCents', 'usageMonthlyCents'], 'activationInputs.budget');
191
+ budget = normalizeApprovalCostCeiling({
192
+ currency: stringField(cost, 'currency', 'activationInputs.budget'),
193
+ fixedMonthlyCents: integerField(cost, 'fixedMonthlyCents', 'activationInputs.budget'),
194
+ usageMonthlyCents: integerField(cost, 'usageMonthlyCents', 'activationInputs.budget')
195
+ });
196
+ }
197
+ return { schemaVersion: 1, phases, ...(repository ? { repository } : {}), ...(azure ? { azure } : {}), ...(budget ? { budget } : {}) };
198
+ }
199
+ function validateExternalOperation(value, path) {
200
+ const item = exactWithOptional(value, ['provider', 'actionId', 'operationId', 'resourceId', 'startedAt', 'observedAt', 'status'], ['pollUrl', 'planDigest'], path);
201
+ const pollUrl = item.pollUrl === undefined ? undefined : stringField(item, 'pollUrl', path);
202
+ if (pollUrl !== undefined) {
203
+ publicJson(pollUrl, `${path}.pollUrl`);
204
+ const url = new URL(pollUrl);
205
+ if (url.protocol !== 'https:' || !['api.github.com', 'management.azure.com'].includes(url.hostname) ||
206
+ url.username || url.password || url.port)
207
+ throw new Error(`${path}.pollUrl must be a credential-free supported provider URL.`);
208
+ }
209
+ const startedAt = isoTimestamp(item.startedAt, `${path}.startedAt`);
210
+ const observedAt = isoTimestamp(item.observedAt, `${path}.observedAt`);
211
+ if (Date.parse(observedAt) < Date.parse(startedAt))
212
+ throw new Error(`${path}.observedAt must not precede startedAt.`);
213
+ return {
214
+ provider: enumValue(item.provider, liveReadbackProviderSet, `${path}.provider`),
215
+ actionId: stringField(item, 'actionId', path),
216
+ operationId: stringField(item, 'operationId', path),
217
+ resourceId: stringField(item, 'resourceId', path),
218
+ startedAt, observedAt,
219
+ status: enumValue(item.status, new Set(['running', 'completed', 'failed']), `${path}.status`),
220
+ ...(pollUrl ? { pollUrl } : {}),
221
+ ...(item.planDigest === undefined ? {} : { planDigest: hexDigest(item.planDigest, `${path}.planDigest`) })
222
+ };
223
+ }
224
+ function validateFileChanges(value, path) {
225
+ if (!Array.isArray(value))
226
+ throw new Error(`${path} must be an array.`);
227
+ const changes = value.map((entry, index) => {
228
+ const item = exact(entry, ['pathParts', 'beforeHash', 'afterHash'], `${path}[${index}]`);
229
+ return {
230
+ pathParts: safePathParts(item.pathParts, `${path}[${index}].pathParts`),
231
+ beforeHash: item.beforeHash === null ? null : hexDigest(item.beforeHash, `${path}[${index}].beforeHash`),
232
+ afterHash: item.afterHash === null ? null : hexDigest(item.afterHash, `${path}[${index}].afterHash`)
233
+ };
234
+ });
235
+ assertNoDuplicateStrings(changes.map((change) => change.pathParts.join('/')), path);
236
+ return changes;
237
+ }
238
+ function validateGitInput(value, path) {
239
+ const git = exact(value, ['head', 'branch', 'pushUrls'], path);
240
+ const head = git.head === null ? null : stringField(git, 'head', path);
241
+ if (head !== null && !/^[a-f0-9]{40,64}$/u.test(head))
242
+ throw new Error(`${path}.head must be an actual Git object ID.`);
243
+ const pushUrls = stringArray(git.pushUrls, `${path}.pushUrls`);
244
+ if (pushUrls.some((url) => !/^(?:https:\/\/github\.com\/|git@github\.com:)[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+(?:\.git)?\/?$/u.test(url))) {
245
+ throw new Error(`${path}.pushUrls must contain credential-free supported GitHub destinations.`);
246
+ }
247
+ return { head, branch: git.branch === null ? null : stringField(git, 'branch', path), pushUrls };
248
+ }
249
+ function validateOutputBindings(value, path) {
250
+ const outputs = exact(value, ['values', 'resources'], path);
251
+ const values = {};
252
+ for (const [key, item] of Object.entries(record(publicJson(outputs.values, `${path}.values`), `${path}.values`))) {
253
+ if (item !== null && typeof item !== 'string' && typeof item !== 'boolean' && typeof item !== 'number') {
254
+ throw new Error(`${path}.values.${key} must be a public primitive value.`);
255
+ }
256
+ values[key] = item;
257
+ }
258
+ if (!Array.isArray(outputs.resources))
259
+ throw new Error(`${path}.resources must be an array.`);
260
+ return {
261
+ values,
262
+ resources: outputs.resources.map((entry, index) => {
263
+ const resource = exact(entry, ['provider', 'resourceType', 'resourceId'], `${path}.resources[${index}]`);
264
+ return {
265
+ provider: enumValue(resource.provider, liveReadbackProviderSet, `${path}.resources[${index}].provider`),
266
+ resourceType: stringField(resource, 'resourceType', path),
267
+ resourceId: stringField(resource, 'resourceId', path)
268
+ };
269
+ })
270
+ };
109
271
  }
110
272
  function enumValue(value, allowed, path) {
111
273
  if (typeof value !== 'string' || !allowed.has(value)) {
@@ -276,7 +438,7 @@ function validateApplicability(value, path) {
276
438
  'exclusiveWith'
277
439
  ], path);
278
440
  enumValue(applicability.kind, new Set(['conditional']), `${path}.kind`);
279
- const discriminator = enumValue(applicability.discriminator, new Set(['state-path', 'private-staging-dast', 'credential-required']), `${path}.discriminator`);
441
+ const discriminator = enumValue(applicability.discriminator, new Set(['state-path', 'private-staging-dast', 'credential-required', 'cloud-state-required', 'private-runner-required']), `${path}.discriminator`);
280
442
  const exclusiveWith = stringArray(applicability.exclusiveWith, `${path}.exclusiveWith`).map((id) => enumValue(id, phaseIdSet, `${path}.exclusiveWith`));
281
443
  return {
282
444
  kind: 'conditional',
@@ -408,7 +570,7 @@ function assertReachableToFinal(nodes) {
408
570
  }
409
571
  }
410
572
  export function validateManagedPhaseGraph(value) {
411
- const graph = exact(value, ['schemaVersion', 'versions', 'phases'], 'phaseGraph');
573
+ const graph = exact(value, ['schemaVersion', 'versions', 'phases', 'completionGroups'], 'phaseGraph');
412
574
  requireVersion(graph.schemaVersion, phaseGraphSchemaVersion, 'phaseGraph.schemaVersion');
413
575
  const versions = exact(graph.versions, [
414
576
  'liftoffVersion',
@@ -514,6 +676,11 @@ export function validateManagedPhaseGraph(value) {
514
676
  visit(id);
515
677
  }
516
678
  assertReachableToFinal(nodes);
679
+ const groups = exact(graph.completionGroups, governanceScopes, 'phaseGraph.completionGroups');
680
+ const completionGroups = {};
681
+ for (const scope of governanceScopes) {
682
+ completionGroups[scope] = exactStringSet(stringArray(groups[scope], `phaseGraph.completionGroups.${scope}`), phaseIds.filter((id) => phaseScope(id) === scope), `phaseGraph.completionGroups.${scope}`);
683
+ }
517
684
  return {
518
685
  schemaVersion: phaseGraphSchemaVersion,
519
686
  versions: {
@@ -522,7 +689,8 @@ export function validateManagedPhaseGraph(value) {
522
689
  activationContractVersion,
523
690
  phaseGraphSchemaVersion
524
691
  },
525
- phases: nodes
692
+ phases: nodes,
693
+ completionGroups
526
694
  };
527
695
  }
528
696
  export function assertCanonicalGraphValid() {
@@ -543,6 +711,86 @@ function validateEvidenceReference(value, path) {
543
711
  result
544
712
  };
545
713
  }
714
+ export function validateGovernanceTaskProjectionContract(value) {
715
+ const label = 'taskProjectionContract';
716
+ const raw = record(value, label);
717
+ const source = enumValue(raw.source, new Set(['existing', 'create']), `${label}.source`);
718
+ const item = exact(value, [
719
+ 'schemaVersion', 'derivation', 'source', 'changeId', 'workflowKind', 'taskPathParts',
720
+ 'metadataPathParts', 'metadataHash', 'layoutHash', ...(source === 'create' ? ['template', 'metadataText'] : [])
721
+ ], label);
722
+ requireVersion(item.schemaVersion, 1, `${label}.schemaVersion`);
723
+ if (item.derivation !== 'validated-current-readiness')
724
+ throw new Error(`${label} requires the bounded current-readiness derivation.`);
725
+ const changeId = stringField(item, 'changeId', label);
726
+ safePathParts([changeId], `${label}.changeId`);
727
+ const workflowKind = enumValue(item.workflowKind, new Set(['openspec', 'spec-kit']), `${label}.workflowKind`);
728
+ if (changeId === 'archive' || changeId.startsWith('bootstrap-') || changeId === '000-liftoff-bootstrap') {
729
+ throw new Error(`${label} cannot target seed tasks or an archive.`);
730
+ }
731
+ const base = workflowKind === 'openspec' ? ['openspec', 'changes', changeId] : ['specs', changeId];
732
+ const taskPathParts = safePathParts(item.taskPathParts, `${label}.taskPathParts`);
733
+ const metadataPathParts = safePathParts(item.metadataPathParts, `${label}.metadataPathParts`);
734
+ if (taskPathParts.join('/') !== [...base, 'tasks.md'].join('/') ||
735
+ metadataPathParts.join('/') !== [...base, 'liftoff-governance.json'].join('/')) {
736
+ throw new Error(`${label} must target the exact current governance task and metadata paths.`);
737
+ }
738
+ const common = {
739
+ schemaVersion: 1, derivation: 'validated-current-readiness',
740
+ changeId, workflowKind, taskPathParts, metadataPathParts,
741
+ metadataHash: hexDigest(item.metadataHash, `${label}.metadataHash`),
742
+ layoutHash: hexDigest(item.layoutHash, `${label}.layoutHash`)
743
+ };
744
+ if (source === 'existing')
745
+ return { ...common, source };
746
+ const template = stringField(item, 'template', label);
747
+ const metadataText = stringField(item, 'metadataText', label);
748
+ if (template.length > 262_144 || metadataText.length > 262_144 || sha256Hex(metadataText) !== common.metadataHash) {
749
+ throw new Error(`${label} has oversized or inconsistently bound creation sources.`);
750
+ }
751
+ publicJson(template, `${label}.template`);
752
+ publicJson(metadataText, `${label}.metadataText`);
753
+ return { ...common, source, template, metadataText };
754
+ }
755
+ export function validateGovernanceTaskProjectionRecord(value) {
756
+ const label = 'taskProjectionRecord';
757
+ const item = exact(value, [
758
+ 'schemaVersion', 'purpose', 'phaseId', 'planDigest', 'contractDigest', 'taskPathParts', 'metadataHash',
759
+ 'layoutHash', 'status', 'observedAt', 'beforeHash', 'afterHash', 'states', 'blockers'
760
+ ], label);
761
+ requireVersion(item.schemaVersion, 1, `${label}.schemaVersion`);
762
+ if (item.purpose !== 'projection-audit-only')
763
+ throw new Error(`${label} is not execution authority.`);
764
+ const status = enumValue(item.status, new Set(['complete', 'blocked']), `${label}.status`);
765
+ const blockers = stringArray(publicJson(item.blockers, `${label}.blockers`), `${label}.blockers`);
766
+ let states = null;
767
+ if (item.states !== null) {
768
+ const raw = exact(item.states, phaseIds, `${label}.states`);
769
+ states = Object.fromEntries(phaseIds.map((id) => [
770
+ id, enumValue(raw[id], new Set([...phaseStates, 'identity-incompatible']), `${label}.states.${id}`)
771
+ ]));
772
+ }
773
+ const beforeHash = item.beforeHash === null ? null : hexDigest(item.beforeHash, `${label}.beforeHash`);
774
+ const afterHash = item.afterHash === null ? null : hexDigest(item.afterHash, `${label}.afterHash`);
775
+ const taskPathParts = safePathParts(item.taskPathParts, `${label}.taskPathParts`);
776
+ const registered = taskPathParts.length === 4 && taskPathParts[0] === 'openspec' && taskPathParts[1] === 'changes' &&
777
+ taskPathParts[2] !== 'archive' && !taskPathParts[2].startsWith('bootstrap-') ||
778
+ taskPathParts.length === 3 && taskPathParts[0] === 'specs' && taskPathParts[1] !== '000-liftoff-bootstrap';
779
+ if (!registered || taskPathParts.at(-1) !== 'tasks.md')
780
+ throw new Error(`${label} cannot name an unregistered task destination.`);
781
+ if (status === 'complete' ? states === null || afterHash === null || blockers.length !== 0 :
782
+ states !== null || afterHash !== null || blockers.length === 0) {
783
+ throw new Error(`${label} must distinguish completed projection from blocked, uncommitted task output.`);
784
+ }
785
+ return {
786
+ schemaVersion: 1, purpose: 'projection-audit-only',
787
+ phaseId: enumValue(item.phaseId, phaseIdSet, `${label}.phaseId`),
788
+ planDigest: hexDigest(item.planDigest, `${label}.planDigest`), contractDigest: hexDigest(item.contractDigest, `${label}.contractDigest`),
789
+ taskPathParts,
790
+ metadataHash: hexDigest(item.metadataHash, `${label}.metadataHash`), layoutHash: hexDigest(item.layoutHash, `${label}.layoutHash`),
791
+ status, observedAt: isoTimestamp(item.observedAt, `${label}.observedAt`), beforeHash, afterHash, states, blockers
792
+ };
793
+ }
546
794
  export function validateUserActivationState(value) {
547
795
  const state = exactWithOptional(value, [
548
796
  'schemaVersion',
@@ -553,7 +801,7 @@ export function validateUserActivationState(value) {
553
801
  'phases',
554
802
  'createdAt',
555
803
  'updatedAt'
556
- ], ['bootstrapState', 'remoteBinding'], 'activationState');
804
+ ], ['bootstrapState', 'remoteBinding', 'baselineAnchor', 'activationInputs', 'phaseOutputs', 'successorHistory', 'taskProjection'], 'activationState');
557
805
  requireVersion(state.schemaVersion, activationStateSchemaVersion, 'activationState.schemaVersion');
558
806
  const identity = validateActivationIdentity(state.identity);
559
807
  if (identity.phaseGraphHash !== canonicalPhaseGraphHash) {
@@ -585,18 +833,47 @@ export function validateUserActivationState(value) {
585
833
  kind: enumValue(change.kind, new Set(['openspec', 'spec-kit']), 'activationState.activeChange.kind')
586
834
  };
587
835
  }
588
- const applicability = exact(state.applicability, [
836
+ let successorHistory;
837
+ if (state.successorHistory !== undefined) {
838
+ const history = exact(state.successorHistory, [
839
+ 'schemaVersion', 'snapshotId', 'journalPathParts', 'historyIndexPathParts', 'historyIndexDigest', 'sourceActiveChange'
840
+ ], 'activationState.successorHistory');
841
+ requireVersion(history.schemaVersion, 1, 'activationState.successorHistory.schemaVersion');
842
+ const snapshotId = hexDigest(history.snapshotId, 'activationState.successorHistory.snapshotId');
843
+ const journalPath = safePathParts(history.journalPathParts, 'activationState.successorHistory.journalPathParts');
844
+ const indexPath = safePathParts(history.historyIndexPathParts, 'activationState.successorHistory.historyIndexPathParts');
845
+ if (journalPath.join('/') !== 'governance/migration-state.json' ||
846
+ indexPath.join('/') !== `governance/history/${snapshotId}/index.json`) {
847
+ throw new Error('activationState.successorHistory must name its exact registered migration journal and snapshot index.');
848
+ }
849
+ let sourceActiveChange = null;
850
+ if (history.sourceActiveChange !== null) {
851
+ const source = exact(history.sourceActiveChange, ['id', 'kind'], 'activationState.successorHistory.sourceActiveChange');
852
+ const id = stringField(source, 'id', 'activationState.successorHistory.sourceActiveChange');
853
+ safePathParts([id], 'activationState.successorHistory.sourceActiveChange.id');
854
+ sourceActiveChange = {
855
+ id, kind: enumValue(source.kind, new Set(['openspec', 'spec-kit']), 'activationState.successorHistory.sourceActiveChange.kind')
856
+ };
857
+ }
858
+ successorHistory = {
859
+ schemaVersion: 1, snapshotId, journalPathParts: ['governance', 'migration-state.json'],
860
+ historyIndexPathParts: indexPath,
861
+ historyIndexDigest: hexDigest(history.historyIndexDigest, 'activationState.successorHistory.historyIndexDigest'),
862
+ sourceActiveChange
863
+ };
864
+ }
865
+ const applicability = exactWithOptional(state.applicability, [
589
866
  'statePath',
590
867
  'privateStagingDast',
591
868
  'credentialRequired'
592
- ], 'activationState.applicability');
869
+ ], ['cloudStateRequired', 'privateRunnerRequired'], 'activationState.applicability');
593
870
  const phases = record(state.phases, 'activationState.phases');
594
871
  const phaseStatesById = {};
595
872
  for (const id of phaseIds) {
596
873
  if (!Object.hasOwn(phases, id)) {
597
874
  throw new Error(`activationState.phases.${id} is required.`);
598
875
  }
599
- const phase = exact(phases[id], ['state', 'updatedAt', 'evidence', 'approvals', 'blockers'], `activationState.phases.${id}`);
876
+ const phase = exactWithOptional(phases[id], ['state', 'updatedAt', 'evidence', 'approvals', 'blockers'], ['operation', 'executionPlanDigest'], `activationState.phases.${id}`);
600
877
  const phaseState = enumValue(phase.state, new Set(phaseStates), `activationState.phases.${id}.state`);
601
878
  if (!Array.isArray(phase.evidence) || !Array.isArray(phase.approvals) || !Array.isArray(phase.blockers)) {
602
879
  throw new Error(`activationState.phases.${id} evidence, approvals, and blockers must be arrays.`);
@@ -606,9 +883,18 @@ export function validateUserActivationState(value) {
606
883
  updatedAt: stringField(phase, 'updatedAt', `activationState.phases.${id}`),
607
884
  evidence: phase.evidence.map((entry, index) => validateEvidenceReference(entry, `activationState.phases.${id}.evidence[${index}]`)),
608
885
  approvals: stringArray(phase.approvals, `activationState.phases.${id}.approvals`),
609
- blockers: stringArray(phase.blockers, `activationState.phases.${id}.blockers`)
886
+ blockers: stringArray(phase.blockers, `activationState.phases.${id}.blockers`),
887
+ ...(phase.operation === undefined ? {} : { operation: validateExternalOperation(phase.operation, `activationState.phases.${id}.operation`) }),
888
+ ...(phase.executionPlanDigest === undefined ? {} : { executionPlanDigest: hexDigest(phase.executionPlanDigest, `activationState.phases.${id}.executionPlanDigest`) })
610
889
  };
611
890
  }
891
+ const phaseOutputs = {};
892
+ if (state.phaseOutputs !== undefined) {
893
+ for (const [id, outputs] of Object.entries(record(state.phaseOutputs, 'activationState.phaseOutputs'))) {
894
+ phaseOutputs[enumValue(id, phaseIdSet, 'activationState.phaseOutputs')] =
895
+ validateOutputBindings(outputs, `activationState.phaseOutputs.${id}`);
896
+ }
897
+ }
612
898
  for (const key of Object.keys(phases)) {
613
899
  if (!phaseIdSet.has(key)) {
614
900
  throw new Error(`activationState.phases.${key} is not a canonical phase.`);
@@ -659,10 +945,21 @@ export function validateUserActivationState(value) {
659
945
  applicability: {
660
946
  statePath: enumValue(applicability.statePath, new Set(['existing-private', 'bootstrap-local', 'none']), 'activationState.applicability.statePath'),
661
947
  privateStagingDast: applicability.privateStagingDast === 'unknown' ? 'unknown' : booleanField(applicability, 'privateStagingDast', 'activationState.applicability'),
662
- credentialRequired: applicability.credentialRequired === 'unknown' ? 'unknown' : booleanField(applicability, 'credentialRequired', 'activationState.applicability')
948
+ credentialRequired: applicability.credentialRequired === 'unknown' ? 'unknown' : booleanField(applicability, 'credentialRequired', 'activationState.applicability'),
949
+ ...(applicability.cloudStateRequired === undefined ? {} : {
950
+ cloudStateRequired: applicability.cloudStateRequired === 'unknown' ? 'unknown' : booleanField(applicability, 'cloudStateRequired', 'activationState.applicability')
951
+ }),
952
+ ...(applicability.privateRunnerRequired === undefined ? {} : {
953
+ privateRunnerRequired: applicability.privateRunnerRequired === 'unknown' ? 'unknown' : booleanField(applicability, 'privateRunnerRequired', 'activationState.applicability')
954
+ })
663
955
  },
664
956
  ...(remoteBinding ? { remoteBinding } : {}),
665
957
  ...(bootstrapState ? { bootstrapState } : {}),
958
+ ...(state.baselineAnchor === undefined ? {} : { baselineAnchor: hexDigest(state.baselineAnchor, 'activationState.baselineAnchor') }),
959
+ ...(successorHistory ? { successorHistory } : {}),
960
+ ...(state.taskProjection === undefined ? {} : { taskProjection: validateGovernanceTaskProjectionRecord(state.taskProjection) }),
961
+ ...(state.activationInputs === undefined ? {} : { activationInputs: validateActivationConfiguration(state.activationInputs) }),
962
+ ...(state.phaseOutputs === undefined ? {} : { phaseOutputs }),
666
963
  phases: phaseStatesById,
667
964
  createdAt: stringField(state, 'createdAt', 'activationState'),
668
965
  updatedAt: stringField(state, 'updatedAt', 'activationState')
@@ -683,7 +980,7 @@ export function validateEvidenceHeader(value) {
683
980
  'producer',
684
981
  'bodyDigest',
685
982
  'result'
686
- ], ['remoteBindingDigest'], 'evidenceHeader');
983
+ ], ['remoteBindingDigest', 'scope', 'inputBindings'], 'evidenceHeader');
687
984
  requireVersion(header.schemaVersion, evidenceHeaderSchemaVersion, 'evidenceHeader.schemaVersion');
688
985
  const identity = validateActivationIdentity(header.identity);
689
986
  const phaseGraphHash = hexDigest(header.phaseGraphHash, 'evidenceHeader.phaseGraphHash');
@@ -701,7 +998,28 @@ export function validateEvidenceHeader(value) {
701
998
  if (transition.baselineSha !== baselineSha) {
702
999
  throw new Error('evidenceHeader.transition.baselineSha must match evidenceHeader.baselineSha.');
703
1000
  }
704
- if (transition.inputDigest !== inputDigest) {
1001
+ let inputBindings;
1002
+ if (header.inputBindings !== undefined) {
1003
+ const bindings = exactWithOptional(header.inputBindings, ['beforeDigest', 'afterDigest', 'files'], ['git'], 'evidenceHeader.inputBindings');
1004
+ const git = bindings.git === undefined ? undefined : exact(bindings.git, ['before', 'after'], 'evidenceHeader.inputBindings.git');
1005
+ inputBindings = {
1006
+ beforeDigest: hexDigest(bindings.beforeDigest, 'evidenceHeader.inputBindings.beforeDigest'),
1007
+ afterDigest: hexDigest(bindings.afterDigest, 'evidenceHeader.inputBindings.afterDigest'),
1008
+ files: validateFileChanges(bindings.files, 'evidenceHeader.inputBindings.files'),
1009
+ ...(git ? { git: {
1010
+ before: validateGitInput(git.before, 'evidenceHeader.inputBindings.git.before'),
1011
+ after: validateGitInput(git.after, 'evidenceHeader.inputBindings.git.after')
1012
+ } } : {})
1013
+ };
1014
+ if (inputBindings.beforeDigest !== transition.inputDigest || inputBindings.afterDigest !== inputDigest) {
1015
+ throw new Error('evidenceHeader.inputBindings must bind the reviewed before-input and observed after-input.');
1016
+ }
1017
+ if (inputBindings.beforeDigest !== inputBindings.afterDigest && !inputBindings.git &&
1018
+ !inputBindings.files.some((file) => file.beforeHash !== file.afterHash)) {
1019
+ throw new Error('Changed phase inputs require a concrete reviewed file or Git transition binding.');
1020
+ }
1021
+ }
1022
+ if (!inputBindings && transition.inputDigest !== inputDigest) {
705
1023
  throw new Error('evidenceHeader.transition.inputDigest must match evidenceHeader.inputDigest.');
706
1024
  }
707
1025
  return {
@@ -720,6 +1038,10 @@ export function validateEvidenceHeader(value) {
720
1038
  ...(header.remoteBindingDigest !== undefined ? {
721
1039
  remoteBindingDigest: hexDigest(header.remoteBindingDigest, 'evidenceHeader.remoteBindingDigest')
722
1040
  } : {}),
1041
+ ...(header.scope === undefined ? {} : {
1042
+ scope: enumValue(header.scope, new Set([phaseScope(phaseId)]), 'evidenceHeader.scope')
1043
+ }),
1044
+ ...(inputBindings ? { inputBindings } : {}),
723
1045
  result: enumValue(header.result, resultStateSet, 'evidenceHeader.result')
724
1046
  };
725
1047
  }
@@ -798,7 +1120,7 @@ function identityMatches(left, right) {
798
1120
  return JSON.stringify(left) === JSON.stringify(right);
799
1121
  }
800
1122
  export function validateApprovalEnvelope(value, options = {}) {
801
- const envelope = exact(value, [
1123
+ const envelope = exactWithOptional(value, [
802
1124
  'schemaVersion',
803
1125
  'id',
804
1126
  'phaseId',
@@ -815,7 +1137,7 @@ export function validateApprovalEnvelope(value, options = {}) {
815
1137
  'expiresAt',
816
1138
  'approvedAt',
817
1139
  'approver'
818
- ], 'approvalEnvelope');
1140
+ ], ['scope', 'coveredPhases', 'operationDigests', 'phasePlanDigests'], 'approvalEnvelope');
819
1141
  requireVersion(envelope.schemaVersion, approvalEnvelopeSchemaVersion, 'approvalEnvelope.schemaVersion');
820
1142
  const cost = exact(envelope.costCeiling, [
821
1143
  'currency',
@@ -860,6 +1182,29 @@ export function validateApprovalEnvelope(value, options = {}) {
860
1182
  if (expectedGateKind !== gateKind) {
861
1183
  throw new Error(`approvalEnvelope.gateKind ${gateKind} does not match phase ${phaseId} gate ${expectedGateKind}.`);
862
1184
  }
1185
+ const coveredPhases = envelope.coveredPhases === undefined ? undefined : stringArray(envelope.coveredPhases, 'approvalEnvelope.coveredPhases')
1186
+ .map((id) => enumValue(id, phaseIdSet, 'approvalEnvelope.coveredPhases'));
1187
+ const operationDigests = envelope.operationDigests === undefined ? undefined : stringArray(envelope.operationDigests, 'approvalEnvelope.operationDigests')
1188
+ .map((digest) => hexDigest(digest, 'approvalEnvelope.operationDigests'));
1189
+ if (coveredPhases) {
1190
+ assertNoDuplicateStrings(coveredPhases, 'approvalEnvelope.coveredPhases');
1191
+ if (!coveredPhases.includes(phaseId) || coveredPhases.some((id) => phaseScope(id) !== phaseScope(phaseId) || canonicalPhaseGraph.phases.find((phase) => phase.id === id)?.approvalGate.kind !== gateKind)) {
1192
+ throw new Error('An approval bundle must contain its primary phase and only phases in the same scope and authority gate.');
1193
+ }
1194
+ }
1195
+ if (operationDigests)
1196
+ assertNoDuplicateStrings(operationDigests, 'approvalEnvelope.operationDigests');
1197
+ const phasePlanDigests = {};
1198
+ if (envelope.phasePlanDigests !== undefined) {
1199
+ for (const [id, digest] of Object.entries(record(envelope.phasePlanDigests, 'approvalEnvelope.phasePlanDigests'))) {
1200
+ phasePlanDigests[enumValue(id, phaseIdSet, 'approvalEnvelope.phasePlanDigests')] =
1201
+ hexDigest(digest, `approvalEnvelope.phasePlanDigests.${id}`);
1202
+ }
1203
+ exactStringSet(Object.keys(phasePlanDigests), coveredPhases ?? [phaseId], 'approvalEnvelope.phasePlanDigests');
1204
+ }
1205
+ else if ((coveredPhases?.length ?? 0) > 1) {
1206
+ throw new Error('An approval covering multiple phases requires every exact phase plan digest.');
1207
+ }
863
1208
  return {
864
1209
  schemaVersion: approvalEnvelopeSchemaVersion,
865
1210
  id: stringField(envelope, 'id', 'approvalEnvelope'),
@@ -876,7 +1221,11 @@ export function validateApprovalEnvelope(value, options = {}) {
876
1221
  destructiveScope,
877
1222
  expiresAt,
878
1223
  approvedAt,
879
- approver: stringField(envelope, 'approver', 'approvalEnvelope')
1224
+ approver: stringField(envelope, 'approver', 'approvalEnvelope'),
1225
+ ...(envelope.scope === undefined ? {} : { scope: enumValue(envelope.scope, new Set([phaseScope(phaseId)]), 'approvalEnvelope.scope') }),
1226
+ ...(coveredPhases ? { coveredPhases } : {}),
1227
+ ...(operationDigests ? { operationDigests } : {}),
1228
+ ...(envelope.phasePlanDigests === undefined ? {} : { phasePlanDigests })
880
1229
  };
881
1230
  }
882
1231
  function validateOperationDestination(value, path) {
@@ -884,7 +1233,7 @@ function validateOperationDestination(value, path) {
884
1233
  const normalized = {
885
1234
  type: enumValue(destination.type, new Set(['local', 'repository', 'subscription', 'environment', 'tenant', 'external']), `${path}.type`),
886
1235
  identity: stringField(destination, 'identity', path),
887
- ...(destination.pathParts !== undefined ? { pathParts: stringArray(destination.pathParts, `${path}.pathParts`) } : {}),
1236
+ ...(destination.pathParts !== undefined ? { pathParts: safePathParts(destination.pathParts, `${path}.pathParts`) } : {}),
888
1237
  ...(destination.repository !== undefined ? { repository: stringField(destination, 'repository', path) } : {}),
889
1238
  ...(destination.subscriptionId !== undefined ? { subscriptionId: stringField(destination, 'subscriptionId', path) } : {}),
890
1239
  ...(destination.ref !== undefined ? { ref: stringField(destination, 'ref', path) } : {})
@@ -892,7 +1241,7 @@ function validateOperationDestination(value, path) {
892
1241
  return normalized;
893
1242
  }
894
1243
  function validateTransitionOperation(value, path) {
895
- const operation = exact(value, [
1244
+ const operation = exactWithOptional(value, [
896
1245
  'adapter',
897
1246
  'actionId',
898
1247
  'mutationClass',
@@ -901,16 +1250,30 @@ function validateTransitionOperation(value, path) {
901
1250
  'destination',
902
1251
  'remote',
903
1252
  'destructive'
904
- ], path);
1253
+ ], ['effects'], path);
1254
+ const effects = operation.effects === undefined ? undefined : (() => {
1255
+ if (!Array.isArray(operation.effects))
1256
+ throw new Error(`${path}.effects must be an array.`);
1257
+ return operation.effects.map((entry, index) => {
1258
+ const effect = exact(entry, ['mutationClass', 'destination', 'remote', 'destructive'], `${path}.effects[${index}]`);
1259
+ return {
1260
+ mutationClass: enumValue(effect.mutationClass, new Set(mutationClasses), `${path}.effects[${index}].mutationClass`),
1261
+ destination: validateOperationDestination(effect.destination, `${path}.effects[${index}].destination`),
1262
+ remote: booleanField(effect, 'remote', `${path}.effects[${index}]`),
1263
+ destructive: booleanField(effect, 'destructive', `${path}.effects[${index}]`)
1264
+ };
1265
+ });
1266
+ })();
905
1267
  return {
906
1268
  adapter: enumValue(operation.adapter, transitionAdapterIds, `${path}.adapter`),
907
1269
  actionId: stringField(operation, 'actionId', path),
908
1270
  mutationClass: enumValue(operation.mutationClass, new Set(mutationClasses), `${path}.mutationClass`),
909
1271
  phaseId: enumValue(operation.phaseId, phaseIdSet, `${path}.phaseId`),
910
- inputs: record(operation.inputs, `${path}.inputs`),
1272
+ inputs: record(publicJson(operation.inputs, `${path}.inputs`), `${path}.inputs`),
911
1273
  destination: validateOperationDestination(operation.destination, `${path}.destination`),
912
1274
  remote: booleanField(operation, 'remote', path),
913
- destructive: booleanField(operation, 'destructive', path)
1275
+ destructive: booleanField(operation, 'destructive', path),
1276
+ ...(effects ? { effects } : {})
914
1277
  };
915
1278
  }
916
1279
  function validateApprovalEvaluation(value, path) {
@@ -983,8 +1346,9 @@ function validateRollbackPlan(value, path) {
983
1346
  };
984
1347
  }
985
1348
  export function validateSavedTransitionPlan(value) {
986
- const plan = exact(value, [
1349
+ const plan = exactWithOptional(value, [
987
1350
  'schemaVersion',
1351
+ 'scope',
988
1352
  'phaseId',
989
1353
  'createdAt',
990
1354
  'expiresAt',
@@ -1000,8 +1364,8 @@ export function validateSavedTransitionPlan(value) {
1000
1364
  'approval',
1001
1365
  'rollbackPlan',
1002
1366
  'noSecrets'
1003
- ], 'transitionPlan');
1004
- requireVersion(plan.schemaVersion, 1, 'transitionPlan.schemaVersion');
1367
+ ], ['configuration', 'fileChanges', 'recovery', 'approvalBundle'], 'transitionPlan');
1368
+ requireVersion(plan.schemaVersion, 2, 'transitionPlan.schemaVersion');
1005
1369
  const identity = validateActivationIdentity(plan.identity);
1006
1370
  const graphHash = hexDigest(plan.graphHash, 'transitionPlan.graphHash');
1007
1371
  if (graphHash !== identity.phaseGraphHash) {
@@ -1028,8 +1392,33 @@ export function validateSavedTransitionPlan(value) {
1028
1392
  const envelopeHash = approval.envelopeHash === null
1029
1393
  ? null
1030
1394
  : hexDigest(approval.envelopeHash, 'transitionPlan.approval.envelopeHash');
1395
+ let approvalBundle;
1396
+ if (plan.approvalBundle !== undefined) {
1397
+ if (!Array.isArray(plan.approvalBundle))
1398
+ throw new Error('transitionPlan.approvalBundle must be an array.');
1399
+ approvalBundle = plan.approvalBundle.map((entry, index) => {
1400
+ const item = exact(entry, ['phaseId', 'inputDigest', 'transitionDigest', 'operations', 'fileChanges'], `transitionPlan.approvalBundle[${index}]`);
1401
+ const id = enumValue(item.phaseId, phaseIdSet, 'transitionPlan.approvalBundle.phaseId');
1402
+ const node = canonicalPhaseGraph.phases.find((phase) => phase.id === id);
1403
+ if (id === phaseId || phaseScope(id) !== phaseScope(phaseId) || node.approvalGate.kind !== approval.gateKind) {
1404
+ throw new Error('A bundled phase must be distinct and share the primary phase scope and approval gate.');
1405
+ }
1406
+ if (!Array.isArray(item.operations))
1407
+ throw new Error('Bundled operations must be an array.');
1408
+ const operations = item.operations.map((operation, operationIndex) => validateTransitionOperation(operation, `transitionPlan.approvalBundle[${index}].operations[${operationIndex}]`));
1409
+ if (operations.some((operation) => operation.phaseId !== id))
1410
+ throw new Error('Bundled operations must match their declared phase.');
1411
+ return {
1412
+ phaseId: id, inputDigest: hexDigest(item.inputDigest, 'approvalBundle.inputDigest'),
1413
+ transitionDigest: hexDigest(item.transitionDigest, 'approvalBundle.transitionDigest'),
1414
+ operations, fileChanges: validateFileChanges(item.fileChanges, 'approvalBundle.fileChanges')
1415
+ };
1416
+ });
1417
+ assertNoDuplicateStrings(approvalBundle.map((entry) => entry.phaseId), 'transitionPlan.approvalBundle');
1418
+ }
1031
1419
  return {
1032
- schemaVersion: 1,
1420
+ schemaVersion: 2,
1421
+ scope: enumValue(plan.scope, new Set([phaseScope(phaseId)]), 'transitionPlan.scope'),
1033
1422
  phaseId,
1034
1423
  createdAt: isoTimestamp(plan.createdAt, 'transitionPlan.createdAt'),
1035
1424
  expiresAt: isoTimestamp(plan.expiresAt, 'transitionPlan.expiresAt'),
@@ -1053,6 +1442,10 @@ export function validateSavedTransitionPlan(value) {
1053
1442
  envelopeHash
1054
1443
  },
1055
1444
  rollbackPlan: validateRollbackPlan(plan.rollbackPlan, 'transitionPlan.rollbackPlan'),
1445
+ ...(plan.configuration === undefined ? {} : { configuration: validateActivationConfiguration(plan.configuration) }),
1446
+ ...(plan.fileChanges === undefined ? {} : { fileChanges: validateFileChanges(plan.fileChanges, 'transitionPlan.fileChanges') }),
1447
+ ...(plan.recovery === undefined ? {} : { recovery: booleanField(plan, 'recovery', 'transitionPlan') }),
1448
+ ...(approvalBundle ? { approvalBundle } : {}),
1056
1449
  noSecrets: plan.noSecrets === true ? true : (() => { throw new Error('transitionPlan.noSecrets must be true.'); })()
1057
1450
  };
1058
1451
  }