@intentius/chant 0.124.28 → 0.124.29

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 (395) hide show
  1. package/dist/build-params.d.ts +34 -1
  2. package/dist/build-params.d.ts.map +1 -1
  3. package/dist/build-params.js +31 -1
  4. package/dist/change-set-graph.d.ts +8 -1
  5. package/dist/change-set-graph.d.ts.map +1 -1
  6. package/dist/change-set-graph.js +4 -2
  7. package/dist/change-set.d.ts +10 -0
  8. package/dist/change-set.d.ts.map +1 -1
  9. package/dist/change-set.js +4 -3
  10. package/dist/cli/build-params-cli.d.ts +7 -1
  11. package/dist/cli/build-params-cli.d.ts.map +1 -1
  12. package/dist/cli/build-params-cli.js +17 -8
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/build.js +6 -3
  15. package/dist/cli/handlers/approve.d.ts.map +1 -1
  16. package/dist/cli/handlers/approve.js +45 -0
  17. package/dist/cli/handlers/build.d.ts.map +1 -1
  18. package/dist/cli/handlers/build.js +15 -4
  19. package/dist/cli/handlers/fan-out.d.ts +9 -0
  20. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  21. package/dist/cli/handlers/fan-out.js +40 -3
  22. package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
  23. package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
  24. package/dist/cli/handlers/lifecycle-gc.js +143 -0
  25. package/dist/cli/handlers/pr.d.ts +11 -3
  26. package/dist/cli/handlers/pr.d.ts.map +1 -1
  27. package/dist/cli/handlers/pr.js +134 -26
  28. package/dist/cli/handlers/promote.d.ts +7 -0
  29. package/dist/cli/handlers/promote.d.ts.map +1 -1
  30. package/dist/cli/handlers/promote.js +210 -33
  31. package/dist/cli/handlers/release.d.ts +62 -5
  32. package/dist/cli/handlers/release.d.ts.map +1 -1
  33. package/dist/cli/handlers/release.js +414 -28
  34. package/dist/cli/handlers/run.d.ts +11 -1
  35. package/dist/cli/handlers/run.d.ts.map +1 -1
  36. package/dist/cli/handlers/run.js +67 -6
  37. package/dist/cli/main.d.ts.map +1 -1
  38. package/dist/cli/main.js +61 -10
  39. package/dist/cli/parse-args.d.ts.map +1 -1
  40. package/dist/cli/parse-args.js +24 -2
  41. package/dist/cli/registry.d.ts +15 -3
  42. package/dist/cli/registry.d.ts.map +1 -1
  43. package/dist/cli/run-events.d.ts +3 -2
  44. package/dist/cli/run-events.d.ts.map +1 -1
  45. package/dist/cli/run-events.js +5 -3
  46. package/dist/components/auto-release.d.ts +2 -0
  47. package/dist/components/auto-release.d.ts.map +1 -1
  48. package/dist/components/auto-release.js +1 -0
  49. package/dist/components/capability.d.ts +9 -0
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/driver.d.ts +8 -0
  52. package/dist/components/driver.d.ts.map +1 -1
  53. package/dist/components/driver.js +35 -0
  54. package/dist/components/fan-out-inputs.d.ts +69 -0
  55. package/dist/components/fan-out-inputs.d.ts.map +1 -0
  56. package/dist/components/fan-out-inputs.js +169 -0
  57. package/dist/components/fan-out-record.d.ts +13 -0
  58. package/dist/components/fan-out-record.d.ts.map +1 -1
  59. package/dist/components/fan-out-record.js +16 -0
  60. package/dist/components/fan-out-run.d.ts.map +1 -1
  61. package/dist/components/fan-out-run.js +50 -0
  62. package/dist/components/fan-out.d.ts +4 -0
  63. package/dist/components/fan-out.d.ts.map +1 -1
  64. package/dist/components/pr-pipeline.d.ts +25 -0
  65. package/dist/components/pr-pipeline.d.ts.map +1 -1
  66. package/dist/components/pr-pipeline.js +22 -4
  67. package/dist/components/promote.d.ts +23 -0
  68. package/dist/components/promote.d.ts.map +1 -1
  69. package/dist/components/promote.js +60 -2
  70. package/dist/components/release-compile.d.ts +9 -0
  71. package/dist/components/release-compile.d.ts.map +1 -1
  72. package/dist/components/release-compile.js +9 -3
  73. package/dist/components/release-gate.d.ts +11 -1
  74. package/dist/components/release-gate.d.ts.map +1 -1
  75. package/dist/components/release-gate.js +10 -4
  76. package/dist/components/wave-plan.d.ts +5 -0
  77. package/dist/components/wave-plan.d.ts.map +1 -1
  78. package/dist/components/wave-plan.js +10 -2
  79. package/dist/config.d.ts +38 -0
  80. package/dist/config.d.ts.map +1 -1
  81. package/dist/config.js +2 -0
  82. package/dist/flag-params.d.ts +54 -0
  83. package/dist/flag-params.d.ts.map +1 -0
  84. package/dist/flag-params.js +130 -0
  85. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
  86. package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
  87. package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
  88. package/dist/lifecycle/change-set-store.d.ts +2 -1
  89. package/dist/lifecycle/change-set-store.d.ts.map +1 -1
  90. package/dist/lifecycle/change-set-store.js +2 -1
  91. package/dist/lifecycle/delivery-metrics.d.ts +139 -0
  92. package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
  93. package/dist/lifecycle/delivery-metrics.js +145 -0
  94. package/dist/lifecycle/deploy-outcome.d.ts +81 -0
  95. package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
  96. package/dist/lifecycle/deploy-outcome.js +101 -0
  97. package/dist/lifecycle/gate-change-set.d.ts +10 -1
  98. package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
  99. package/dist/lifecycle/gate-change-set.js +35 -3
  100. package/dist/lifecycle/gate-ledger.d.ts +2 -3
  101. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  102. package/dist/lifecycle/git.d.ts +30 -1
  103. package/dist/lifecycle/git.d.ts.map +1 -1
  104. package/dist/lifecycle/git.js +31 -5
  105. package/dist/lifecycle/index.d.ts +4 -0
  106. package/dist/lifecycle/index.d.ts.map +1 -1
  107. package/dist/lifecycle/index.js +4 -0
  108. package/dist/lifecycle/mixed-environment.d.ts +95 -0
  109. package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
  110. package/dist/lifecycle/mixed-environment.js +154 -0
  111. package/dist/lifecycle/publish-attachments.d.ts +79 -0
  112. package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
  113. package/dist/lifecycle/publish-attachments.js +179 -0
  114. package/dist/lifecycle/publish-credentials.d.ts +37 -0
  115. package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
  116. package/dist/lifecycle/publish-credentials.js +69 -0
  117. package/dist/lifecycle/publish-declaration.d.ts +97 -0
  118. package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
  119. package/dist/lifecycle/publish-declaration.js +83 -0
  120. package/dist/lifecycle/publish-forge.d.ts +53 -0
  121. package/dist/lifecycle/publish-forge.d.ts.map +1 -0
  122. package/dist/lifecycle/publish-forge.js +298 -0
  123. package/dist/lifecycle/publish-npm.d.ts +61 -0
  124. package/dist/lifecycle/publish-npm.d.ts.map +1 -0
  125. package/dist/lifecycle/publish-npm.js +242 -0
  126. package/dist/lifecycle/publish-oci.d.ts +64 -0
  127. package/dist/lifecycle/publish-oci.d.ts.map +1 -0
  128. package/dist/lifecycle/publish-oci.js +383 -0
  129. package/dist/lifecycle/publish-record.d.ts +66 -0
  130. package/dist/lifecycle/publish-record.d.ts.map +1 -0
  131. package/dist/lifecycle/publish-record.js +84 -0
  132. package/dist/lifecycle/publish-targets.d.ts +45 -0
  133. package/dist/lifecycle/publish-targets.d.ts.map +1 -0
  134. package/dist/lifecycle/publish-targets.js +85 -0
  135. package/dist/lifecycle/release-cut.d.ts +48 -1
  136. package/dist/lifecycle/release-cut.d.ts.map +1 -1
  137. package/dist/lifecycle/release-cut.js +116 -23
  138. package/dist/lifecycle/release-join.d.ts +95 -0
  139. package/dist/lifecycle/release-join.d.ts.map +1 -0
  140. package/dist/lifecycle/release-join.js +139 -0
  141. package/dist/lifecycle/release-publish.d.ts +312 -0
  142. package/dist/lifecycle/release-publish.d.ts.map +1 -0
  143. package/dist/lifecycle/release-publish.js +335 -0
  144. package/dist/lifecycle/release-revocation.d.ts +80 -0
  145. package/dist/lifecycle/release-revocation.d.ts.map +1 -0
  146. package/dist/lifecycle/release-revocation.js +123 -0
  147. package/dist/lifecycle/release-store.d.ts +12 -2
  148. package/dist/lifecycle/release-store.d.ts.map +1 -1
  149. package/dist/lifecycle/release-store.js +4 -2
  150. package/dist/lifecycle/release-verify.d.ts +61 -0
  151. package/dist/lifecycle/release-verify.d.ts.map +1 -0
  152. package/dist/lifecycle/release-verify.js +92 -0
  153. package/dist/lifecycle/release-workstation.d.ts +91 -0
  154. package/dist/lifecycle/release-workstation.d.ts.map +1 -0
  155. package/dist/lifecycle/release-workstation.js +180 -0
  156. package/dist/lifecycle/retention-policy.d.ts +45 -0
  157. package/dist/lifecycle/retention-policy.d.ts.map +1 -0
  158. package/dist/lifecycle/retention-policy.js +43 -0
  159. package/dist/lifecycle/retention.d.ts +185 -0
  160. package/dist/lifecycle/retention.d.ts.map +1 -0
  161. package/dist/lifecycle/retention.js +400 -0
  162. package/dist/plan-summary.d.ts.map +1 -1
  163. package/dist/plan-summary.js +8 -2
  164. package/dist/pr-loop.d.ts +7 -0
  165. package/dist/pr-loop.d.ts.map +1 -1
  166. package/dist/pr-loop.js +23 -4
  167. package/dist/pr-report-url.d.ts +82 -0
  168. package/dist/pr-report-url.d.ts.map +1 -0
  169. package/dist/pr-report-url.js +174 -0
  170. package/dist/provenance.d.ts +2 -2
  171. package/dist/provenance.d.ts.map +1 -1
  172. package/dist/run-event-store.d.ts +29 -2
  173. package/dist/run-event-store.d.ts.map +1 -1
  174. package/dist/run-event-store.js +50 -3
  175. package/dist/run-event-stream.d.ts +2 -1
  176. package/dist/run-event-stream.d.ts.map +1 -1
  177. package/dist/run-event-stream.js +5 -1
  178. package/dist/run-event.d.ts +19 -1
  179. package/dist/run-event.d.ts.map +1 -1
  180. package/dist/run-event.js +2 -0
  181. package/dist/wave-runtime.mjs +260 -229
  182. package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
  183. package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
  184. package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
  185. package/dist/workspace/change-set.schema.json +5 -1
  186. package/dist/workspace/check.schema.json +4 -2
  187. package/dist/workspace/checks/records.d.ts +9 -1
  188. package/dist/workspace/checks/records.d.ts.map +1 -1
  189. package/dist/workspace/checks/records.js +35 -1
  190. package/dist/workspace/checks.d.ts +1 -0
  191. package/dist/workspace/checks.d.ts.map +1 -1
  192. package/dist/workspace/checks.js +2 -1
  193. package/dist/workspace/ci-green.d.ts +8 -0
  194. package/dist/workspace/ci-green.d.ts.map +1 -1
  195. package/dist/workspace/ci-green.js +10 -3
  196. package/dist/workspace/declaration.d.ts +16 -0
  197. package/dist/workspace/declaration.d.ts.map +1 -1
  198. package/dist/workspace/declaration.js +26 -0
  199. package/dist/workspace/declaration.schema.json +64 -0
  200. package/dist/workspace/declared-kinds.d.ts +16 -0
  201. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  202. package/dist/workspace/declared-kinds.js +25 -0
  203. package/dist/workspace/delivery-metrics.d.ts +91 -0
  204. package/dist/workspace/delivery-metrics.d.ts.map +1 -0
  205. package/dist/workspace/delivery-metrics.js +217 -0
  206. package/dist/workspace/delivery-metrics.schema.json +519 -0
  207. package/dist/workspace/flags-read.d.ts +37 -0
  208. package/dist/workspace/flags-read.d.ts.map +1 -0
  209. package/dist/workspace/flags-read.js +103 -0
  210. package/dist/workspace/flags.d.ts +100 -0
  211. package/dist/workspace/flags.d.ts.map +1 -0
  212. package/dist/workspace/flags.js +98 -0
  213. package/dist/workspace/graph-cli.js +1 -1
  214. package/dist/workspace/intent-cli.d.ts +11 -2
  215. package/dist/workspace/intent-cli.d.ts.map +1 -1
  216. package/dist/workspace/intent-cli.js +54 -5
  217. package/dist/workspace/intent-trace.d.ts +219 -0
  218. package/dist/workspace/intent-trace.d.ts.map +1 -0
  219. package/dist/workspace/intent-trace.js +314 -0
  220. package/dist/workspace/intent.d.ts +30 -6
  221. package/dist/workspace/intent.d.ts.map +1 -1
  222. package/dist/workspace/intent.js +141 -21
  223. package/dist/workspace/intent.schema.json +702 -6
  224. package/dist/workspace/overlap.d.ts +146 -0
  225. package/dist/workspace/overlap.d.ts.map +1 -0
  226. package/dist/workspace/overlap.js +276 -0
  227. package/dist/workspace/reason-codes.d.ts +2 -0
  228. package/dist/workspace/reason-codes.d.ts.map +1 -1
  229. package/dist/workspace/reason-codes.js +3 -0
  230. package/dist/workspace/records.d.ts +15 -2
  231. package/dist/workspace/records.d.ts.map +1 -1
  232. package/dist/workspace/records.js +32 -0
  233. package/dist/workspace/records.schema.json +21 -2
  234. package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  235. package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
  236. package/dist/workspace/releases.d.ts +9 -0
  237. package/dist/workspace/releases.d.ts.map +1 -1
  238. package/dist/workspace/releases.js +9 -0
  239. package/dist/workspace/releases.schema.json +123 -1
  240. package/dist/workspace/run-event.schema.json +10 -3
  241. package/dist/workspace/status-gates.d.ts +8 -0
  242. package/dist/workspace/status-gates.d.ts.map +1 -1
  243. package/dist/workspace/status.d.ts +78 -0
  244. package/dist/workspace/status.d.ts.map +1 -1
  245. package/dist/workspace/status.js +170 -3
  246. package/dist/workspace/status.schema.json +198 -1
  247. package/dist/workspace/wip.d.ts.map +1 -1
  248. package/dist/workspace/wip.js +11 -3
  249. package/package.json +1 -1
  250. package/src/build-params.test.ts +46 -0
  251. package/src/build-params.ts +55 -1
  252. package/src/change-set-graph.ts +10 -2
  253. package/src/change-set.ts +14 -3
  254. package/src/cli/build-params-cli.ts +22 -9
  255. package/src/cli/commands/build.ts +6 -3
  256. package/src/cli/handlers/approve-release.test.ts +5 -1
  257. package/src/cli/handlers/approve-revoked.test.ts +155 -0
  258. package/src/cli/handlers/approve.ts +47 -0
  259. package/src/cli/handlers/build.test.ts +15 -0
  260. package/src/cli/handlers/build.ts +17 -4
  261. package/src/cli/handlers/fan-out.test.ts +180 -1
  262. package/src/cli/handlers/fan-out.ts +38 -3
  263. package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
  264. package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
  265. package/src/cli/handlers/lifecycle-gc.ts +146 -0
  266. package/src/cli/handlers/pr.test.ts +148 -1
  267. package/src/cli/handlers/pr.ts +139 -28
  268. package/src/cli/handlers/promote.test.ts +280 -2
  269. package/src/cli/handlers/promote.ts +210 -34
  270. package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
  271. package/src/cli/handlers/release-join.e2e.test.ts +185 -0
  272. package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
  273. package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
  274. package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
  275. package/src/cli/handlers/release.e2e.test.ts +146 -32
  276. package/src/cli/handlers/release.ts +415 -28
  277. package/src/cli/handlers/run-auto-release.test.ts +9 -0
  278. package/src/cli/handlers/run-release.test.ts +55 -0
  279. package/src/cli/handlers/run.ts +69 -5
  280. package/src/cli/main.test.ts +1 -0
  281. package/src/cli/main.ts +61 -10
  282. package/src/cli/parse-args.ts +19 -2
  283. package/src/cli/registry.ts +15 -3
  284. package/src/cli/run-events.ts +5 -2
  285. package/src/components/auto-release.test.ts +9 -0
  286. package/src/components/auto-release.ts +3 -0
  287. package/src/components/capability.ts +9 -0
  288. package/src/components/driver.test.ts +24 -0
  289. package/src/components/driver.ts +36 -0
  290. package/src/components/fan-out-inputs.test.ts +85 -0
  291. package/src/components/fan-out-inputs.ts +192 -0
  292. package/src/components/fan-out-record.test.ts +10 -0
  293. package/src/components/fan-out-record.ts +19 -0
  294. package/src/components/fan-out-run.ts +48 -0
  295. package/src/components/fan-out.ts +4 -0
  296. package/src/components/pr-pipeline.ts +35 -4
  297. package/src/components/promote.ts +68 -2
  298. package/src/components/release-compile.ts +17 -3
  299. package/src/components/release-gate.ts +18 -4
  300. package/src/components/wave-plan.ts +15 -2
  301. package/src/config.ts +8 -0
  302. package/src/flag-params.test.ts +144 -0
  303. package/src/flag-params.ts +134 -0
  304. package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
  305. package/src/lifecycle/change-set-store.ts +2 -1
  306. package/src/lifecycle/delivery-metrics.ts +263 -0
  307. package/src/lifecycle/deploy-outcome.ts +147 -0
  308. package/src/lifecycle/gate-change-set.ts +37 -6
  309. package/src/lifecycle/gate-ledger.ts +2 -2
  310. package/src/lifecycle/git.ts +61 -8
  311. package/src/lifecycle/index.ts +4 -0
  312. package/src/lifecycle/mixed-environment.test.ts +97 -0
  313. package/src/lifecycle/mixed-environment.ts +204 -0
  314. package/src/lifecycle/publish-attachments.ts +196 -0
  315. package/src/lifecycle/publish-credentials.ts +84 -0
  316. package/src/lifecycle/publish-declaration.ts +95 -0
  317. package/src/lifecycle/publish-forge.ts +325 -0
  318. package/src/lifecycle/publish-npm.ts +259 -0
  319. package/src/lifecycle/publish-oci.ts +396 -0
  320. package/src/lifecycle/publish-record.ts +113 -0
  321. package/src/lifecycle/publish-resume.test.ts +168 -0
  322. package/src/lifecycle/publish-targets.test.ts +494 -0
  323. package/src/lifecycle/publish-targets.ts +100 -0
  324. package/src/lifecycle/release-cut.test.ts +74 -2
  325. package/src/lifecycle/release-cut.ts +158 -22
  326. package/src/lifecycle/release-flag.test.ts +112 -0
  327. package/src/lifecycle/release-join.test.ts +68 -0
  328. package/src/lifecycle/release-join.ts +182 -0
  329. package/src/lifecycle/release-publish.test.ts +225 -0
  330. package/src/lifecycle/release-publish.ts +568 -0
  331. package/src/lifecycle/release-revocation.ts +147 -0
  332. package/src/lifecycle/release-store.ts +15 -4
  333. package/src/lifecycle/release-verify.test.ts +110 -0
  334. package/src/lifecycle/release-verify.ts +120 -0
  335. package/src/lifecycle/release-workstation.test.ts +204 -0
  336. package/src/lifecycle/release-workstation.ts +216 -0
  337. package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
  338. package/src/lifecycle/retention-policy.ts +63 -0
  339. package/src/lifecycle/retention.test.ts +363 -0
  340. package/src/lifecycle/retention.ts +508 -0
  341. package/src/plan-summary.test.ts +20 -0
  342. package/src/plan-summary.ts +8 -1
  343. package/src/pr-loop.ts +25 -4
  344. package/src/pr-report-url.test.ts +131 -0
  345. package/src/pr-report-url.ts +209 -0
  346. package/src/provenance.ts +2 -2
  347. package/src/run-event-store.ts +63 -3
  348. package/src/run-event-stream.ts +4 -1
  349. package/src/run-event.ts +21 -1
  350. package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
  351. package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
  352. package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
  353. package/src/workspace/change-set.schema.json +5 -1
  354. package/src/workspace/check.schema.json +4 -2
  355. package/src/workspace/checks/records.ts +35 -1
  356. package/src/workspace/checks.ts +2 -1
  357. package/src/workspace/ci-green.ts +14 -3
  358. package/src/workspace/declaration.schema.json +64 -0
  359. package/src/workspace/declaration.test.ts +28 -0
  360. package/src/workspace/declaration.ts +38 -0
  361. package/src/workspace/declared-kinds.ts +40 -1
  362. package/src/workspace/delivery-metrics.schema.json +519 -0
  363. package/src/workspace/delivery-metrics.test.ts +236 -0
  364. package/src/workspace/delivery-metrics.ts +286 -0
  365. package/src/workspace/flags-read.ts +121 -0
  366. package/src/workspace/flags.test.ts +141 -0
  367. package/src/workspace/flags.ts +157 -0
  368. package/src/workspace/graph-cli.ts +1 -1
  369. package/src/workspace/intent-cli.ts +46 -5
  370. package/src/workspace/intent-trace.test.ts +173 -0
  371. package/src/workspace/intent-trace.ts +484 -0
  372. package/src/workspace/intent.schema.json +702 -6
  373. package/src/workspace/intent.ts +148 -25
  374. package/src/workspace/older-records.test.ts +412 -0
  375. package/src/workspace/overlap.test.ts +165 -0
  376. package/src/workspace/overlap.ts +340 -0
  377. package/src/workspace/read-contract.e2e.test.ts +2 -1
  378. package/src/workspace/reason-codes.test.ts +5 -0
  379. package/src/workspace/reason-codes.ts +3 -0
  380. package/src/workspace/record-assets.test.ts +2 -1
  381. package/src/workspace/records-contract.e2e.test.ts +2 -1
  382. package/src/workspace/records.schema.json +21 -2
  383. package/src/workspace/records.ts +36 -2
  384. package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
  385. package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
  386. package/src/workspace/releases.schema.json +123 -1
  387. package/src/workspace/releases.ts +11 -0
  388. package/src/workspace/run-event.schema.json +10 -3
  389. package/src/workspace/status-contract.test.ts +29 -0
  390. package/src/workspace/status-gates.ts +8 -0
  391. package/src/workspace/status-mixed.test.ts +152 -0
  392. package/src/workspace/status-revoked.test.ts +153 -0
  393. package/src/workspace/status.schema.json +198 -1
  394. package/src/workspace/status.ts +254 -3
  395. package/src/workspace/wip.ts +11 -2
@@ -39,7 +39,10 @@ import type { CommandContext } from "../cli/registry";
39
39
  import { GATES_DIR } from "../lifecycle/gate-ledger";
40
40
  import { readPathSha } from "../lifecycle/git";
41
41
  import { latestPerComponent, readReleaseLedger, type ReleaseRecord } from "../lifecycle/release-ledger";
42
+ import { describeRevocation, revokedCommitsAt, revokedReleaseLookup, type ReleaseRevocation, type RevokedRelease } from "../lifecycle/release-revocation";
42
43
  import { readReleasePlan, type ReleasePlan } from "../lifecycle/plan-ledger";
44
+ import { readDeployOutcomes, type DeployOutcome } from "../lifecycle/deploy-outcome";
45
+ import { mixedEnvironment, releaseRollbackLine, rollbackTarget, type MixedEnvironment, type ReleaseCheck } from "../lifecycle/mixed-environment";
43
46
  import { listWorkLeases, type WorkLeaseState } from "../lifecycle/work-lease";
44
47
  import { readCheckoutHead, worktreeChanges, type CheckoutHead } from "./records-checkout";
45
48
  import { policyView, replicationState, type ReplicationState } from "./wip";
@@ -50,6 +53,9 @@ import { loadKindRegistry, resolveMemberFields, type MemberFields } from "./kind
50
53
  import { factoryView, listingView, plantability, treeBytes, type FactoryView, type ListingView, type Plantable } from "./box-factory";
51
54
  import { declaredRecordKinds, extensionsAt, readDeclaration, readerVersion, WorkspaceReadError, type Declaration, type ErrorLocation, type Member } from "./declaration";
52
55
  import type { ReasonCode } from "./reason-codes";
56
+ import type { FlagDecision, FlagValue } from "./flags";
57
+ import type { OverlapChangeSetRef, OverlapReport } from "./overlap";
58
+ import type { WorkLink } from "./work";
53
59
  import { readMemberPromotionOrder, type PromotionStep } from "./promotion-order";
54
60
  import { gateAdmissionFrom, releaseRuleFrom } from "./identity";
55
61
  import { checkReleaseSignature, type GateAdmissionRule, type ReleaseSignatureCheck } from "./identity-gate-rule";
@@ -126,6 +132,10 @@ export interface StatusRelease {
126
132
  approver: string | null;
127
133
  /** The gate resolution that admitted the release (#4020): the gate and when it was resolved. Null for an ungated release and for one recorded before chant 0.124.28. */
128
134
  admission: { op: string; gate: string; resolvedAt: string } | null;
135
+ /** The release this deploy was made from (#4043): the `releaseDigest` of `_releases/<releaseDigest>.json`. Null for a deploy that named no release and for one recorded before chant 0.124.29. */
136
+ releaseDigest: string | null;
137
+ /** The label that release was cut with (#4043), or null when it has none, when no release is named, or when the cut can't be read. Chant 0.124.29 and newer write it. */
138
+ releaseLabel: string | null;
129
139
  /** Read flags, such as `legacy-digest` (#2514). */
130
140
  flags: string[];
131
141
  /**
@@ -137,6 +147,25 @@ export interface StatusRelease {
137
147
  * hasn't fetched yet.
138
148
  */
139
149
  plan: ReleasePlan | null;
150
+ /**
151
+ * Set when the deploy names a release (`releaseDigest` on its record) that
152
+ * was cut from a revoked commit (#4033, ws-105): the revoked tag on that
153
+ * commit, the release and its label, and the line that rolls the component
154
+ * back. Absent otherwise, and from chant older than 0.124.29.
155
+ */
156
+ revoked?: StatusRevokedRelease;
157
+ }
158
+
159
+ /** A release an environment runs that was cut from a revoked commit (#4033). */
160
+ export interface StatusRevokedRelease extends RevokedRelease {
161
+ /**
162
+ * The command that rolls the environment back, run in the member's
163
+ * directory: `chant components rollback <env> --release <label|digest>`
164
+ * naming the newest release the environment ran before this one that is
165
+ * still kept and not revoked (#4035), or `chant components rollback <env>
166
+ * --component <component>` when there is none.
167
+ */
168
+ rollback: string;
140
169
  }
141
170
 
142
171
  export interface StatusLedger {
@@ -154,6 +183,14 @@ export interface StatusEnvironment {
154
183
  /** The latest release of each component in the ledger, sorted by component. */
155
184
  releases: StatusRelease[];
156
185
  reason: { code: StatusReasonCode; message: string } | null;
186
+ /**
187
+ * Set when the environment runs a mix (#4035): a promote, rollback or
188
+ * redeploy left a component failed or gated and nothing has been recorded
189
+ * for it since, or its components' latest records name more than one
190
+ * release. Each component with what it runs, and the lines that settle it.
191
+ * Absent otherwise, and from chant older than 0.124.29.
192
+ */
193
+ mixed?: MixedEnvironment;
157
194
  }
158
195
 
159
196
  export type CompareState = "same" | "differs" | "only-env" | "only-compare";
@@ -207,6 +244,35 @@ export interface StatusMember {
207
244
  * cuts could not be read. Added in contract 1 by chant 0.124.28.
208
245
  */
209
246
  releaseCuts: StatusReleaseCut[] | null;
247
+ /**
248
+ * The member's feature flags (#4024): each record of a declared flag kind
249
+ * that names this member, with its value per environment and the work item
250
+ * it hides. Sorted by id. `[]` when none names it, null when the flag
251
+ * records could not be read. Called `featureFlags` because a release
252
+ * record's `flags` already means something else. Added in contract 1 by
253
+ * chant 0.124.29.
254
+ */
255
+ featureFlags: StatusFeatureFlag[] | null;
256
+ }
257
+
258
+ /** One feature flag of a member (#4024), as `status --json` lists it. */
259
+ export interface StatusFeatureFlag {
260
+ /** The flag's name, its record's id. */
261
+ id: string;
262
+ /** The build parameter of the member the flag sets. */
263
+ param: string;
264
+ owner: string;
265
+ /** The value per environment, as the record writes it. An environment it leaves out builds with the parameter's default. */
266
+ values: Record<string, FlagValue>;
267
+ /** The work item the flag hides, with its state (null when no record has it), or null when the record names none. */
268
+ work: WorkLink | null;
269
+ /** Each decision the work item implements, with its state and whether it is confirmed. */
270
+ implements: FlagDecision[];
271
+ /** True when every decision the work item implements is confirmed: the flag is due for removal (check WSP118). */
272
+ stale: boolean;
273
+ /** The kind file and the record file, from the workspace root. */
274
+ kind: string;
275
+ path: string;
210
276
  }
211
277
 
212
278
  /** One cut of a member's release (#4031), as `status --json` lists it. */
@@ -328,6 +394,14 @@ export type StatusDocument =
328
394
  acceptance: StatusAcceptance[];
329
395
  /** How far each ref the box's replicate policy names is from its remote (#3172), read locally; null when no box declares one. */
330
396
  replication: ReplicationState | null;
397
+ /**
398
+ * Overlap between in-flight work items (#4025): pairs of items whose
399
+ * linked commits touch the same files, or that both reach one entry of
400
+ * a change set a pending or approved gate binds. A report, never a
401
+ * lock. Null when no work kind is declared or the work records or
402
+ * history can't be read. Added in contract 1 by chant 0.124.29.
403
+ */
404
+ overlap: OverlapReport | null;
331
405
  summary: { members: number; released: number; unreadable: number; differing: number | null };
332
406
  }
333
407
  | {
@@ -346,6 +420,8 @@ export interface StatusQuery {
346
420
  compareTo?: string;
347
421
  /** Reads one ledger; the lifecycle reader unless a test swaps it. */
348
422
  readLedger?: LedgerReader;
423
+ /** Reads the deploy outcomes beside one ledger (#4035), given the ledger's path; the lifecycle reader unless a test swaps it. */
424
+ readOutcomes?: OutcomeReader;
349
425
  /** Reads one member's gate ledger directory; git unless a test swaps it. */
350
426
  readGates?: GateLedgerReader;
351
427
  /** The instant a gate's expiry is measured against; now by default. */
@@ -448,7 +524,27 @@ async function shipView(member: Member, cwd: string, read: LedgerReader): Promis
448
524
 
449
525
  const defaultReader: LedgerReader = (path, cwd) => readReleaseLedger(path.slice(0, -"/releases.jsonl".length), { cwd });
450
526
 
451
- async function readEnvironment(member: Member, env: string, cwd: string, read: LedgerReader): Promise<StatusEnvironment> {
527
+ /** Reads the deploy outcomes (#4035) in the directory of the ledger at `path`. */
528
+ type OutcomeReader = (path: string, cwd: string) => Promise<DeployOutcome[]>;
529
+
530
+ const defaultOutcomeReader: OutcomeReader = (path, cwd) => readDeployOutcomes(path.slice(0, -"/releases.jsonl".length), { cwd }).catch(() => []);
531
+
532
+ /** A release digest's revocation, for one member, or null (#4033). */
533
+ type RevokedOf = (releaseDigest: string) => Promise<RevokedRelease | null>;
534
+
535
+ /** The per-component rollback line, for a component an environment runs from a revoked release when no earlier release can be named (#4033). */
536
+ export function rollbackLine(env: string, component: string): string {
537
+ return `chant components rollback ${env} --component ${component}`;
538
+ }
539
+
540
+ async function readEnvironment(
541
+ member: Member,
542
+ env: string,
543
+ cwd: string,
544
+ read: LedgerReader,
545
+ revokedOf: RevokedOf = async () => null,
546
+ readOutcomes: OutcomeReader = defaultOutcomeReader,
547
+ ): Promise<StatusEnvironment> {
452
548
  const ledger = await ledgerFor(member, env, cwd);
453
549
  let records: ReleaseRecord[];
454
550
  let malformed: number;
@@ -470,6 +566,37 @@ async function readEnvironment(member: Member, env: string, cwd: string, read: L
470
566
  // member (see plan-ledger.ts's readReleasePlan doc).
471
567
  const planPrefix = ledger.layout === "members" ? `${MEMBERS_DIR}/${member.name}/` : "";
472
568
  const latest = [...latestPerComponent(records).values()].sort((a, b) => (a.component < b.component ? -1 : a.component > b.component ? 1 : 0));
569
+ // #4043: a release item names the label of the release it was deployed from.
570
+ const memberDir = resolve(cwd, member.dir);
571
+ let labels = new Map<string, string>();
572
+ let labelsRead = false;
573
+ const readLabels = async (): Promise<Map<string, string>> => {
574
+ if (labelsRead) return labels;
575
+ labelsRead = true;
576
+ try {
577
+ const { readCuts, labelsByDigest } = await import("../lifecycle/release-store");
578
+ labels = labelsByDigest((await readCuts({ cwd: memberDir })).cuts);
579
+ } catch {
580
+ // The label is a convenience; the digest is the join key.
581
+ }
582
+ return labels;
583
+ };
584
+ if (latest.some((r) => r.releaseDigest !== undefined)) await readLabels();
585
+ // #4035: whether a release a rollback could go back to is kept and not revoked.
586
+ const checks = new Map<string, Promise<ReleaseCheck>>();
587
+ const check = (digest: string): Promise<ReleaseCheck> => {
588
+ let found = checks.get(digest);
589
+ if (!found) {
590
+ found = (async () => {
591
+ const { readRelease } = await import("../lifecycle/release-store");
592
+ const kept = (await readRelease(digest, { cwd: memberDir }).catch(() => null)) !== null;
593
+ return { kept, revoked: kept && (await revokedOf(digest)) !== null };
594
+ })();
595
+ checks.set(digest, found);
596
+ }
597
+ return found;
598
+ };
599
+ const back = { records, check, label: async (digest: string) => (await readLabels()).get(digest) };
473
600
  const releases: StatusRelease[] = await Promise.all(
474
601
  latest.map(async (r) => ({
475
602
  component: r.component,
@@ -481,10 +608,14 @@ async function readEnvironment(member: Member, env: string, cwd: string, read: L
481
608
  actor: r.actor,
482
609
  approver: r.approver ?? null,
483
610
  admission: r.admission ? { op: r.admission.op, gate: r.admission.gate, resolvedAt: r.admission.resolvedAt } : null,
611
+ releaseDigest: r.releaseDigest ?? null,
612
+ releaseLabel: (r.releaseDigest !== undefined ? labels.get(r.releaseDigest) : undefined) ?? null,
484
613
  flags: [...(r.flags ?? [])],
485
614
  plan: await readReleasePlan(r.digest, { cwd, prefix: planPrefix }),
615
+ ...(await revokedItem(r, env, revokedOf, back)),
486
616
  })),
487
617
  );
618
+ const mixed = await mixedEnvironment({ env, records, outcomes: await readOutcomes(ledger.path, cwd), label: back.label, check });
488
619
  return {
489
620
  env,
490
621
  ledger: { ...ledger, shared: false },
@@ -493,9 +624,47 @@ async function readEnvironment(member: Member, env: string, cwd: string, read: L
493
624
  malformed > 0
494
625
  ? { code: "ledger-malformed", message: `${ledger.path}: ${malformed} line${malformed === 1 ? " is" : "s are"} not a release record and ${malformed === 1 ? "was" : "were"} skipped` }
495
626
  : null,
627
+ ...(mixed ? { mixed } : {}),
496
628
  };
497
629
  }
498
630
 
631
+ /**
632
+ * `{ revoked }` when the record names a release cut from a revoked commit
633
+ * (#4033), else nothing. Its rollback line names the newest release the
634
+ * environment ran before this one that is kept and not revoked (#4035).
635
+ */
636
+ async function revokedItem(
637
+ r: ReleaseRecord,
638
+ env: string,
639
+ revokedOf: RevokedOf,
640
+ back: { records: readonly ReleaseRecord[]; check: (digest: string) => Promise<ReleaseCheck>; label: (digest: string) => Promise<string | undefined> },
641
+ ): Promise<{ revoked?: StatusRevokedRelease }> {
642
+ if (r.releaseDigest === undefined) return {};
643
+ const revoked = await revokedOf(r.releaseDigest);
644
+ if (!revoked) return {};
645
+ const target = await rollbackTarget({ records: back.records, exclude: new Set([r.releaseDigest]), before: r.timestamp, check: back.check });
646
+ const rollback = target ? releaseRollbackLine(env, target, await back.label(target)) : rollbackLine(env, r.component);
647
+ return { revoked: { ...revoked, rollback } };
648
+ }
649
+
650
+ /**
651
+ * A release gate whose release was cut from a revoked commit is superseded
652
+ * (#4033): `chant approve` refuses it and the promote refuses the release, so
653
+ * neither an approval nor a pending fact will pass it. An expired gate stays
654
+ * expired. The gate carries the revocation.
655
+ */
656
+ async function supersedeRevokedGates(gates: StatusGate[], revokedOf: RevokedOf): Promise<StatusGate[]> {
657
+ return Promise.all(
658
+ gates.map(async (g) => {
659
+ if (g.releaseDigest === undefined) return g;
660
+ const revoked = await revokedOf(g.releaseDigest);
661
+ if (!revoked) return g;
662
+ const { releaseDigest: _d, label: _l, ...revocation } = revoked;
663
+ return { ...g, state: g.state === "expired" ? g.state : "superseded", revoked: revocation };
664
+ }),
665
+ );
666
+ }
667
+
499
668
  /** The digest two environments are compared on: the input side when there is one. */
500
669
  const compareKey = (r: StatusRelease): string => r.inputDigest ?? r.digest;
501
670
 
@@ -545,10 +714,17 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
545
714
  const ruleSource = scopeSource(found.dir);
546
715
  const rules = (gate: string) => gateAdmissionFrom(ruleSource, gate);
547
716
  const releaseRule = releaseRuleFrom(ruleSource).rule;
717
+ // #4033 (ws-105): revoked commits, read from local tags once and only when a release is met.
718
+ // Tags that can't be judged read as none revoked; workspace check reports the declaration.
719
+ let revokedCommits: Promise<ReadonlyMap<string, ReleaseRevocation>> | undefined;
720
+ const revoked = () => (revokedCommits ??= revokedCommitsAt(top, ruleSource.declaration?.ci?.green ?? null).catch(() => new Map()));
721
+ // #4024: the flag records, read once for every member. Unreadable records list as null.
722
+ const flags = await readStatusFlags(declaration, found.dir);
548
723
  const members: StatusMember[] = [];
549
724
  for (const m of declaration.members) {
725
+ const revokedOf = revokedReleaseLookup(resolve(found.dir, m.dir), revoked);
550
726
  const environments: StatusEnvironment[] = [];
551
- for (const env of envs) environments.push(await readEnvironment(m, env, found.dir, read));
727
+ for (const env of envs) environments.push(await readEnvironment(m, env, found.dir, read, revokedOf, query.readOutcomes));
552
728
  const own = await hasMemberLedger(m, found.dir);
553
729
  const gates = await readMemberGates(own ? `${MEMBERS_DIR}/${m.name}/${GATES_DIR}` : GATES_DIR, own ? "members" : "flat", commit, envs, found.dir, now, query.readGates, rules);
554
730
  const stewards = await (query.readStewards ?? readMemberStewards)(resolve(found.dir, m.dir), query.env, now, m.kind, m.box);
@@ -565,7 +741,7 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
565
741
  compare: query.compareTo === undefined ? null : compareEnvironments(environments[0], environments[environments.length - 1]),
566
742
  readable: environments.every((e) => e.reason === null),
567
743
  gateLedger: gates.ledger,
568
- gates: gates.gates,
744
+ gates: await supersedeRevokedGates(gates.gates, revokedOf),
569
745
  box:
570
746
  m.box === null
571
747
  ? null
@@ -594,6 +770,7 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
594
770
  stewardReasons: stewards.reasons,
595
771
  promotionOrder: await (query.readPromotionOrder ?? readMemberPromotionOrder)(resolve(found.dir, m.dir)),
596
772
  releaseCuts: await readReleaseCuts(resolve(found.dir, m.dir), releaseRule),
773
+ featureFlags: flags === null ? null : flags.filter((f) => f.member === m.name).map(({ member: _member, ...f }) => f),
597
774
  ...x(m.pointer),
598
775
  });
599
776
  }
@@ -606,6 +783,16 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
606
783
 
607
784
  const leases = await readWorkspaceLeases(declaration.members, found.dir, new Date(now));
608
785
  const acceptance = await readWorkspaceAcceptance(declaration, found.dir);
786
+ // #4025: the change sets still waiting to apply, the ones a pending or approved gate binds.
787
+ const pendingChangeSets: OverlapChangeSetRef[] = [];
788
+ for (const m of members) {
789
+ const own = m.gateLedger.layout === "members";
790
+ for (const g of m.gates) {
791
+ if (g.changeSet === undefined || (g.state !== "pending" && g.state !== "approved")) continue;
792
+ pendingChangeSets.push({ digest: g.changeSet, cwd: resolve(found.dir, m.dir), ledgerMember: own ? m.name : null, gitSha: g.gitSha ?? null });
793
+ }
794
+ }
795
+ const overlap = await readStatusOverlap(declaration, found.dir, top, pendingChangeSets);
609
796
 
610
797
  return {
611
798
  ...head,
@@ -619,6 +806,7 @@ export async function workspaceStatus(query: StatusQuery): Promise<StatusDocumen
619
806
  leases,
620
807
  acceptance,
621
808
  replication: replicationOf(declaration, top),
809
+ overlap,
622
810
  summary: {
623
811
  members: members.length,
624
812
  released: members.filter((m) => m.environments[0].releases.length > 0).length,
@@ -680,6 +868,31 @@ async function readWorkspaceLeases(members: Member[], cwd: string, now: Date): P
680
868
  return out;
681
869
  }
682
870
 
871
+ /** Every flag of the declared flag kinds (#4024), read in the working tree, sorted by id; null when they can't be read. */
872
+ async function readStatusFlags(declaration: Declaration, root: string): Promise<Array<StatusFeatureFlag & { member: string }> | null> {
873
+ if (declaredRecordKinds(declaration).length === 0) return [];
874
+ try {
875
+ const { readWorkspaceFlags } = await import("./flags-read");
876
+ const { flags } = await readWorkspaceFlags(root);
877
+ return flags
878
+ .map((f) => ({ id: f.id, member: f.member, param: f.param, owner: f.owner, values: f.values, work: f.work, implements: f.implements, stale: f.stale, kind: f.kind, path: f.path }))
879
+ .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
880
+ } catch {
881
+ return null;
882
+ }
883
+ }
884
+
885
+ /** The overlap report (#4025); null when no work kind is declared or it can't be read. */
886
+ async function readStatusOverlap(declaration: Declaration, root: string, top: string, changeSets: OverlapChangeSetRef[]): Promise<OverlapReport | null> {
887
+ if (declaredRecordKinds(declaration).length === 0) return null;
888
+ try {
889
+ const { readOverlap } = await import("./overlap");
890
+ return await readOverlap({ declaration, root, top, changeSets });
891
+ } catch {
892
+ return null;
893
+ }
894
+ }
895
+
683
896
  /** The acceptance counts of every declared work kind with criteria (#2772), read in the working tree. */
684
897
  async function readWorkspaceAcceptance(declaration: Declaration, root: string): Promise<StatusAcceptance[]> {
685
898
  if (declaredRecordKinds(declaration).length === 0) return [];
@@ -765,6 +978,22 @@ export function formatStatus(doc: Extract<StatusDocument, { members: unknown }>)
765
978
  for (const e of m.environments) {
766
979
  if (e.reason) memberNotes.push(` ${e.reason.code}: ${e.reason.message}`);
767
980
  else if (e.ledger.shared && e.releases.length > 0) memberNotes.push(` ${e.env} is read from the shared flat ledger ${e.ledger.path}`);
981
+ // #4033: a component running a release cut from a revoked commit, and the way back.
982
+ for (const r of e.releases) {
983
+ if (!r.revoked) continue;
984
+ const name = r.revoked.label ?? r.revoked.releaseDigest;
985
+ memberNotes.push(` ${e.env}: ${r.component} runs release ${name}, revoked by ${r.revoked.tag} (${describeRevocation(r.revoked)}); roll back: ${r.revoked.rollback}`);
986
+ }
987
+ // #4035: an environment running a mix, what each component runs, and the lines that settle it.
988
+ if (e.mixed) {
989
+ const what = e.mixed.components.map((c) => {
990
+ const runs = c.releaseDigest ? `release ${c.releaseLabel ?? c.releaseDigest.slice(0, 24)}` : c.digest ? "no release" : "nothing recorded";
991
+ return c.state === "settled" ? `${c.component} runs ${runs}` : `${c.component} ${c.state} (${c.attempt!.verb} ${c.attempt!.runId}), recorded ${runs}`;
992
+ });
993
+ memberNotes.push(` ${e.env} is mixed: ${what.join("; ")}`);
994
+ for (const line of e.mixed.redeploy) memberNotes.push(` redeploy: ${line}`);
995
+ if (e.mixed.rollback) memberNotes.push(` roll back: ${e.mixed.rollback}`);
996
+ }
768
997
  }
769
998
  if (memberNotes.length > 0) notes.set(rows.length - 1, memberNotes);
770
999
  }
@@ -794,6 +1023,28 @@ export function formatStatus(doc: Extract<StatusDocument, { members: unknown }>)
794
1023
  }
795
1024
  lines.push(...table(rows));
796
1025
  }
1026
+ const flagged = doc.members.filter((m) => (m.featureFlags ?? []).length > 0);
1027
+ if (flagged.length > 0) {
1028
+ lines.push("");
1029
+ const rows: string[][] = [["FLAG", "PARAM", doc.env.toUpperCase(), "WORK ITEM", ""]];
1030
+ for (const m of flagged) {
1031
+ for (const f of m.featureFlags!) {
1032
+ const value = f.values[doc.env];
1033
+ rows.push([`${m.name}/${f.id}`, f.param, value === undefined ? "default" : JSON.stringify(value), f.work ? `${f.work.id} (${f.work.state ?? "unknown"})` : "-", f.stale ? "due for removal" : ""]);
1034
+ }
1035
+ }
1036
+ lines.push(...table(rows));
1037
+ }
1038
+ // #4025: in-flight work items that changed the same thing.
1039
+ if (doc.overlap && doc.overlap.pairs.length > 0) {
1040
+ lines.push("");
1041
+ const rows: string[][] = [["OVERLAP", "FILES", "ADDRESSES"]];
1042
+ for (const p of doc.overlap.pairs) {
1043
+ const list = (xs: string[]) => (xs.length === 0 ? "-" : xs.length <= 2 ? xs.join(", ") : `${xs.slice(0, 2).join(", ")} and ${xs.length - 2} more`);
1044
+ rows.push([`${p.items[0]} and ${p.items[1]}`, list(p.files), list([...new Set(p.addresses.map((x) => x.node ?? x.address))])]);
1045
+ }
1046
+ lines.push(...table(rows));
1047
+ }
797
1048
  const boxes = doc.members.filter((m) => m.box?.isolation);
798
1049
  if (boxes.length > 0) {
799
1050
  lines.push("");
@@ -38,7 +38,7 @@
38
38
  */
39
39
 
40
40
  import { spawnSync } from "node:child_process";
41
- import { copyFileSync, existsSync, lstatSync, mkdtempSync, rmSync, unlinkSync } from "node:fs";
41
+ import { copyFileSync, existsSync, lstatSync, mkdtempSync, rmSync, statSync, unlinkSync, utimesSync } from "node:fs";
42
42
  import { tmpdir } from "node:os";
43
43
  import { join, resolve } from "node:path";
44
44
  import { KEPT_REF_PREFIX } from "../lifecycle/work-lease";
@@ -348,7 +348,16 @@ function worktreeTree(top: string): string {
348
348
  const index = join(dir, "index");
349
349
  // Start from the checkout's index so git reuses its stat data; the original is never written.
350
350
  const own = out(top, ["rev-parse", "--git-path", "index"]);
351
- if (own && existsSync(resolve(top, own))) copyFileSync(resolve(top, own), index);
351
+ if (own && existsSync(resolve(top, own))) {
352
+ const source = resolve(top, own);
353
+ copyFileSync(source, index);
354
+ // Keep the index's own timestamps on the copy. git treats an entry whose
355
+ // file is as new as the index as racily clean and reads its content; a
356
+ // copy stamped now would make a same-size edit made in the same second as
357
+ // the last index write look unchanged, and the snapshot would miss it.
358
+ const { atime, mtime } = statSync(source);
359
+ utimesSync(index, atime, mtime);
360
+ }
352
361
  const env = { GIT_INDEX_FILE: index };
353
362
  const add = git(top, ["add", "--all", "--", ":/"], { env });
354
363
  if (!add.ok) throw new Error(`wip: git add into a temporary index failed: ${add.stderr.trim()}`);