@intentius/chant 0.79.0 → 0.81.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 (462) hide show
  1. package/dist/audit/core.d.ts.map +1 -1
  2. package/dist/audit/discover.d.ts +30 -0
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/report-html.d.ts +5 -0
  5. package/dist/audit/report-html.d.ts.map +1 -1
  6. package/dist/audit/report-model.d.ts +13 -0
  7. package/dist/audit/report-model.d.ts.map +1 -1
  8. package/dist/audit/terraform-state.d.ts +13 -0
  9. package/dist/audit/terraform-state.d.ts.map +1 -1
  10. package/dist/cli/command-group.d.ts +6 -0
  11. package/dist/cli/command-group.d.ts.map +1 -1
  12. package/dist/cli/commands/audit.d.ts +44 -3
  13. package/dist/cli/commands/audit.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +8 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/doctor.d.ts.map +1 -1
  18. package/dist/cli/commands/import-agents.d.ts.map +1 -1
  19. package/dist/cli/commands/init.d.ts +6 -0
  20. package/dist/cli/commands/init.d.ts.map +1 -1
  21. package/dist/cli/commands/lint.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts.map +1 -1
  23. package/dist/cli/commands/update.d.ts +6 -0
  24. package/dist/cli/commands/update.d.ts.map +1 -1
  25. package/dist/cli/commands/vendor.d.ts +5 -0
  26. package/dist/cli/commands/vendor.d.ts.map +1 -1
  27. package/dist/cli/conflict-check.d.ts +6 -0
  28. package/dist/cli/conflict-check.d.ts.map +1 -1
  29. package/dist/cli/handlers/build.d.ts.map +1 -1
  30. package/dist/cli/handlers/components.d.ts.map +1 -1
  31. package/dist/cli/handlers/dev.d.ts.map +1 -1
  32. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  33. package/dist/cli/handlers/init.d.ts.map +1 -1
  34. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  35. package/dist/cli/handlers/misc.d.ts.map +1 -1
  36. package/dist/cli/handlers/operator.d.ts +7 -0
  37. package/dist/cli/handlers/operator.d.ts.map +1 -1
  38. package/dist/cli/handlers/promote.d.ts +20 -0
  39. package/dist/cli/handlers/promote.d.ts.map +1 -0
  40. package/dist/cli/handlers/run-generate.d.ts +39 -0
  41. package/dist/cli/handlers/run-generate.d.ts.map +1 -0
  42. package/dist/cli/handlers/run.d.ts +4 -1
  43. package/dist/cli/handlers/run.d.ts.map +1 -1
  44. package/dist/cli/handlers/vendor.d.ts +8 -3
  45. package/dist/cli/handlers/vendor.d.ts.map +1 -1
  46. package/dist/cli/main.d.ts +12 -0
  47. package/dist/cli/main.d.ts.map +1 -1
  48. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  49. package/dist/cli/plugins.d.ts +14 -0
  50. package/dist/cli/plugins.d.ts.map +1 -1
  51. package/dist/cli/registry.d.ts +46 -1
  52. package/dist/cli/registry.d.ts.map +1 -1
  53. package/dist/codegen/fetch.d.ts +10 -0
  54. package/dist/codegen/fetch.d.ts.map +1 -1
  55. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  56. package/dist/components/cli-support.d.ts +3 -1
  57. package/dist/components/cli-support.d.ts.map +1 -1
  58. package/dist/components/discover.d.ts.map +1 -1
  59. package/dist/components/driver.d.ts +16 -2
  60. package/dist/components/driver.d.ts.map +1 -1
  61. package/dist/components/fan-out-support.d.ts.map +1 -1
  62. package/dist/components/gate-plan.d.ts +40 -0
  63. package/dist/components/gate-plan.d.ts.map +1 -0
  64. package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
  65. package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
  66. package/dist/components/presets/ecs-fargate.d.ts +10 -4
  67. package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
  68. package/dist/components/promote.d.ts +264 -0
  69. package/dist/components/promote.d.ts.map +1 -0
  70. package/dist/components/sandbox/driver.d.ts.map +1 -1
  71. package/dist/components/sandbox/run.d.ts.map +1 -1
  72. package/dist/components/verbs/build-archive.d.ts +22 -11
  73. package/dist/components/verbs/build-archive.d.ts.map +1 -1
  74. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  75. package/dist/config-static.d.ts +59 -0
  76. package/dist/config-static.d.ts.map +1 -0
  77. package/dist/config.d.ts +104 -5
  78. package/dist/config.d.ts.map +1 -1
  79. package/dist/content-digest.d.ts +17 -0
  80. package/dist/content-digest.d.ts.map +1 -0
  81. package/dist/discovery/files.d.ts +48 -4
  82. package/dist/discovery/files.d.ts.map +1 -1
  83. package/dist/discovery/fold-import.d.ts +32 -1
  84. package/dist/discovery/fold-import.d.ts.map +1 -1
  85. package/dist/discovery/index.d.ts.map +1 -1
  86. package/dist/discovery/sandbox/driver.d.ts +20 -0
  87. package/dist/discovery/sandbox/driver.d.ts.map +1 -1
  88. package/dist/discovery/sandbox/fork.d.ts +20 -0
  89. package/dist/discovery/sandbox/fork.d.ts.map +1 -1
  90. package/dist/discovery/walk.d.ts +119 -0
  91. package/dist/discovery/walk.d.ts.map +1 -0
  92. package/dist/graph-ir.d.ts +17 -1
  93. package/dist/graph-ir.d.ts.map +1 -1
  94. package/dist/join-key.d.ts +31 -0
  95. package/dist/join-key.d.ts.map +1 -0
  96. package/dist/lexicon-module.d.ts +110 -0
  97. package/dist/lexicon-module.d.ts.map +1 -0
  98. package/dist/lexicon.d.ts +82 -1
  99. package/dist/lexicon.d.ts.map +1 -1
  100. package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
  101. package/dist/lifecycle/digest.d.ts +5 -1
  102. package/dist/lifecycle/digest.d.ts.map +1 -1
  103. package/dist/lifecycle/gate-ledger.d.ts +20 -5
  104. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  105. package/dist/lifecycle/git.d.ts +64 -0
  106. package/dist/lifecycle/git.d.ts.map +1 -1
  107. package/dist/lifecycle/lease.d.ts +8 -1
  108. package/dist/lifecycle/lease.d.ts.map +1 -1
  109. package/dist/lifecycle/legacy-digest.d.ts +59 -0
  110. package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
  111. package/dist/lifecycle/member-ledger.d.ts +67 -0
  112. package/dist/lifecycle/member-ledger.d.ts.map +1 -0
  113. package/dist/lifecycle/plan-digest.d.ts +4 -3
  114. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  115. package/dist/lifecycle/release-ledger.d.ts +42 -1
  116. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  117. package/dist/lifecycle/status.d.ts +90 -5
  118. package/dist/lifecycle/status.d.ts.map +1 -1
  119. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
  120. package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
  121. package/dist/op/activities/activity-contracts.d.ts +32 -0
  122. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  123. package/dist/op/activities/index.d.ts +2 -0
  124. package/dist/op/activities/index.d.ts.map +1 -1
  125. package/dist/op/activities/propose-upgrade.d.ts +75 -0
  126. package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
  127. package/dist/op/activity-contract-registry.d.ts.map +1 -1
  128. package/dist/op/activity-registry.d.ts.map +1 -1
  129. package/dist/op/discover.d.ts.map +1 -1
  130. package/dist/op/gate-approval.d.ts.map +1 -1
  131. package/dist/op/gate-name.d.ts +7 -0
  132. package/dist/op/gate-name.d.ts.map +1 -1
  133. package/dist/op/gate-summary.d.ts +16 -0
  134. package/dist/op/gate-summary.d.ts.map +1 -1
  135. package/dist/op/gate.d.ts +16 -2
  136. package/dist/op/gate.d.ts.map +1 -1
  137. package/dist/op/generate-pipeline.d.ts.map +1 -1
  138. package/dist/op/runtimes/local.d.ts.map +1 -1
  139. package/dist/project-root.d.ts +26 -0
  140. package/dist/project-root.d.ts.map +1 -1
  141. package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
  142. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
  143. package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
  144. package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
  145. package/dist/workspace/checks/generated.d.ts +156 -0
  146. package/dist/workspace/checks/generated.d.ts.map +1 -0
  147. package/dist/workspace/checks/ledgers.d.ts +78 -0
  148. package/dist/workspace/checks/ledgers.d.ts.map +1 -0
  149. package/dist/workspace/checks/links.d.ts +25 -0
  150. package/dist/workspace/checks/links.d.ts.map +1 -0
  151. package/dist/workspace/checks/pipelines.d.ts +76 -0
  152. package/dist/workspace/checks/pipelines.d.ts.map +1 -0
  153. package/dist/workspace/checks.d.ts +161 -0
  154. package/dist/workspace/checks.d.ts.map +1 -0
  155. package/dist/workspace/compose-graph.d.ts +115 -0
  156. package/dist/workspace/compose-graph.d.ts.map +1 -0
  157. package/dist/workspace/compose-reports.d.ts +68 -0
  158. package/dist/workspace/compose-reports.d.ts.map +1 -0
  159. package/dist/workspace/declaration.d.ts +220 -0
  160. package/dist/workspace/declaration.d.ts.map +1 -0
  161. package/dist/workspace/declaration.schema.json +434 -0
  162. package/dist/workspace/generated-files.d.ts +65 -0
  163. package/dist/workspace/generated-files.d.ts.map +1 -0
  164. package/dist/workspace/glob.d.ts +13 -0
  165. package/dist/workspace/glob.d.ts.map +1 -0
  166. package/dist/workspace/graph-cli.d.ts +75 -0
  167. package/dist/workspace/graph-cli.d.ts.map +1 -0
  168. package/dist/workspace/init.d.ts +96 -0
  169. package/dist/workspace/init.d.ts.map +1 -0
  170. package/dist/workspace/jsonc.d.ts +38 -0
  171. package/dist/workspace/jsonc.d.ts.map +1 -0
  172. package/dist/workspace/kinds.d.ts +192 -0
  173. package/dist/workspace/kinds.d.ts.map +1 -0
  174. package/dist/workspace/lineage-check.d.ts +84 -0
  175. package/dist/workspace/lineage-check.d.ts.map +1 -0
  176. package/dist/workspace/lineage-cli.d.ts +31 -0
  177. package/dist/workspace/lineage-cli.d.ts.map +1 -0
  178. package/dist/workspace/lineage-init.d.ts +101 -0
  179. package/dist/workspace/lineage-init.d.ts.map +1 -0
  180. package/dist/workspace/lineage-lock.d.ts +258 -0
  181. package/dist/workspace/lineage-lock.d.ts.map +1 -0
  182. package/dist/workspace/lineage-merge.d.ts +14 -0
  183. package/dist/workspace/lineage-merge.d.ts.map +1 -0
  184. package/dist/workspace/lineage-migrations.d.ts +178 -0
  185. package/dist/workspace/lineage-migrations.d.ts.map +1 -0
  186. package/dist/workspace/lineage-update.d.ts +59 -0
  187. package/dist/workspace/lineage-update.d.ts.map +1 -0
  188. package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
  189. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
  190. package/dist/workspace/lineage-upgrade.d.ts +146 -0
  191. package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
  192. package/dist/workspace/lineage-vendor.d.ts +63 -0
  193. package/dist/workspace/lineage-vendor.d.ts.map +1 -0
  194. package/dist/workspace/lineage-version.d.ts +32 -0
  195. package/dist/workspace/lineage-version.d.ts.map +1 -0
  196. package/dist/workspace/links.d.ts +150 -0
  197. package/dist/workspace/links.d.ts.map +1 -0
  198. package/dist/workspace/ls.d.ts +99 -0
  199. package/dist/workspace/ls.d.ts.map +1 -0
  200. package/dist/workspace/member-commands.d.ts +139 -0
  201. package/dist/workspace/member-commands.d.ts.map +1 -0
  202. package/dist/workspace/member-handles.d.ts +54 -0
  203. package/dist/workspace/member-handles.d.ts.map +1 -0
  204. package/dist/workspace/member-pipeline.d.ts +143 -0
  205. package/dist/workspace/member-pipeline.d.ts.map +1 -0
  206. package/dist/workspace/member-run.d.ts +67 -0
  207. package/dist/workspace/member-run.d.ts.map +1 -0
  208. package/dist/workspace/reason-codes.d.ts +65 -0
  209. package/dist/workspace/reason-codes.d.ts.map +1 -0
  210. package/dist/workspace/record-source.d.ts +22 -0
  211. package/dist/workspace/record-source.d.ts.map +1 -0
  212. package/dist/workspace/records-cli.d.ts +81 -0
  213. package/dist/workspace/records-cli.d.ts.map +1 -0
  214. package/dist/workspace/records.d.ts +118 -0
  215. package/dist/workspace/records.d.ts.map +1 -0
  216. package/dist/workspace/root-refusal.d.ts +53 -0
  217. package/dist/workspace/root-refusal.d.ts.map +1 -0
  218. package/dist/workspace/status.d.ts +150 -0
  219. package/dist/workspace/status.d.ts.map +1 -0
  220. package/dist/workspace/template-manifest.d.ts +90 -0
  221. package/dist/workspace/template-manifest.d.ts.map +1 -0
  222. package/dist/workspace/tree.d.ts +36 -0
  223. package/dist/workspace/tree.d.ts.map +1 -0
  224. package/dist/workspace/trust/attestor.d.ts +67 -0
  225. package/dist/workspace/trust/attestor.d.ts.map +1 -0
  226. package/dist/workspace/trust/policy.d.ts +111 -0
  227. package/dist/workspace/trust/policy.d.ts.map +1 -0
  228. package/dist/workspace/trust/provenance.d.ts +67 -0
  229. package/dist/workspace/trust/provenance.d.ts.map +1 -0
  230. package/dist/workspace/trust/ssh-commit.d.ts +43 -0
  231. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
  232. package/dist/workspace/trust/test-repo.d.ts +42 -0
  233. package/dist/workspace/trust/test-repo.d.ts.map +1 -0
  234. package/dist/workspace/trust/verify-cli.d.ts +9 -0
  235. package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
  236. package/dist/workspace/trust/verify.d.ts +66 -0
  237. package/dist/workspace/trust/verify.d.ts.map +1 -0
  238. package/dist/workspace/which-chant.d.ts +58 -0
  239. package/dist/workspace/which-chant.d.ts.map +1 -0
  240. package/dist/workspace/workspace-kinds.schema.json +133 -0
  241. package/package.json +4 -3
  242. package/src/audit/core.ts +13 -2
  243. package/src/audit/discover.ts +102 -36
  244. package/src/audit/report-html.ts +3 -1
  245. package/src/audit/report-model.ts +14 -1
  246. package/src/audit/terraform-state.test.ts +81 -1
  247. package/src/audit/terraform-state.ts +25 -0
  248. package/src/cli/command-group.ts +7 -0
  249. package/src/cli/commands/audit-truncation.test.ts +167 -0
  250. package/src/cli/commands/audit.ts +132 -29
  251. package/src/cli/commands/check-lexicon-examples.ts +2 -1
  252. package/src/cli/commands/check-lexicon.ts +24 -0
  253. package/src/cli/commands/doctor.test.ts +30 -0
  254. package/src/cli/commands/doctor.ts +21 -0
  255. package/src/cli/commands/import-agents.ts +15 -4
  256. package/src/cli/commands/init.ts +76 -25
  257. package/src/cli/commands/lint.test.ts +52 -1
  258. package/src/cli/commands/lint.ts +41 -71
  259. package/src/cli/commands/onboard.ts +15 -0
  260. package/src/cli/commands/update.ts +18 -5
  261. package/src/cli/commands/vendor.ts +11 -6
  262. package/src/cli/conflict-check.test.ts +18 -1
  263. package/src/cli/conflict-check.ts +13 -0
  264. package/src/cli/discovery-skip.test.ts +168 -0
  265. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
  266. package/src/cli/handlers/build-member.test.ts +127 -0
  267. package/src/cli/handlers/build.test.ts +11 -0
  268. package/src/cli/handlers/build.ts +35 -1
  269. package/src/cli/handlers/components.test.ts +127 -0
  270. package/src/cli/handlers/components.ts +70 -8
  271. package/src/cli/handlers/dev.ts +6 -1
  272. package/src/cli/handlers/fan-out.ts +9 -2
  273. package/src/cli/handlers/graph.test.ts +20 -0
  274. package/src/cli/handlers/graph.ts +4 -2
  275. package/src/cli/handlers/init.ts +54 -1
  276. package/src/cli/handlers/lifecycle.ts +2 -1
  277. package/src/cli/handlers/misc.ts +16 -0
  278. package/src/cli/handlers/operator.test.ts +68 -0
  279. package/src/cli/handlers/operator.ts +39 -10
  280. package/src/cli/handlers/promote.test.ts +377 -0
  281. package/src/cli/handlers/promote.ts +391 -0
  282. package/src/cli/handlers/run-generate-member.test.ts +83 -0
  283. package/src/cli/handlers/run-generate.test.ts +158 -0
  284. package/src/cli/handlers/run-generate.ts +168 -0
  285. package/src/cli/handlers/run.test.ts +47 -0
  286. package/src/cli/handlers/run.ts +41 -6
  287. package/src/cli/handlers/vendor.ts +147 -43
  288. package/src/cli/main.test.ts +137 -1
  289. package/src/cli/main.ts +290 -24
  290. package/src/cli/mcp/op-tools.ts +2 -1
  291. package/src/cli/path-lexicon-messages.test.ts +152 -0
  292. package/src/cli/plugins.ts +43 -5
  293. package/src/cli/registry.ts +46 -1
  294. package/src/cli/static-config-read.test.ts +208 -0
  295. package/src/codegen/fetch.test.ts +63 -1
  296. package/src/codegen/fetch.ts +30 -1
  297. package/src/components/__fixtures__/alb-ecs-service.json +0 -8
  298. package/src/components/capability-plugin-loader.ts +3 -1
  299. package/src/components/cli-support.ts +9 -3
  300. package/src/components/discover.test.ts +27 -57
  301. package/src/components/discover.ts +20 -51
  302. package/src/components/driver.test.ts +90 -12
  303. package/src/components/driver.ts +43 -5
  304. package/src/components/fan-out-support.ts +2 -1
  305. package/src/components/gate-plan.ts +53 -0
  306. package/src/components/pilots/README.md +1 -1
  307. package/src/components/pilots/alb-ecs.pilot.ts +7 -7
  308. package/src/components/presets/ecs-fargate.ts +10 -5
  309. package/src/components/promote.test.ts +645 -0
  310. package/src/components/promote.ts +641 -0
  311. package/src/components/sandbox/driver.ts +8 -8
  312. package/src/components/sandbox/run.ts +4 -1
  313. package/src/components/verbs/build-archive.ts +27 -21
  314. package/src/components/verbs/run-agent.ts +3 -5
  315. package/src/config-static.test.ts +136 -0
  316. package/src/config-static.ts +232 -0
  317. package/src/config.test.ts +40 -0
  318. package/src/config.ts +161 -11
  319. package/src/content-digest.test.ts +120 -0
  320. package/src/content-digest.ts +21 -0
  321. package/src/discovery/files.test.ts +127 -1
  322. package/src/discovery/files.ts +91 -59
  323. package/src/discovery/fold-import.ts +127 -9
  324. package/src/discovery/index.ts +1 -0
  325. package/src/discovery/sandbox/driver.ts +73 -35
  326. package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
  327. package/src/discovery/sandbox/fork.ts +83 -43
  328. package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
  329. package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
  330. package/src/discovery/walk.test.ts +263 -0
  331. package/src/discovery/walk.ts +334 -0
  332. package/src/effect-receipt.ts +2 -2
  333. package/src/graph-ir.ts +18 -1
  334. package/src/join-key.test.ts +25 -0
  335. package/src/join-key.ts +39 -0
  336. package/src/lexicon-module.test.ts +203 -0
  337. package/src/lexicon-module.ts +215 -0
  338. package/src/lexicon.ts +105 -1
  339. package/src/lifecycle/build-ledger-store.ts +13 -2
  340. package/src/lifecycle/digest.ts +9 -5
  341. package/src/lifecycle/gate-ledger.test.ts +20 -0
  342. package/src/lifecycle/gate-ledger.ts +28 -6
  343. package/src/lifecycle/git.test.ts +13 -1
  344. package/src/lifecycle/git.ts +221 -176
  345. package/src/lifecycle/lease.ts +25 -10
  346. package/src/lifecycle/legacy-digest.test.ts +206 -0
  347. package/src/lifecycle/legacy-digest.ts +103 -0
  348. package/src/lifecycle/member-ledger.test.ts +319 -0
  349. package/src/lifecycle/member-ledger.ts +145 -0
  350. package/src/lifecycle/plan-digest.ts +7 -7
  351. package/src/lifecycle/release-ledger.test.ts +20 -0
  352. package/src/lifecycle/release-ledger.ts +53 -3
  353. package/src/lifecycle/status.test.ts +151 -1
  354. package/src/lifecycle/status.ts +228 -7
  355. package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
  356. package/src/op/activities/activity-contracts.ts +37 -0
  357. package/src/op/activities/index.ts +10 -0
  358. package/src/op/activities/propose-upgrade.ts +206 -0
  359. package/src/op/activity-contract-registry.ts +18 -4
  360. package/src/op/activity-registry.ts +26 -0
  361. package/src/op/discover.test.ts +14 -4
  362. package/src/op/discover.ts +19 -23
  363. package/src/op/gate-approval.ts +3 -1
  364. package/src/op/gate-name.ts +8 -0
  365. package/src/op/gate-summary.test.ts +14 -0
  366. package/src/op/gate-summary.ts +25 -2
  367. package/src/op/gate.ts +36 -7
  368. package/src/op/generate-pipeline.test.ts +14 -5
  369. package/src/op/generate-pipeline.ts +2 -1
  370. package/src/op/runtimes/local.ts +2 -1
  371. package/src/project-root.test.ts +66 -1
  372. package/src/project-root.ts +48 -0
  373. package/src/workspace/__fixtures__/contract-repo.ts +100 -0
  374. package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
  375. package/src/workspace/behold-kinds.test.ts +162 -0
  376. package/src/workspace/check-contract.test.ts +127 -0
  377. package/src/workspace/check.schema.json +347 -0
  378. package/src/workspace/checks/generated.test.ts +287 -0
  379. package/src/workspace/checks/generated.ts +475 -0
  380. package/src/workspace/checks/ledgers.test.ts +139 -0
  381. package/src/workspace/checks/ledgers.ts +246 -0
  382. package/src/workspace/checks/links.ts +183 -0
  383. package/src/workspace/checks/pipelines.test.ts +133 -0
  384. package/src/workspace/checks/pipelines.ts +225 -0
  385. package/src/workspace/checks-repo.test.ts +71 -0
  386. package/src/workspace/checks.test.ts +450 -0
  387. package/src/workspace/checks.ts +461 -0
  388. package/src/workspace/compose-graph.test.ts +146 -0
  389. package/src/workspace/compose-graph.ts +228 -0
  390. package/src/workspace/compose-reports.test.ts +109 -0
  391. package/src/workspace/compose-reports.ts +186 -0
  392. package/src/workspace/declaration.schema.json +434 -0
  393. package/src/workspace/declaration.test.ts +347 -0
  394. package/src/workspace/declaration.ts +623 -0
  395. package/src/workspace/generated-files.test.ts +114 -0
  396. package/src/workspace/generated-files.ts +94 -0
  397. package/src/workspace/glob.ts +49 -0
  398. package/src/workspace/graph-cli.ts +220 -0
  399. package/src/workspace/graph-contract.test.ts +204 -0
  400. package/src/workspace/graph.schema.json +662 -0
  401. package/src/workspace/init.test.ts +223 -0
  402. package/src/workspace/init.ts +585 -0
  403. package/src/workspace/jsonc.test.ts +65 -0
  404. package/src/workspace/jsonc.ts +231 -0
  405. package/src/workspace/kinds.test.ts +288 -0
  406. package/src/workspace/kinds.ts +506 -0
  407. package/src/workspace/lineage-check.ts +264 -0
  408. package/src/workspace/lineage-cli.ts +114 -0
  409. package/src/workspace/lineage-init.test.ts +231 -0
  410. package/src/workspace/lineage-init.ts +365 -0
  411. package/src/workspace/lineage-lock.test.ts +224 -0
  412. package/src/workspace/lineage-lock.ts +357 -0
  413. package/src/workspace/lineage-merge.ts +44 -0
  414. package/src/workspace/lineage-migrations.test.ts +178 -0
  415. package/src/workspace/lineage-migrations.ts +343 -0
  416. package/src/workspace/lineage-update.ts +160 -0
  417. package/src/workspace/lineage-upgrade-cli.ts +151 -0
  418. package/src/workspace/lineage-upgrade.test.ts +474 -0
  419. package/src/workspace/lineage-upgrade.ts +624 -0
  420. package/src/workspace/lineage-vendor.test.ts +126 -0
  421. package/src/workspace/lineage-vendor.ts +215 -0
  422. package/src/workspace/lineage-version.ts +127 -0
  423. package/src/workspace/links.test.ts +344 -0
  424. package/src/workspace/links.ts +331 -0
  425. package/src/workspace/ls-contract.test.ts +200 -0
  426. package/src/workspace/ls.schema.json +190 -0
  427. package/src/workspace/ls.ts +254 -0
  428. package/src/workspace/member-commands.e2e.test.ts +244 -0
  429. package/src/workspace/member-commands.test.ts +217 -0
  430. package/src/workspace/member-commands.ts +540 -0
  431. package/src/workspace/member-handles.test.ts +76 -0
  432. package/src/workspace/member-handles.ts +228 -0
  433. package/src/workspace/member-pipeline.test.ts +135 -0
  434. package/src/workspace/member-pipeline.ts +317 -0
  435. package/src/workspace/member-run.ts +146 -0
  436. package/src/workspace/read-contract.test.ts +129 -0
  437. package/src/workspace/reason-codes.test.ts +113 -0
  438. package/src/workspace/reason-codes.ts +81 -0
  439. package/src/workspace/record-source.ts +117 -0
  440. package/src/workspace/records-cli.ts +198 -0
  441. package/src/workspace/records-contract.test.ts +93 -0
  442. package/src/workspace/records.schema.json +150 -0
  443. package/src/workspace/records.test.ts +243 -0
  444. package/src/workspace/records.ts +384 -0
  445. package/src/workspace/root-refusal.ts +97 -0
  446. package/src/workspace/status-contract.test.ts +308 -0
  447. package/src/workspace/status.schema.json +210 -0
  448. package/src/workspace/status.ts +412 -0
  449. package/src/workspace/template-manifest.test.ts +79 -0
  450. package/src/workspace/template-manifest.ts +221 -0
  451. package/src/workspace/tree.ts +132 -0
  452. package/src/workspace/trust/attestor.ts +93 -0
  453. package/src/workspace/trust/policy.ts +306 -0
  454. package/src/workspace/trust/provenance.ts +181 -0
  455. package/src/workspace/trust/ssh-commit.ts +138 -0
  456. package/src/workspace/trust/test-repo.ts +138 -0
  457. package/src/workspace/trust/trust.test.ts +397 -0
  458. package/src/workspace/trust/verify-cli.ts +56 -0
  459. package/src/workspace/trust/verify.ts +194 -0
  460. package/src/workspace/which-chant.test.ts +117 -0
  461. package/src/workspace/which-chant.ts +151 -0
  462. package/src/workspace/workspace-kinds.schema.json +133 -0
@@ -15,10 +15,15 @@
15
15
  * for it. Someone (or some pipeline) deployed outside the recorded path.
16
16
  * - **stale** — a release record exists but nothing live corresponds to it
17
17
  * anymore (the component disappeared from observation).
18
- * - **drifted** — the environment's live/owned resources don't confirm the
19
- * recorded digest one way or the other from this join alone (recorded,
20
- * but live identity can't be read back to a digest) — surfaced as a
21
- * lower-confidence signal rather than silently treated as "reconciled".
18
+ * - **drifted** — a release record exists and the component is observed
19
+ * live, but a positive live observation contradicts the record (#2513):
20
+ * the live identity a lexicon reports (`attributes.digest` /
21
+ * `attributes.gitSha`) differs from the recorded release's, the ownership
22
+ * marker went from chant's to someone else's, or the live configuration
23
+ * changed against a lifecycle snapshot taken at or after the release (the
24
+ * `update` verdict `lifecycle plan` gives). Only things chant saw count:
25
+ * a resource it could not read, or one that reports no identity, never
26
+ * makes a component drifted.
22
27
  * - **reconciled** — a release record exists and live evidence (via
23
28
  * ownership) confirms the component is present and owned by chant.
24
29
  * - **unknown** — live evidence was requested and could not be read (#1089),
@@ -37,7 +42,9 @@
37
42
  */
38
43
 
39
44
  import type { ChangeSet, ChangeAction } from "./change-set";
45
+ import type { ResourceMetadata } from "../lexicon";
40
46
  import { latestPerComponent, type ReleaseRecord } from "./release-ledger";
47
+ import { isLegacyContentDigest } from "./legacy-digest";
41
48
  import type { BuildLedgerEntry, ComponentBomSummary } from "./build-ledger";
42
49
  import { unobservedReasonText, type UnobservedReason } from "../observation";
43
50
 
@@ -166,6 +173,99 @@ export interface LiveComponentEvidence {
166
173
  * configured.
167
174
  */
168
175
  rollup?: ComponentResourceRollup;
176
+ /**
177
+ * What the component's live resources say they are running (#2513), one
178
+ * entry per present resource that reported an identity attribute
179
+ * ({@link LIVE_IDENTITY_ATTRIBUTES}). A resource that reports none has no
180
+ * entry: no identity is no evidence, never a mismatch.
181
+ */
182
+ identities?: LiveIdentity[];
183
+ /**
184
+ * Positive live observations that changed against the last lifecycle
185
+ * snapshot (#2513), each stamped with that snapshot's timestamp so the
186
+ * status join can drop a change the release itself made (a snapshot taken
187
+ * before the release). When present, `reconcileStatus` reads these instead
188
+ * of the bare `action === "update"`.
189
+ */
190
+ driftSignals?: LiveDriftSignal[];
191
+ }
192
+
193
+ /**
194
+ * The `ResourceMetadata.attributes` keys a lexicon sets to report which build
195
+ * a live resource is running (#2513), and the release-record field each one is
196
+ * compared with. Opt-in: a lexicon that sets neither is reconciled on presence,
197
+ * ownership and snapshot drift alone, exactly as before.
198
+ *
199
+ * - `digest` — the artifact digest serving now (`sha256:...`). Matches the
200
+ * record's `digest`, or its `inputDigest` when the record has one.
201
+ * - `gitSha` — the commit the live build came from. Matches the record's
202
+ * `gitSha`; an abbreviated sha (7+ characters) matches by prefix.
203
+ */
204
+ export const LIVE_IDENTITY_ATTRIBUTES = { digest: "digest", gitSha: "gitSha" } as const;
205
+
206
+ /** One live resource's reported identity (#2513). */
207
+ export interface LiveIdentity {
208
+ /** The live entity/resource name that reported it. */
209
+ entity: string;
210
+ digest?: string;
211
+ gitSha?: string;
212
+ }
213
+
214
+ /** One live change against the last snapshot (#2513). */
215
+ export interface LiveDriftSignal {
216
+ entity: string;
217
+ /**
218
+ * `attributes` — the change set's `update` (status, physicalId, lastUpdated
219
+ * or an attribute changed). `ownership` — the snapshot read chant's marker
220
+ * and the live read now reads none (`owned` -> `foreign`), or the marker now
221
+ * names a different stack/env. `unknown` on either side is never a signal.
222
+ */
223
+ kind: "attributes" | "ownership";
224
+ /** The snapshot's timestamp. Absent when the caller did not say. */
225
+ since?: string;
226
+ /** Human-readable backing (the changed paths, the marker transition). */
227
+ detail?: string;
228
+ }
229
+
230
+ /**
231
+ * The per-entity observation `liveEvidenceFromChangeSet` reads identity and
232
+ * marker transitions from (#2513). `now` is the live read; `then`/`thenAt`
233
+ * are the last lifecycle snapshot's record for the same entity, when one
234
+ * exists.
235
+ */
236
+ export interface EntityLiveRead {
237
+ now?: ResourceMetadata;
238
+ then?: ResourceMetadata;
239
+ thenAt?: string;
240
+ }
241
+
242
+ function identityOf(entity: string, meta: ResourceMetadata | undefined): LiveIdentity | undefined {
243
+ const attrs = meta?.attributes;
244
+ if (!attrs) return undefined;
245
+ const digest = attrs[LIVE_IDENTITY_ATTRIBUTES.digest];
246
+ const gitSha = attrs[LIVE_IDENTITY_ATTRIBUTES.gitSha];
247
+ const d = typeof digest === "string" && digest.length > 0 ? digest : undefined;
248
+ const g = typeof gitSha === "string" && gitSha.length > 0 ? gitSha : undefined;
249
+ if (!d && !g) return undefined;
250
+ return { entity, ...(d ? { digest: d } : {}), ...(g ? { gitSha: g } : {}) };
251
+ }
252
+
253
+ /**
254
+ * An ownership transition between the snapshot and now that contradicts a
255
+ * chant release. `unknown` on either side answers nothing and is skipped, the
256
+ * same way the change set never escalates `unknown` (#1089, #1168).
257
+ */
258
+ function ownershipChange(then: ResourceMetadata | undefined, now: ResourceMetadata | undefined): string | undefined {
259
+ if (!then || !now) return undefined;
260
+ if (then.ownership === "owned" && now.ownership === "foreign") {
261
+ return "ownership marker was chant's at the last snapshot and is gone now";
262
+ }
263
+ const a = then.marker;
264
+ const b = now.marker;
265
+ if (a && b && (a.stack !== b.stack || (a.env !== undefined && b.env !== undefined && a.env !== b.env))) {
266
+ return `ownership marker changed from ${a.stack}${a.env ? `/${a.env}` : ""} to ${b.stack}${b.env ? `/${b.env}` : ""}`;
267
+ }
268
+ return undefined;
169
269
  }
170
270
 
171
271
  /**
@@ -211,6 +311,11 @@ export function mergeLiveEvidence(
211
311
  // Base first: the counts come from the change set, and a stack
212
312
  // observation has none to offer.
213
313
  ...(b?.rollup ?? sup.rollup ? { rollup: b?.rollup ?? sup.rollup } : {}),
314
+ // Identity and snapshot drift come from the per-resource reads; a stack
315
+ // observation carries neither, so dropping them here would make
316
+ // `drifted` unreachable again on every lexicon with a stack observer.
317
+ ...(b?.identities ? { identities: b.identities } : {}),
318
+ ...(b?.driftSignals ? { driftSignals: b.driftSignals } : {}),
214
319
  });
215
320
  }
216
321
  return merged;
@@ -299,7 +404,18 @@ function mergeEvidence(entries: LiveComponentEvidence[]): LiveComponentEvidence
299
404
  // actually seen live, which already answers "is this deployed".
300
405
  const unobserved = live ? undefined : entries.find((e) => e.unobserved)?.unobserved;
301
406
 
302
- return { live, ownership, action, ...(unobserved ? { unobserved } : {}), rollup: rollUp(entries) };
407
+ const identities = entries.flatMap((e) => e.identities ?? []);
408
+ const driftSignals = entries.flatMap((e) => e.driftSignals ?? []);
409
+
410
+ return {
411
+ live,
412
+ ownership,
413
+ action,
414
+ ...(unobserved ? { unobserved } : {}),
415
+ rollup: rollUp(entries),
416
+ ...(identities.length ? { identities } : {}),
417
+ ...(driftSignals.length ? { driftSignals } : {}),
418
+ };
303
419
  }
304
420
 
305
421
  /**
@@ -317,9 +433,41 @@ function mergeEvidence(entries: LiveComponentEvidence[]): LiveComponentEvidence
317
433
  export function liveEvidenceFromChangeSet(
318
434
  cs: ChangeSet,
319
435
  nameMapping?: LiveNameMapping,
436
+ opts?: {
437
+ /**
438
+ * Per-entity live read and snapshot record (#2513). With it, the evidence
439
+ * carries the identity each live resource reports and the drift signals
440
+ * (stamped with the snapshot's time) `reconcileStatus` reads for
441
+ * `drifted`. Without it, the evidence is what it was before: the change
442
+ * set's `action` alone.
443
+ */
444
+ observations?: Map<string, EntityLiveRead>;
445
+ },
320
446
  ): Map<string, LiveComponentEvidence> {
321
447
  const evidenceByName = new Map<string, LiveComponentEvidence>();
322
448
  for (const entry of cs.entries) {
449
+ const obs = opts?.observations?.get(entry.name);
450
+ // Identity and marker transitions only from a resource that was actually
451
+ // read present. An unobserved or absent entity contributes nothing.
452
+ const identity = entry.evidence.live ? identityOf(entry.name, obs?.now) : undefined;
453
+ const driftSignals: LiveDriftSignal[] = [];
454
+ if (obs && entry.action === "update") {
455
+ driftSignals.push({
456
+ entity: entry.name,
457
+ kind: "attributes",
458
+ ...(obs.thenAt ? { since: obs.thenAt } : {}),
459
+ ...(entry.deltas?.length ? { detail: entry.deltas.map((d) => d.path).join(", ") } : {}),
460
+ });
461
+ }
462
+ const ownershipDetail = obs && entry.evidence.live ? ownershipChange(obs.then, obs.now) : undefined;
463
+ if (ownershipDetail) {
464
+ driftSignals.push({
465
+ entity: entry.name,
466
+ kind: "ownership",
467
+ ...(obs?.thenAt ? { since: obs.thenAt } : {}),
468
+ detail: ownershipDetail,
469
+ });
470
+ }
323
471
  evidenceByName.set(entry.name, {
324
472
  rollup: rollUp([
325
473
  {
@@ -342,6 +490,8 @@ export function liveEvidenceFromChangeSet(
342
490
  },
343
491
  }
344
492
  : {}),
493
+ ...(identity ? { identities: [identity] } : {}),
494
+ ...(driftSignals.length ? { driftSignals } : {}),
345
495
  });
346
496
  }
347
497
 
@@ -404,6 +554,7 @@ export function reconcileStatus(
404
554
 
405
555
  let reconciliation: ComponentStatusRow["reconciliation"];
406
556
  let detail: string;
557
+ let contradiction: string | undefined;
407
558
 
408
559
  if (!liveEvidence) {
409
560
  // No live evidence requested at all (digest-only / offline mode) — the
@@ -437,9 +588,9 @@ export function reconcileStatus(
437
588
  detail = evidence?.partial
438
589
  ? `recorded ${recorded.timestamp} (digest ${recorded.digest}), but only ${evidence.partial.present} of ${evidence.partial.total} deploy units observed live now (missing: ${evidence.partial.missing.join(", ")})`
439
590
  : `recorded ${recorded.timestamp} (digest ${recorded.digest}), but nothing observed live now`;
440
- } else if (recorded && evidence?.action === "update") {
591
+ } else if (recorded && (contradiction = contradictsRecord(recorded, evidence!))) {
441
592
  reconciliation = "drifted";
442
- detail = `recorded ${recorded.timestamp} (digest ${recorded.digest}), but live configuration has drifted since`;
593
+ detail = `recorded ${recorded.timestamp} (digest ${recorded.digest}), but ${contradiction}`;
443
594
  } else {
444
595
  reconciliation = "reconciled";
445
596
  detail = `recorded ${recorded!.timestamp} (digest ${recorded!.digest}), live and consistent`;
@@ -467,6 +618,66 @@ export function reconcileStatus(
467
618
  return rows;
468
619
  }
469
620
 
621
+ /** `sha256:abc` and `abc` name the same digest; anything else compares exactly. */
622
+ function sameDigest(a: string, b: string): boolean {
623
+ const bare = (d: string) => d.replace(/^sha256:/, "");
624
+ return a === b || bare(a) === bare(b);
625
+ }
626
+
627
+ /** Full shas compare exactly; an abbreviated one (7+ chars) matches by prefix. */
628
+ function sameGitSha(a: string, b: string): boolean {
629
+ if (a === b) return true;
630
+ const [short, long] = a.length <= b.length ? [a, b] : [b, a];
631
+ return short.length >= 7 && long.startsWith(short);
632
+ }
633
+
634
+ function notBefore(since: string, recordedAt: string): boolean {
635
+ const s = Date.parse(since);
636
+ const r = Date.parse(recordedAt);
637
+ // An unparseable timestamp cannot place the change after the release, so
638
+ // it is not attributed to drift since it.
639
+ return !Number.isNaN(s) && !Number.isNaN(r) && s >= r;
640
+ }
641
+
642
+ /**
643
+ * Does live evidence for a present, recorded component contradict its latest
644
+ * release record (#2513)? Returns why, or `undefined` when nothing seen says
645
+ * so. Every rule reads a positive observation; a missing identity, an
646
+ * `unknown` ownership or a resource nobody read is not a contradiction.
647
+ */
648
+ function contradictsRecord(recorded: ReleaseRecord, evidence: LiveComponentEvidence): string | undefined {
649
+ for (const id of evidence.identities ?? []) {
650
+ if (id.digest !== undefined) {
651
+ const matches =
652
+ sameDigest(id.digest, recorded.digest) ||
653
+ (recorded.inputDigest !== undefined && sameDigest(id.digest, recorded.inputDigest));
654
+ if (!matches) return `live ${id.entity} reports digest ${id.digest}`;
655
+ }
656
+ if (id.gitSha !== undefined && !sameGitSha(id.gitSha, recorded.gitSha)) {
657
+ return `live ${id.entity} reports git sha ${id.gitSha}, recorded ${recorded.gitSha}`;
658
+ }
659
+ }
660
+
661
+ if (evidence.driftSignals) {
662
+ // A signal against a snapshot taken before the release may be the release
663
+ // itself, so only a change seen since the release counts. A signal with no
664
+ // `since` was produced by a caller that did not say, and counts.
665
+ const since = evidence.driftSignals.filter((d) => d.since === undefined || notBefore(d.since, recorded.timestamp));
666
+ const first = since[0];
667
+ if (first) {
668
+ const what = first.kind === "ownership" ? first.detail ?? "ownership marker changed" : `live configuration of ${first.entity} changed${first.detail ? ` (${first.detail})` : ""}`;
669
+ const more = since.length > 1 ? ` (+${since.length - 1} more)` : "";
670
+ return `${what} since the snapshot of ${first.since ?? "an earlier read"}${more}`;
671
+ }
672
+ return undefined;
673
+ }
674
+
675
+ // Evidence assembled without per-entity observations: the change set's own
676
+ // `update` verdict is the only drift signal there is.
677
+ if (evidence.action === "update") return "live configuration has drifted since";
678
+ return undefined;
679
+ }
680
+
470
681
  /**
471
682
  * Answer "which build is in `envA`, and is it the one tested in `envB`" —
472
683
  * the single-query comparison the epic names explicitly — for one component
@@ -491,6 +702,13 @@ export interface CrossEnvComparison {
491
702
  same: boolean;
492
703
  /** The identities `same` was decided on (`inputDigest ?? digest` per side), when both sides had one and they differ from the digests shown. */
493
704
  comparedOn?: { a: string; b: string };
705
+ /**
706
+ * Set when the two identities differ and exactly one of them is a
707
+ * `legacy-digest` value from before real SHA-256 (#2514). The same content
708
+ * hashes differently in the two forms, so `same: false` then says nothing
709
+ * about whether the builds match.
710
+ */
711
+ mixedDigestForms?: true;
494
712
  }
495
713
 
496
714
  /** Compare the latest recorded digest for `component` between two environments' release records. */
@@ -512,5 +730,8 @@ export function compareAcrossEnvironments(
512
730
  digestB: latestB?.digest,
513
731
  same: !!idA && !!idB && idA === idB,
514
732
  ...(comparedOnInputs && idA && idB ? { comparedOn: { a: idA, b: idB } } : {}),
733
+ ...(idA && idB && idA !== idB && isLegacyContentDigest(idA) !== isLegacyContentDigest(idB)
734
+ ? { mixedDigestForms: true as const }
735
+ : {}),
515
736
  };
516
737
  }
@@ -24,9 +24,8 @@
24
24
  * component.schema.json), a component may attach a `noRollback: "<reason>"`
25
25
  * string property directly on the step to declare the compensation gap is
26
26
  * intentional and understood, or supply its own explicit compensation via a
27
- * component-level `rollback` field (as the ALB/ECS pilot does with
28
- * `rollback-previous`) or a sibling `rollback-previous`/`snapshot-before` step
29
- * in the same phase.
27
+ * component-level `rollback` field or a sibling
28
+ * `rollback-previous`/`snapshot-before` step in the same phase.
30
29
  *
31
30
  * Triggers on: a bare `{ kind: "run-migration", ... }` step with no
32
31
  * `noRollback` reason, no component-level `rollback`, and no sibling
@@ -129,3 +129,40 @@ export const behaviourFindingContract = activityContract(
129
129
  mergeRequest: z.string().optional(),
130
130
  }),
131
131
  );
132
+
133
+ /**
134
+ * The propose-only template upgrade (#2550, ws-032). `args` mirrors
135
+ * `ProposeWorkspaceUpgradeArgs` without its test hooks. `returns` names what
136
+ * a step reads: whether anything changed, whether it was proposed, the patch
137
+ * digest the command's gate would bind, and the branch and pull request.
138
+ */
139
+ export const proposeWorkspaceUpgradeContract = activityContract(
140
+ "proposeWorkspaceUpgrade",
141
+ z.strictObject({
142
+ scope: z.string().optional(),
143
+ to: z.string().optional(),
144
+ mode: z.enum(["report", "branch", "pull-request"]).optional(),
145
+ branch: z.string().optional(),
146
+ base: z.string().optional(),
147
+ remote: z.string().optional(),
148
+ allowCode: z.boolean().optional(),
149
+ cwd: z.string().optional(),
150
+ }),
151
+ z.object({
152
+ scope: z.string(),
153
+ mode: z.enum(["report", "branch", "pull-request"]),
154
+ changed: z.boolean(),
155
+ proposed: z.boolean(),
156
+ checksOk: z.boolean(),
157
+ from: z.string().nullable(),
158
+ to: z.string().nullable(),
159
+ digest: z.string(),
160
+ manualSteps: z.number(),
161
+ governance: z.boolean(),
162
+ branch: z.string().optional(),
163
+ commit: z.string().optional(),
164
+ pushed: z.boolean().optional(),
165
+ prUrl: z.string().optional(),
166
+ summary: z.string(),
167
+ }),
168
+ );
@@ -147,3 +147,13 @@ export type {
147
147
  BehaviourFindingDeps,
148
148
  BaseCheckout,
149
149
  } from "./predict-behaviour";
150
+
151
+ // The propose-only template upgrade (#2550). Loads the workspace code on first
152
+ // call only, so importing this module loads nothing under `workspace/`.
153
+ export { proposeWorkspaceUpgrade } from "./propose-upgrade";
154
+ export type {
155
+ ProposeWorkspaceUpgradeArgs,
156
+ ProposeWorkspaceUpgradeResult,
157
+ ProposeUpgradeMode,
158
+ CommandRunner,
159
+ } from "./propose-upgrade";
@@ -0,0 +1,206 @@
1
+ /**
2
+ * proposeWorkspaceUpgrade — the propose-only activity behind scheduled
3
+ * template upgrades (#2550, D9, ws-032).
4
+ *
5
+ * `chant workspace upgrade` is a core command. An Op, including a workspace Op
6
+ * later (#2554), schedules it through this activity, which stages the same
7
+ * upgrade (fetch, migrate, merge per file, build, lint and `workspace check`
8
+ * in a worktree) and then only proposes it:
9
+ *
10
+ * - `report` (default): stage and check, and return the summary.
11
+ * - `branch`: commit the patch on a proposal branch, by default
12
+ * `chant/upgrade/<scope>`, and push it when the repository has the remote.
13
+ * - `pull-request`: the branch, plus an open pull request against the default
14
+ * branch, created once and edited on later runs.
15
+ *
16
+ * It never writes the default branch or the checked-out branch, and it never
17
+ * applies the patch to the working tree, so an Op cannot rewrite itself or
18
+ * the gates that judge it. Review and merge of the pull request are the
19
+ * approval. The PR body names the patch digest, the value `chant workspace
20
+ * upgrade` binds its gate to, so the two paths approve the same thing.
21
+ *
22
+ * The workspace code loads on first call, so importing core's activities
23
+ * loads nothing under `workspace/` (#2525 rule 5).
24
+ */
25
+
26
+ import { execFile } from "node:child_process";
27
+ import { promisify } from "node:util";
28
+ import type { StagedUpgrade } from "../../workspace/lineage-upgrade";
29
+
30
+ const execFileAsync = promisify(execFile);
31
+
32
+ export type ProposeUpgradeMode = "report" | "branch" | "pull-request";
33
+
34
+ export interface ProposeWorkspaceUpgradeArgs {
35
+ /** The lineage scope: `"."` (default) or a vendor scope's directory. */
36
+ scope?: string;
37
+ /** The target ref. Defaults to the ref the scope is pinned at, so a moving ref such as a branch is followed. */
38
+ to?: string;
39
+ mode?: ProposeUpgradeMode;
40
+ /** The proposal branch. Default `chant/upgrade/<scope>`, with `root` for `"."`. */
41
+ branch?: string;
42
+ /** The branch the pull request targets. Default: the remote's default branch. */
43
+ base?: string;
44
+ /** The remote to push to. Default `origin`. */
45
+ remote?: string;
46
+ /** Run code migrations. Default false. */
47
+ allowCode?: boolean;
48
+ /** The directory holding the lineage lock. Default: the working directory. */
49
+ cwd?: string;
50
+ /** Replaces the git and gh child processes. For tests. */
51
+ _run?: CommandRunner;
52
+ /** Replaces build and lint in the worktree. For tests. */
53
+ _runChant?: import("../../workspace/lineage-upgrade").ChantRunner;
54
+ }
55
+
56
+ /** Runs `git` or `gh` with arguments, returning stdout. Rejects on a non-zero exit. */
57
+ export type CommandRunner = (bin: "git" | "gh", args: string[], cwd: string) => Promise<string>;
58
+
59
+ export interface ProposeWorkspaceUpgradeResult {
60
+ scope: string;
61
+ mode: ProposeUpgradeMode;
62
+ /** Whether the template has anything new for the scope. */
63
+ changed: boolean;
64
+ /** Whether a branch or pull request was written. */
65
+ proposed: boolean;
66
+ checksOk: boolean;
67
+ from: string | null;
68
+ to: string | null;
69
+ /** The patch digest `chant workspace upgrade` binds its gate to. */
70
+ digest: string;
71
+ manualSteps: number;
72
+ governance: boolean;
73
+ branch?: string;
74
+ commit?: string;
75
+ pushed?: boolean;
76
+ prUrl?: string;
77
+ summary: string;
78
+ }
79
+
80
+ const defaultRun: CommandRunner = async (bin, args, cwd) => (await execFileAsync(bin, args, { cwd, maxBuffer: 64 * 1024 * 1024 })).stdout;
81
+
82
+ /** The proposal branch for a scope. */
83
+ export function proposalBranch(scope: string): string {
84
+ return `chant/upgrade/${scope === "." ? "root" : scope.replace(/[^A-Za-z0-9._/-]/g, "-")}`;
85
+ }
86
+
87
+ /** The marker that finds this scope's pull request again. */
88
+ export function proposalMarker(scope: string): string {
89
+ return `<!-- chant-workspace-upgrade:${scope} -->`;
90
+ }
91
+
92
+ function body(staged: StagedUpgrade, lines: string[]): string {
93
+ return [
94
+ proposalMarker(staged.scope),
95
+ `## Template upgrade: \`${staged.scope}\``,
96
+ "",
97
+ `\`${staged.template}\` from \`${staged.from ?? "(no ref)"}\` to \`${staged.to ?? "(no ref)"}\`.`,
98
+ "",
99
+ `Patch digest: \`${staged.digest}\``,
100
+ "",
101
+ "```",
102
+ ...lines,
103
+ "```",
104
+ "",
105
+ staged.manualSteps.length > 0
106
+ ? `${staged.manualSteps.length} file(s) did not merge cleanly and are kept as they were, each with a manual step. \`chant workspace check\` fails until each is merged and resolved with \`chant workspace lineage resolve <path>\`.`
107
+ : "Every file merged cleanly.",
108
+ staged.governance ? "\nThis upgrade changes governance files. Review it under the rules on the default branch, not the ones it proposes." : "",
109
+ "",
110
+ "Proposed by the `proposeWorkspaceUpgrade` activity. It never writes the default branch.",
111
+ ].join("\n");
112
+ }
113
+
114
+ async function defaultBranch(run: CommandRunner, repo: string, remote: string): Promise<string | null> {
115
+ try {
116
+ const ref = (await run("git", ["symbolic-ref", "--short", `refs/remotes/${remote}/HEAD`], repo)).trim();
117
+ return ref.startsWith(`${remote}/`) ? ref.slice(remote.length + 1) : ref;
118
+ } catch {
119
+ return null;
120
+ }
121
+ }
122
+
123
+ /** Stage a workspace upgrade and propose it as a branch or pull request. Never writes the default branch. */
124
+ export async function proposeWorkspaceUpgrade(args: ProposeWorkspaceUpgradeArgs): Promise<ProposeWorkspaceUpgradeResult> {
125
+ const mode = args.mode ?? "report";
126
+ const run = args._run ?? defaultRun;
127
+ const remote = args.remote ?? "origin";
128
+ const { stageUpgrade, describeStaged, commitStagedUpgrade } = await import("../../workspace/lineage-upgrade");
129
+ const staged = await stageUpgrade({
130
+ root: args.cwd ?? process.cwd(),
131
+ scope: args.scope,
132
+ to: args.to,
133
+ allowCode: args.allowCode,
134
+ ...(args._runChant ? { runChant: args._runChant } : {}),
135
+ });
136
+ try {
137
+ const lines = describeStaged(staged);
138
+ const result: ProposeWorkspaceUpgradeResult = {
139
+ scope: staged.scope,
140
+ mode,
141
+ changed: staged.changed,
142
+ proposed: false,
143
+ checksOk: staged.checksOk,
144
+ from: staged.from,
145
+ to: staged.to,
146
+ digest: staged.digest,
147
+ manualSteps: staged.manualSteps.length,
148
+ governance: staged.governance !== null,
149
+ summary: lines.join("\n"),
150
+ };
151
+ if (!result.changed || !result.checksOk || mode === "report") return result;
152
+
153
+ const branch = args.branch ?? proposalBranch(staged.scope);
154
+ const remoteDefault = await defaultBranch(run, staged.repo, remote);
155
+ const base = args.base ?? remoteDefault;
156
+ let current: string | null = null;
157
+ try {
158
+ current = (await run("git", ["symbolic-ref", "--short", "HEAD"], staged.repo)).trim();
159
+ } catch {
160
+ // Detached HEAD: nothing checked out to protect.
161
+ }
162
+ for (const [what, name] of [["default branch", remoteDefault], ["base branch", base], ["checked-out branch", current]] as const) {
163
+ if (name && branch === name) {
164
+ throw new Error(`proposeWorkspaceUpgrade only proposes: it will not write the ${what} "${name}". Choose another branch.`);
165
+ }
166
+ }
167
+ if (mode === "pull-request" && !base) {
168
+ throw new Error(`proposeWorkspaceUpgrade: cannot tell the default branch of "${remote}"; pass base`);
169
+ }
170
+
171
+ const title = `chore(upgrade): ${staged.template} ${staged.to ?? ""} (${staged.scope})`.replace(/\s+/g, " ").replace(" )", ")");
172
+ result.commit = commitStagedUpgrade(staged, branch, `${title}\n\nPatch digest: ${staged.digest}\n`);
173
+ result.branch = branch;
174
+ result.proposed = true;
175
+
176
+ let hasRemote = false;
177
+ try {
178
+ await run("git", ["remote", "get-url", remote], staged.repo);
179
+ hasRemote = true;
180
+ } catch {
181
+ hasRemote = false;
182
+ }
183
+ if (hasRemote) {
184
+ // The proposal branch is rebuilt from the default branch on every run, so it is replaced.
185
+ await run("git", ["push", "--force", remote, `${result.commit}:refs/heads/${branch}`], staged.repo);
186
+ result.pushed = true;
187
+ } else {
188
+ result.pushed = false;
189
+ }
190
+ if (mode !== "pull-request") return result;
191
+ if (!hasRemote) throw new Error(`proposeWorkspaceUpgrade: no remote "${remote}" to open a pull request on`);
192
+
193
+ const text = body(staged, lines);
194
+ const existing = (await run("gh", ["pr", "list", "--head", branch, "--state", "open", "--json", "url", "--jq", ".[0].url // empty"], staged.repo)).trim();
195
+ if (existing) {
196
+ await run("gh", ["pr", "edit", existing, "--title", title, "--body", text], staged.repo);
197
+ result.prUrl = existing;
198
+ } else {
199
+ const out = await run("gh", ["pr", "create", "--base", base!, "--head", branch, "--title", title, "--body", text], staged.repo);
200
+ result.prUrl = out.trim().split("\n").pop();
201
+ }
202
+ return result;
203
+ } finally {
204
+ staged.dispose();
205
+ }
206
+ }
@@ -36,6 +36,7 @@
36
36
 
37
37
  import * as coreContracts from "./activities/activity-contracts";
38
38
  import { collectActivityContracts, type ActivityContract } from "./activity-contract";
39
+ import { importLexiconModule } from "../lexicon-module";
39
40
 
40
41
  /**
41
42
  * The shape {@link loadActivityContracts} reads off a plugin: an optional
@@ -74,16 +75,29 @@ export async function loadActivityContracts(
74
75
 
75
76
  for (const entry of lexicons) {
76
77
  const name = typeof entry === "string" ? entry : entry.name;
78
+ let contributor: LexiconActivityContractContributor | undefined = typeof entry === "string" ? undefined : entry;
77
79
  try {
78
- const spec = `@intentius/chant-lexicon-${name}/op/activity-contracts`;
79
- collectActivityContracts((await import(spec)) as Record<string, unknown>, contracts);
80
+ // chant #2520 — a lexicon declared by module path has no subpath to
81
+ // import. Its plugin's `activityContracts()` member stands in for it;
82
+ // given only the name, the plugin is read from the declared module.
83
+ const local = await importLexiconModule(name);
84
+ if (local !== undefined) {
85
+ contributor ??= Object.values(local).find(
86
+ (v): v is LexiconActivityContractContributor =>
87
+ typeof v === "object" && v !== null && (v as { name?: unknown }).name === name &&
88
+ typeof (v as { activityContracts?: unknown }).activityContracts === "function",
89
+ );
90
+ } else {
91
+ const spec = `@intentius/chant-lexicon-${name}/op/activity-contracts`;
92
+ collectActivityContracts((await import(spec)) as Record<string, unknown>, contracts);
93
+ }
80
94
  } catch {
81
95
  // Lexicon absent or declares no contracts at the conventional subpath.
82
96
  }
83
97
 
84
- if (typeof entry !== "string" && typeof entry.activityContracts === "function") {
98
+ if (contributor !== undefined && typeof contributor.activityContracts === "function") {
85
99
  try {
86
- for (const contract of entry.activityContracts()) contracts.set(contract.name, contract);
100
+ for (const contract of contributor.activityContracts()) contracts.set(contract.name, contract);
87
101
  } catch {
88
102
  // A plugin member that throws contributes nothing, the same as one that is absent.
89
103
  }