@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
@@ -0,0 +1,340 @@
1
+ /**
2
+ * Overlap between in-flight work items (#4025, policy from #4016).
3
+ *
4
+ * A work lease locks one item, and nothing compares two items with each
5
+ * other. This report does, after the fact: it says which in-flight work items
6
+ * changed the same files, and which entries of a planned change set more than
7
+ * one of them reaches. It never blocks anything. Two people or agents read it
8
+ * and sort the overlap out between them.
9
+ *
10
+ * ## In flight
11
+ *
12
+ * A work item is a current record of a declared work kind. It is in flight
13
+ * unless it implements at least one decision and every decision it implements
14
+ * is confirmed, as #4024 defines confirmed ({@link decisionConfirmed}: in one
15
+ * of the decision kind's closed states and superseded by nothing). The item's
16
+ * own state does not matter: work marked done whose decision is not settled
17
+ * can still collide.
18
+ *
19
+ * ## Linked commits
20
+ *
21
+ * The commits of a work item are the commits linked to it as #4029 defines
22
+ * linked ({@link judgeCommits}): a `Chant-Record: <work kind>:<id>` trailer,
23
+ * or a `Chant-Lease` trailer that resolves to the item. A window match does
24
+ * not link. The walk covers every local branch, every remote-tracking branch
25
+ * and HEAD, so work pushed to a branch and fetched shows before it is merged.
26
+ * The ledger branches under `chant/` hold no work and are left out. Merge
27
+ * commits carry no work of their own and are skipped. Nothing is fetched.
28
+ *
29
+ * ## Files
30
+ *
31
+ * An item's files are the paths its linked commits add, change or delete,
32
+ * from the repository root, with renames read as a delete and an add. Two
33
+ * items overlap by file when both touch one path.
34
+ *
35
+ * ## Change-set entries
36
+ *
37
+ * A change set is planned from many commits, and a release bundles many work
38
+ * items, so an entry belongs to no single item on its face. An entry is
39
+ * attributed to a work item when:
40
+ *
41
+ * 1. the entry has a `source`: the file that declares its graph node, from
42
+ * the repository root, recorded when the change set was stored
43
+ * (`../change-set-graph.ts`);
44
+ * 2. a commit linked to the item changed that file; and
45
+ * 3. that commit is in the history the change set was planned from: an
46
+ * ancestor of (or equal to) the commit its graph was read at, else the
47
+ * commit the gate was reached at. When neither is known, or the commit is
48
+ * not in this repository, the third test is skipped.
49
+ *
50
+ * An entry can be attributed to several items, and two items overlap by
51
+ * address when one entry is attributed to both. An entry with no `source`
52
+ * (stored by an older chant, mapped onto no node, or a node with no source
53
+ * location) is attributed to none and counted in `unattributed`.
54
+ *
55
+ * The change sets read are the ones `workspace status` lists on a gate that
56
+ * is pending or approved: the plans still waiting to apply.
57
+ */
58
+
59
+ import { execFileSync } from "node:child_process";
60
+ import { dirname } from "node:path";
61
+ import type { ChangeSetDocument } from "../change-set";
62
+ import { judgeCommits, hasLeaseLedger, type WorkIndex } from "./commit-links";
63
+ import { declaredRecordKinds, type Declaration } from "./declaration";
64
+ import { declaredKindFile } from "./declared-kinds";
65
+ import { decisionConfirmed } from "./flags";
66
+ import { loadRecordKind } from "./records";
67
+ import { readRecordsFor } from "./records-cli";
68
+ import { LEASE_TRAILER, RECORD_TRAILER } from "./trailers";
69
+
70
+ /** One in-flight work item with at least one linked commit. */
71
+ export interface OverlapItem {
72
+ /** `<work kind>:<id>`, as a `Chant-Record` trailer names it. */
73
+ item: string;
74
+ /** The work record file, from the repository root. */
75
+ path: string;
76
+ state: string | null;
77
+ /** The commits linked to the item, newest first. */
78
+ commits: string[];
79
+ }
80
+
81
+ /** One change-set entry two items both reach. */
82
+ export interface OverlapAddress {
83
+ /** The change set's digest, as `chant change-set show` takes it. */
84
+ changeSet: string;
85
+ /** The workspace member whose ledger stores the change set, or null for the flat ledger. */
86
+ ledgerMember: string | null;
87
+ /** The entry's change-set member and planner address. */
88
+ member: string;
89
+ address: string;
90
+ /** The entry's graph node, or null. */
91
+ node: string | null;
92
+ /** The source file the attribution went through, from the repository root. */
93
+ source: string;
94
+ }
95
+
96
+ /** Two in-flight work items that changed the same thing. */
97
+ export interface OverlapPair {
98
+ /** The two items, `<work kind>:<id>`, in sorted order. */
99
+ items: [string, string];
100
+ /** The files both items' linked commits touch, sorted. */
101
+ files: string[];
102
+ /** The change-set entries attributed to both items, by change set then address. */
103
+ addresses: OverlapAddress[];
104
+ }
105
+
106
+ /** What `workspace status` reports as `overlap`. */
107
+ export interface OverlapReport {
108
+ /** The in-flight work items with a linked commit, by item. */
109
+ items: OverlapItem[];
110
+ /** Each pair of items that share a file or an entry, by the first item then the second. */
111
+ pairs: OverlapPair[];
112
+ /** The digests of the change sets read, sorted. */
113
+ changeSets: string[];
114
+ /** Entries of those change sets with no `source`, so attributed to no item. */
115
+ unattributed: number;
116
+ }
117
+
118
+ /** A change set to attribute: one a pending or approved gate binds. */
119
+ export interface OverlapChangeSetRef {
120
+ digest: string;
121
+ /** A directory whose ledger stores it: the member's directory. */
122
+ cwd: string;
123
+ ledgerMember: string | null;
124
+ /** The commit the gate was reached at, when the pending fact records one. */
125
+ gitSha: string | null;
126
+ }
127
+
128
+ /** Reads a stored change set; the lifecycle store unless a test swaps it. */
129
+ export type StoredChangeSetReader = (digest: string, cwd: string) => Promise<ChangeSetDocument | null>;
130
+
131
+ const defaultReadChangeSet: StoredChangeSetReader = async (digest, cwd) => {
132
+ const { readStoredChangeSet } = await import("../lifecycle/change-set-store");
133
+ const read = await readStoredChangeSet(digest, { cwd });
134
+ return read.status === "found" ? read.document : null;
135
+ };
136
+
137
+ function git(top: string, args: string[], input?: string): string {
138
+ return execFileSync("git", ["-c", "core.quotepath=off", ...args], {
139
+ cwd: top,
140
+ encoding: "utf-8",
141
+ stdio: [input === undefined ? "ignore" : "pipe", "pipe", "ignore"],
142
+ maxBuffer: 512 * 1024 * 1024,
143
+ ...(input !== undefined ? { input } : {}),
144
+ });
145
+ }
146
+
147
+ /** The work items of every declared work kind, with the index commits are judged against and the ones in flight. */
148
+ async function readWorkItems(declaration: Declaration, root: string, top: string): Promise<{ index: WorkIndex; inFlight: Map<string, { path: string; state: string | null }> } | null> {
149
+ const kinds = new Map<string, Set<string>>();
150
+ const inFlight = new Map<string, { path: string; state: string | null }>();
151
+ for (const d of declaredRecordKinds(declaration)) {
152
+ const file = declaredKindFile(d, root);
153
+ const loaded = await loadRecordKind(file, root);
154
+ if (!loaded.kind.work) continue;
155
+ const decisionKind = await loadRecordKind(loaded.kind.work.decisions, dirname(loaded.file));
156
+ const closed = new Set(decisionKind.kind.closedStates ?? []);
157
+ const read = await readRecordsFor({ kind: file, cwd: root });
158
+ const decisions = new Map((read.result.decisions ?? []).map((x) => [x.id, x]));
159
+ const ids = kinds.get(loaded.kind.name) ?? new Set<string>();
160
+ for (const r of read.result.records) {
161
+ if (r.id === null) continue;
162
+ ids.add(r.id);
163
+ if (r.supersededBy !== null) continue;
164
+ const implemented = r.implements ?? [];
165
+ const settled = implemented.length > 0 && implemented.every((x) => decisionConfirmed(decisions.get(x.id), closed));
166
+ const key = `${loaded.kind.name}:${r.id}`;
167
+ if (!settled && !inFlight.has(key)) inFlight.set(key, { path: r.path, state: r.state });
168
+ }
169
+ kinds.set(loaded.kind.name, ids);
170
+ }
171
+ if (kinds.size === 0) return null;
172
+ return { index: { kinds, ledger: hasLeaseLedger(top) }, inFlight };
173
+ }
174
+
175
+ /**
176
+ * The non-merge commits on every local and remote-tracking branch and HEAD
177
+ * that carry a `Chant-Record` or `Chant-Lease` trailer, newest first. The
178
+ * ledger branches under `chant/` are left out.
179
+ */
180
+ export function trailerCommits(top: string): string[] {
181
+ let text: string;
182
+ try {
183
+ text = git(top, [
184
+ "log",
185
+ "--no-merges",
186
+ `--format=%H%x1f%(trailers:key=${RECORD_TRAILER},key=${LEASE_TRAILER},valueonly,separator=%x1e)%x1d`,
187
+ "--exclude=chant/*",
188
+ "--branches",
189
+ "--exclude=*/chant/*",
190
+ "--remotes",
191
+ "HEAD",
192
+ "--",
193
+ ]);
194
+ } catch {
195
+ // An unborn HEAD has no history to read.
196
+ return [];
197
+ }
198
+ const out: string[] = [];
199
+ for (const chunk of text.split("\x1d")) {
200
+ const [sha, trailers] = chunk.trim().split("\x1f");
201
+ if (sha && trailers && trailers.trim() !== "") out.push(sha);
202
+ }
203
+ return out;
204
+ }
205
+
206
+ /** The paths each commit adds, changes or deletes, from the repository root, renames read as a delete and an add. */
207
+ export function commitFiles(top: string, shas: string[]): Map<string, string[]> {
208
+ const out = new Map<string, string[]>();
209
+ if (shas.length === 0) return out;
210
+ const text = git(top, ["log", "--no-walk=unsorted", "--stdin", "--no-renames", "--name-only", "--format=%x1e%H"], `${shas.join("\n")}\n`);
211
+ for (const block of text.split("\x1e")) {
212
+ const lines = block.split("\n").map((l) => l.trim()).filter(Boolean);
213
+ if (lines.length === 0) continue;
214
+ out.set(lines[0], lines.slice(1));
215
+ }
216
+ return out;
217
+ }
218
+
219
+ /** Every commit reachable from `rev`, or null when `rev` is not in the repository. */
220
+ function reachable(top: string, rev: string): Set<string> | null {
221
+ try {
222
+ return new Set(git(top, ["rev-list", rev, "--"]).split("\n").filter(Boolean));
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+
228
+ const byString = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0);
229
+
230
+ /**
231
+ * The work items a change-set entry is attributed to, sorted: each item with
232
+ * a linked commit that changed the entry's `source` and is in `within`, the
233
+ * history the change set was planned from (null when that is unknown, so
234
+ * every linked commit counts). An entry with no source has none.
235
+ *
236
+ * `commitsOf` maps each in-flight item to its linked commits, and `files`
237
+ * each commit to the paths it touches.
238
+ */
239
+ export function attributeEntry(
240
+ source: string | undefined,
241
+ commitsOf: ReadonlyMap<string, readonly string[]>,
242
+ files: ReadonlyMap<string, readonly string[]>,
243
+ within: ReadonlySet<string> | null,
244
+ ): string[] {
245
+ if (source === undefined) return [];
246
+ const out: string[] = [];
247
+ for (const [item, commits] of commitsOf) {
248
+ if (commits.some((sha) => (files.get(sha) ?? []).includes(source) && (within === null || within.has(sha)))) out.push(item);
249
+ }
250
+ return out.sort(byString);
251
+ }
252
+
253
+ /**
254
+ * The overlap report for the workspace declared at `root` in the repository
255
+ * at `top`. Null when the declaration names no work kind. Throws when a work
256
+ * kind or its records can't be read, or git fails; `workspace status` writes
257
+ * null then.
258
+ */
259
+ export async function readOverlap(input: {
260
+ declaration: Declaration;
261
+ root: string;
262
+ top: string;
263
+ changeSets: OverlapChangeSetRef[];
264
+ readChangeSet?: StoredChangeSetReader;
265
+ }): Promise<OverlapReport | null> {
266
+ const { top } = input;
267
+ const work = await readWorkItems(input.declaration, input.root, top);
268
+ if (work === null) return null;
269
+
270
+ // The commits of each in-flight item, newest first.
271
+ const commitsOf = new Map<string, string[]>();
272
+ if (work.inFlight.size > 0) {
273
+ const candidates = trailerCommits(top);
274
+ const links = judgeCommits(top, candidates, work.index);
275
+ for (const sha of candidates) {
276
+ for (const id of links.get(sha)?.ids ?? []) {
277
+ if (!work.inFlight.has(id)) continue;
278
+ const list = commitsOf.get(id) ?? [];
279
+ if (!list.includes(sha)) list.push(sha);
280
+ commitsOf.set(id, list);
281
+ }
282
+ }
283
+ }
284
+ const items = [...commitsOf.keys()].sort(byString);
285
+ const files = commitFiles(top, [...new Set([...commitsOf.values()].flat())]);
286
+ const filesOf = new Map(items.map((id) => [id, new Set(commitsOf.get(id)!.flatMap((sha) => files.get(sha) ?? []))]));
287
+
288
+ // Each change-set entry with a source, attributed to the items whose linked commits changed it within the change set's history.
289
+ const read = input.readChangeSet ?? defaultReadChangeSet;
290
+ const seen = new Set<string>();
291
+ const changeSets: string[] = [];
292
+ let unattributed = 0;
293
+ const addressesOf = new Map<string, OverlapAddress[]>();
294
+ const history = new Map<string, Set<string> | null>();
295
+ for (const ref of input.changeSets) {
296
+ const key = `${ref.ledgerMember ?? ""}\0${ref.digest}`;
297
+ if (seen.has(key)) continue;
298
+ seen.add(key);
299
+ const doc = await read(ref.digest, ref.cwd);
300
+ if (doc === null) continue;
301
+ if (!changeSets.includes(doc.digest)) changeSets.push(doc.digest);
302
+ const at = doc.graph?.at ?? ref.gitSha;
303
+ for (const e of doc.entries) {
304
+ if (e.source === undefined) {
305
+ unattributed++;
306
+ continue;
307
+ }
308
+ // Only an entry two items could share needs the change set's history.
309
+ if (attributeEntry(e.source, commitsOf, files, null).length < 2) continue;
310
+ if (at && !history.has(at)) history.set(at, reachable(top, at));
311
+ const owners = attributeEntry(e.source, commitsOf, files, at ? history.get(at)! : null);
312
+ if (owners.length < 2) continue;
313
+ const address: OverlapAddress = { changeSet: doc.digest, ledgerMember: ref.ledgerMember, member: e.member, address: e.address, node: e.node ?? null, source: e.source };
314
+ for (const id of owners) addressesOf.set(id, [...(addressesOf.get(id) ?? []), address]);
315
+ }
316
+ }
317
+ changeSets.sort(byString);
318
+
319
+ const pairs: OverlapPair[] = [];
320
+ for (let i = 0; i < items.length; i++) {
321
+ for (let j = i + 1; j < items.length; j++) {
322
+ const a = items[i];
323
+ const b = items[j];
324
+ const fb = filesOf.get(b)!;
325
+ const shared = [...filesOf.get(a)!].filter((f) => fb.has(f)).sort(byString);
326
+ const ab = addressesOf.get(b) ?? [];
327
+ const addresses = (addressesOf.get(a) ?? [])
328
+ .filter((x) => ab.includes(x))
329
+ .sort((x, y) => byString(x.changeSet, y.changeSet) || byString(x.member, y.member) || byString(x.address, y.address));
330
+ if (shared.length > 0 || addresses.length > 0) pairs.push({ items: [a, b], files: shared, addresses });
331
+ }
332
+ }
333
+
334
+ return {
335
+ items: items.map((id) => ({ item: id, path: work.inFlight.get(id)!.path, state: work.inFlight.get(id)!.state, commits: commitsOf.get(id)! })),
336
+ pairs,
337
+ changeSets,
338
+ unattributed,
339
+ };
340
+ }
@@ -46,13 +46,14 @@ import { workspacePoints } from "./points-cli";
46
46
  import pointsSchema from "./points.schema.json";
47
47
  import releasesSchema from "./releases.schema.json";
48
48
  import changeSetShowSchema from "./change-set-show.schema.json";
49
+ import deliveryMetricsSchema from "./delivery-metrics.schema.json";
49
50
  import { workspaceStatus } from "./status";
50
51
  import statusSchema from "./status.schema.json";
51
52
 
52
53
  const FIXTURE = join(REPO, "reference-workspace");
53
54
  const TIMEOUT = 240_000;
54
55
 
55
- const SCHEMAS = { ls: lsSchema, graph: graphSchema, check: checkSchema, status: statusSchema, records: recordsSchema, "records-since": recordsSinceSchema, intent: intentSchema, "intent-record": intentRecordSchema, composites: compositesSchema, points: pointsSchema, patch: patchSchema, "ci-last-green": ciLastGreenSchema, "ci-list": ciListSchema, releases: releasesSchema, "change-set-show": changeSetShowSchema };
56
+ const SCHEMAS = { ls: lsSchema, graph: graphSchema, check: checkSchema, status: statusSchema, records: recordsSchema, "records-since": recordsSinceSchema, intent: intentSchema, "intent-record": intentRecordSchema, composites: compositesSchema, points: pointsSchema, patch: patchSchema, "ci-last-green": ciLastGreenSchema, "ci-list": ciListSchema, releases: releasesSchema, "change-set-show": changeSetShowSchema, "delivery-metrics": deliveryMetricsSchema };
56
57
 
57
58
  /** This checkout's chant, started the way the CLI starts it, for members with no toolchain of their own. */
58
59
  const reader: Toolchain = {
@@ -34,6 +34,7 @@ import { RUN_STATEMENT_FAILURE_CODES } from "./trust/run-statement";
34
34
  import { WORK_EVIDENCE_ERROR_CODES } from "./work-evidence";
35
35
  import { RECORD_FINDING_CODES } from "./checks/records";
36
36
  import { ANSWER_WARNING_CODES } from "./points";
37
+ import { FLAG_WARNING_CODES } from "./flags";
37
38
  import { POINTS_ERROR_CODES, POINTS_SOURCE_REASON_CODES } from "./points-cli";
38
39
  import { POINTS_WRITE_ERROR_CODES } from "./decide";
39
40
  import { PATCH_ERROR_CODES } from "./patch";
@@ -50,6 +51,7 @@ import { CI_GREEN_ERROR_CODES } from "./ci-green";
50
51
  import { ROTATION_REFUSAL_CODES } from "./trust/rotation";
51
52
  import { SIGNERS_ERROR_CODES } from "./trust/signers-cli";
52
53
  import { EVIDENCE_ERROR_CODES } from "./trust/evidence";
54
+ import { METRICS_ERROR_CODES, METRICS_REASON_CODES } from "./delivery-metrics";
53
55
 
54
56
  const HERE = import.meta.dirname;
55
57
 
@@ -101,6 +103,7 @@ const PER_COMMAND: Record<string, readonly string[]> = {
101
103
  RUN_STATEMENT_FAILURE_CODES,
102
104
  WORK_EVIDENCE_ERROR_CODES,
103
105
  RECORD_FINDING_CODES,
106
+ FLAG_WARNING_CODES,
104
107
  ANSWER_WARNING_CODES,
105
108
  POINTS_ERROR_CODES,
106
109
  POINTS_SOURCE_REASON_CODES,
@@ -121,6 +124,8 @@ const PER_COMMAND: Record<string, readonly string[]> = {
121
124
  ROTATION_REFUSAL_CODES,
122
125
  SIGNERS_ERROR_CODES,
123
126
  EVIDENCE_ERROR_CODES,
127
+ METRICS_ERROR_CODES,
128
+ METRICS_REASON_CODES,
124
129
  };
125
130
 
126
131
  /** Every string in an `enum` under a property named `code`, anywhere in a schema. */
@@ -95,6 +95,9 @@ export const REASONS = {
95
95
  "work-contract-unknown": "A work record names a contract that no record of its kind's contract kind has.",
96
96
  "work-contract-undecided": "A work record names a contract whose state is not approved, such as a draft.",
97
97
  "work-tier-unknown": "A work record names a builder tier that its kind's work.tier.tiers does not list.",
98
+ // A feature flag that is valid but warned about (records, check, #4024).
99
+ "flag-work-unknown": "A flag record names a work item that no record of its kind's work kind has.",
100
+ "flag-stale": "Every decision a flag's work item implements is confirmed, so the flag is due for removal. check reports it (WSP118).",
98
101
  // An answer to a decision point that is valid but warned about (records, points, ws-058, #2739).
99
102
  "answer-points-unreadable": "The points file the answer kind names can't be read, or is not valid.",
100
103
  "answer-point-unknown": "The answer's point is not declared in the points file the answer kind names.",
@@ -19,6 +19,7 @@ import { pinFile, queryRecords, type RecordsDocument } from "./records-cli";
19
19
  import { parseFrontMatter, RECORD_WARNING_CODES, SEAL_WARNING_CODES } from "./records";
20
20
  import { WORK_WARNING_CODES } from "./work";
21
21
  import { ANSWER_WARNING_CODES } from "./points";
22
+ import { FLAG_WARNING_CODES } from "./flags";
22
23
  import { workspaceGraph } from "./graph-cli";
23
24
  import graphSchema from "./graph.schema.json";
24
25
  import recordsSchema from "./records.schema.json";
@@ -172,7 +173,7 @@ describe("chant workspace records checks each pin", () => {
172
173
  test("the schema lists exactly the warning codes", () => {
173
174
  // A work kind's records carry the work warnings too (#2683), work-done-gap-open included since records walks a done item's region (#2686),
174
175
  // an answer kind's the answer warnings (ws-058), and records adds record-unattested for an author seal under a signers file at base (#2688).
175
- expect(recordsSchema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES, ...ANSWER_WARNING_CODES, ...SEAL_WARNING_CODES]);
176
+ expect(recordsSchema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES, ...ANSWER_WARNING_CODES, ...SEAL_WARNING_CODES, ...FLAG_WARNING_CODES]);
176
177
  });
177
178
 
178
179
  test("records pin <path> prints the entry's path from the workspace root and the file's hash", () => {
@@ -14,6 +14,7 @@ import { queryRecords, RECORDS_CONTRACT_VERSION, RECORDS_OUTPUT_SCHEMA_ID, type
14
14
  import { READ_ERROR_CODES, RECORD_REASON_CODES, RECORD_WARNING_CODES, recordTextDigest, REVIEW_REASON_CODES, SEAL_WARNING_CODES } from "./records";
15
15
  import { WORK_WARNING_CODES } from "./work";
16
16
  import { ANSWER_WARNING_CODES } from "./points";
17
+ import { FLAG_WARNING_CODES } from "./flags";
17
18
  import schema from "./records.schema.json";
18
19
  import { PROVENANCE_LEVELS } from "./trust/attestor";
19
20
 
@@ -152,7 +153,7 @@ describe("records output schema", () => {
152
153
  test("lists exactly the warning codes the code can return", () => {
153
154
  // A work kind's records carry the work warnings too (#2683), work-done-gap-open included since records walks a done item's region (#2686),
154
155
  // an answer kind's the answer warnings (ws-058), and records adds record-unattested for an author seal under a signers file at base (#2688).
155
- expect(schema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES, ...ANSWER_WARNING_CODES, ...SEAL_WARNING_CODES]);
156
+ expect(schema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES, ...ANSWER_WARNING_CODES, ...SEAL_WARNING_CODES, ...FLAG_WARNING_CODES]);
156
157
  });
157
158
 
158
159
  test("every failure validates with its code", async () => {
@@ -254,7 +254,7 @@
254
254
  "items": { "$ref": "#/$defs/asset" }
255
255
  },
256
256
  "warnings": {
257
- "description": "Added in contract 1 by #2549. Findings that leave the record valid: a pinned file that changed, went missing or did not follow a superseding decision, a supersedes link that has no effect yet, (added by #2654) an evidence list that is empty, (added by #2672) a verdict that names no digest, or (added by #2683) a work record's links and dates, or (added by #2688) a record whose author seal is not attested under a signers file at base, or (added by #2772) a work record's acceptance criteria unmet when it is done, or met by a manual verdict from its implementer, or (added by #3147) a work record's contract that no record has or nobody approved, or a builder tier its kind does not list. A work record never carries record-no-evidence; a done one with no evidence carries work-done-unpinned. valid and --current do not look at them.",
257
+ "description": "Added in contract 1 by #2549. Findings that leave the record valid: a pinned file that changed, went missing or did not follow a superseding decision, a supersedes link that has no effect yet, (added by #2654) an evidence list that is empty, (added by #2672) a verdict that names no digest, or (added by #2683) a work record's links and dates, or (added by #2688) a record whose author seal is not attested under a signers file at base, or (added by #2772) a work record's acceptance criteria unmet when it is done, or met by a manual verdict from its implementer, or (added by #3147) a work record's contract that no record has or nobody approved, or a builder tier its kind does not list, or (added by #4024) a flag record's work item that no record has, or a flag due for removal. A work record never carries record-no-evidence; a done one with no evidence carries work-done-unpinned. valid and --current do not look at them.",
258
258
  "type": "array",
259
259
  "items": { "$ref": "#/$defs/warning" }
260
260
  },
@@ -322,6 +322,23 @@
322
322
  "description": "Added in contract 1 by #3147, on every parsed record of a work kind whose work block names a contract kind. The contract record the item builds, by the id in the work block's contract field, with that record's state now; state is null when no record of the contract kind has the id. Null when the item names no contract.",
323
323
  "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/workLink" }]
324
324
  },
325
+ "flag": {
326
+ "description": "Added in contract 1 by #4024, on every parsed record of a flag kind (a kind with a flag block). work is the work item the flag hides, with that item's state (null when no record of the flag kind's work kind has it), or null when the record names none. implements lists each decision the item implements, with its state and confirmed: true when it is in one of its kind's closed states and nothing supersedes it. stale is true when the item implements at least one decision and every one is confirmed: the flag is due for removal, warned flag-stale, and chant workspace check reports it as WSP118.",
327
+ "type": "object",
328
+ "required": ["work", "implements", "stale"],
329
+ "properties": {
330
+ "work": { "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/workLink" }] },
331
+ "implements": {
332
+ "type": "array",
333
+ "items": {
334
+ "type": "object",
335
+ "required": ["id", "state", "confirmed"],
336
+ "properties": { "id": { "type": "string" }, "state": { "type": ["string", "null"] }, "confirmed": { "type": "boolean" } }
337
+ }
338
+ },
339
+ "stale": { "type": "boolean" }
340
+ }
341
+ },
325
342
  "answers": {
326
343
  "description": "Added in contract 1 by #3147, on every record with an id of a work kind whose work block names an answer kind. The decision-point answers about the item: each answer record whose constrains names the item's id, in path order, joined by id and never copied onto the item. Read at the same revision; in the working tree, with the answers a steward keeps on the lifecycle ledger laid over it. Absent when the answer kind can't be read.",
327
344
  "type": "array",
@@ -414,7 +431,9 @@
414
431
  "answer-point-unknown",
415
432
  "answer-point-changed",
416
433
  "record-unattested",
417
- "record-ratified-below-quorum"
434
+ "record-ratified-below-quorum",
435
+ "flag-work-unknown",
436
+ "flag-stale"
418
437
  ]
419
438
  },
420
439
  "message": { "type": "string" }
@@ -38,6 +38,7 @@ import { sourceBlock, sourceBlockProblems, transcriptDrift } from "./source-bloc
38
38
  import type { WorkspaceTree } from "./tree";
39
39
  import type { DecisionWork, WorkAcceptance, WorkLink, WorkWarningCode } from "./work";
40
40
  import type { AnswerWarningCode } from "./points";
41
+ import type { FlagState, FlagWarningCode } from "./flags";
41
42
 
42
43
  // ── Reason codes ─────────────────────────────────────────────────────────────
43
44
 
@@ -209,8 +210,8 @@ export interface VerdictAttestation {
209
210
  }
210
211
 
211
212
  export interface RecordWarning {
212
- /** A work kind's records also carry the codes of `WORK_WARNING_CODES` (#2683), an answer kind's those of `ANSWER_WARNING_CODES` (#2739), and `records` adds `SEAL_WARNING_CODES` (#2688, #2878). */
213
- code: RecordWarningCode | WorkWarningCode | SealWarningCode | AnswerWarningCode;
213
+ /** A work kind's records also carry the codes of `WORK_WARNING_CODES` (#2683), an answer kind's those of `ANSWER_WARNING_CODES` (#2739), a flag kind's those of `FLAG_WARNING_CODES` (#4024), and `records` adds `SEAL_WARNING_CODES` (#2688, #2878). */
214
+ code: RecordWarningCode | WorkWarningCode | SealWarningCode | AnswerWarningCode | FlagWarningCode;
214
215
  message: string;
215
216
  }
216
217
 
@@ -489,6 +490,31 @@ export const recordKindSchema = z
489
490
  * Optional.
490
491
  */
491
492
  answers: z.object({ points: z.string().min(1) }).strict().optional(),
493
+ /**
494
+ * A flag kind (#4024): each record is a feature flag, a workspace record
495
+ * a member's build reads as one build-time parameter. The block names the
496
+ * fields that hold the member whose parameter it sets, the parameter's
497
+ * name, the flag's owner and its value per environment, and the field
498
+ * naming the work item the flag hides, with that work kind's file
499
+ * (relative to this kind file). The record's id is the flag's name.
500
+ *
501
+ * With it, `chant build --env <env>` in that member resolves the
502
+ * parameter from the flag (`flag-params.ts`), `chant workspace status`
503
+ * lists the flag under the member's `featureFlags`, and `records` warns
504
+ * `flag-work-unknown` for a work item no record has and `flag-stale`
505
+ * when every decision the work item implements is confirmed, so the
506
+ * flag is due for removal (`check` WSP118). Optional.
507
+ */
508
+ flag: z
509
+ .object({
510
+ member: z.string().min(1),
511
+ param: z.string().min(1),
512
+ owner: z.string().min(1),
513
+ values: z.string().min(1),
514
+ work: z.object({ field: z.string().min(1), kind: z.string().min(1) }).strict(),
515
+ })
516
+ .strict()
517
+ .optional(),
492
518
  /**
493
519
  * The top-level field that holds a closed record's seal (#2546, ws-003,
494
520
  * ws-063): {@link recordSeal}, the whole-file JCS/SHA-256 of the record
@@ -559,6 +585,10 @@ export const recordKindSchema = z
559
585
  message: "a work kind must have states, and its work open and done states must be listed in them",
560
586
  path: ["work"],
561
587
  })
588
+ .refine((k) => !k.flag || k.idField !== undefined, {
589
+ message: "a flag kind names its id with idField: a flag's id is its name, and a build finds it by that name",
590
+ path: ["flag"],
591
+ })
562
592
  .refine((k) => !k.work?.acceptance || k.pins !== undefined, {
563
593
  message: "a work kind with acceptance criteria must have pins: a criterion is met by the evidence in its pins field",
564
594
  path: ["work", "acceptance"],
@@ -1229,6 +1259,8 @@ export interface RecordEntry {
1229
1259
  acceptance?: WorkAcceptance | null;
1230
1260
  /** For a work kind with a contract link (#3147): the contract record the item builds, with its state, or null when it names none. */
1231
1261
  contract?: WorkLink | null;
1262
+ /** For a flag kind (#4024): the work item the flag hides, the decisions it implements, and whether the flag is due for removal. */
1263
+ flag?: FlagState;
1232
1264
  }
1233
1265
 
1234
1266
  export interface ReadRecordsOptions {
@@ -1575,6 +1607,8 @@ export async function readRecords(loaded: LoadedRecordKind, options: ReadRecords
1575
1607
  const work = kind.work ? await (await import("./work")).applyWork(loaded, entries, options) : undefined;
1576
1608
  // An answer kind's points (ws-058): an answer whose point is gone or changed is warned about.
1577
1609
  if (kind.answers) (await import("./points")).applyAnswers(loaded, entries, options);
1610
+ // A flag kind's work item and whether the flag is due for removal (#4024).
1611
+ if (kind.flag) await (await import("./flags")).applyFlags(loaded, entries, options);
1578
1612
 
1579
1613
  for (const e of entries) e.valid = e.reasons.length === 0;
1580
1614
  const records = options.current ? entries.filter((e) => e.supersededBy === null) : entries;
@@ -0,0 +1,32 @@
1
+ // The feature flag record kind (#4024), data only: no imports, no code.
2
+ //
3
+ // chant workspace records --kind flags/flag.kind.mjs --json
4
+ //
5
+ // A flag is a workspace record a member's build reads as one build-time
6
+ // parameter: chant build --env <env> in that member takes the flag's value
7
+ // for the environment, and records it with source flag:<id>. Turning a flag
8
+ // on is a commit that changes this record, so it is reviewed, and the release
9
+ // cut from that commit carries the new value through the environment's gate.
10
+ //
11
+ // The canonical copy ships in @intentius/chant at
12
+ // src/workspace/reference-kinds/flag. A workspace copies this file and
13
+ // flag.schema.json into a directory of its own, such as flags/, names the
14
+ // kind file in chant.workspace.json's records, and points work.kind at its
15
+ // own work kind file.
16
+ export const recordKind = {
17
+ name: "flag",
18
+ location: { dir: ".", match: "^[a-z][a-z0-9-]{0,62}\\.flag\\.md$" },
19
+ format: "markdown-front-matter",
20
+ schema: { id: "urn:intentius:chant:flag:1", path: "flag.schema.json" },
21
+ idField: "id",
22
+ // The fields chant reads as the flag: the member and parameter it sets,
23
+ // its owner, its value per environment, and the work item it hides, a
24
+ // record of the work kind named here (relative to this file).
25
+ flag: {
26
+ member: "member",
27
+ param: "param",
28
+ owner: "owner",
29
+ values: "values",
30
+ work: { field: "work", kind: "../work/work.kind.mjs" },
31
+ },
32
+ };
@@ -0,0 +1,53 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "urn:intentius:chant:flag:1",
4
+ "title": "Feature flag front matter",
5
+ "description": "The front matter of one feature flag record (#4024): a workspace record one member's build reads as a build-time parameter. The file is named <id>.flag.md.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "patternProperties": {
9
+ "^x-": {}
10
+ },
11
+ "required": ["schema", "id", "owner", "work", "member", "param", "values"],
12
+ "properties": {
13
+ "schema": {
14
+ "description": "Format version of this file. Bumped only by a change to this schema.",
15
+ "const": 1
16
+ },
17
+ "id": {
18
+ "description": "The flag's name, which the build records as the source flag:<id> of the parameter it sets. Lowercase, with dashes.",
19
+ "type": "string",
20
+ "pattern": "^[a-z][a-z0-9-]{0,62}$"
21
+ },
22
+ "title": {
23
+ "description": "What the flag hides, in a few words. Optional.",
24
+ "type": "string"
25
+ },
26
+ "owner": {
27
+ "description": "Who owns the flag, and removes it once its work is settled.",
28
+ "type": "string",
29
+ "minLength": 1
30
+ },
31
+ "work": {
32
+ "description": "The id of the work item the flag hides, a record of the work kind the flag kind names. A work item's implements names a decision, never a flag, so the link is kept here.",
33
+ "type": "string",
34
+ "minLength": 1
35
+ },
36
+ "member": {
37
+ "description": "The workspace member whose build parameter the flag sets, by its name in chant.workspace.json.",
38
+ "type": "string",
39
+ "minLength": 1
40
+ },
41
+ "param": {
42
+ "description": "The build parameter the flag sets: a key of buildParams in the member's chant.config.ts. Its value per environment must have the parameter's type and, when it declares one, be in its enum.",
43
+ "type": "string",
44
+ "minLength": 1
45
+ },
46
+ "values": {
47
+ "description": "The flag's value per environment, keyed by the environment's name. An environment left out builds with the parameter's declared default.",
48
+ "type": "object",
49
+ "propertyNames": { "pattern": "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
50
+ "additionalProperties": { "type": ["string", "number", "boolean"] }
51
+ }
52
+ }
53
+ }