@intentius/chant 0.100.0 → 0.102.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (608) hide show
  1. package/dist/agents/checks.d.ts.map +1 -1
  2. package/dist/attrref.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts.map +1 -1
  4. package/dist/audit/nginx.d.ts.map +1 -1
  5. package/dist/audit/prior-art.d.ts +48 -0
  6. package/dist/audit/prior-art.d.ts.map +1 -1
  7. package/dist/behaviour-kinds.d.ts.map +1 -1
  8. package/dist/build.d.ts +6 -0
  9. package/dist/build.d.ts.map +1 -1
  10. package/dist/builder.d.ts.map +1 -1
  11. package/dist/cli/build-options.d.ts +2 -0
  12. package/dist/cli/build-options.d.ts.map +1 -1
  13. package/dist/cli/commands/build.d.ts.map +1 -1
  14. package/dist/cli/commands/carve-emit.d.ts.map +1 -1
  15. package/dist/cli/commands/check-lexicon.d.ts +16 -0
  16. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  17. package/dist/cli/commands/import.d.ts +8 -0
  18. package/dist/cli/commands/import.d.ts.map +1 -1
  19. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  20. package/dist/cli/commands/init.d.ts +19 -0
  21. package/dist/cli/commands/init.d.ts.map +1 -1
  22. package/dist/cli/commands/onboard.d.ts +9 -0
  23. package/dist/cli/commands/onboard.d.ts.map +1 -1
  24. package/dist/cli/commands/pinned-upgrade.d.ts +19 -5
  25. package/dist/cli/commands/pinned-upgrade.d.ts.map +1 -1
  26. package/dist/cli/handlers/dev.d.ts.map +1 -1
  27. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  28. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  29. package/dist/cli/handlers/misc.d.ts.map +1 -1
  30. package/dist/cli/handlers/operator.d.ts +9 -0
  31. package/dist/cli/handlers/operator.d.ts.map +1 -1
  32. package/dist/cli/handlers/run.d.ts.map +1 -1
  33. package/dist/cli/lsp/server.d.ts.map +1 -1
  34. package/dist/cli/main.d.ts.map +1 -1
  35. package/dist/cli/mcp/server.d.ts.map +1 -1
  36. package/dist/cli/mcp/tools/build.d.ts.map +1 -1
  37. package/dist/cli/mcp/tools/composites.d.ts.map +1 -1
  38. package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
  39. package/dist/cli/mcp/tools/import.d.ts.map +1 -1
  40. package/dist/cli/mcp/tools/lint.d.ts.map +1 -1
  41. package/dist/cli/mcp/tools/scaffold.d.ts.map +1 -1
  42. package/dist/cli/mcp/tools/search.d.ts.map +1 -1
  43. package/dist/cli/mcp/workspace-tools.d.ts +9 -1
  44. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  45. package/dist/cli/registry.d.ts +33 -2
  46. package/dist/cli/registry.d.ts.map +1 -1
  47. package/dist/codegen/fetch.d.ts.map +1 -1
  48. package/dist/codegen/naming.d.ts.map +1 -1
  49. package/dist/codegen/pinned-upgrade.d.ts +67 -3
  50. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  51. package/dist/codegen/registry.d.ts.map +1 -1
  52. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  53. package/dist/components/capability.d.ts.map +1 -1
  54. package/dist/components/driver.d.ts.map +1 -1
  55. package/dist/components/fan-out.d.ts.map +1 -1
  56. package/dist/components/starter-plugin.d.ts.map +1 -1
  57. package/dist/components/verbs/process-runner.d.ts.map +1 -1
  58. package/dist/components/verbs/r2-sync.d.ts.map +1 -1
  59. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  60. package/dist/components/verbs/sbom-generator.d.ts.map +1 -1
  61. package/dist/components/verbs/sign.d.ts.map +1 -1
  62. package/dist/components/verbs/verify.d.ts.map +1 -1
  63. package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
  64. package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
  65. package/dist/components/verbs/wrangler.d.ts.map +1 -1
  66. package/dist/composite.d.ts.map +1 -1
  67. package/dist/config.d.ts +30 -7
  68. package/dist/config.d.ts.map +1 -1
  69. package/dist/declarable.d.ts +16 -0
  70. package/dist/declarable.d.ts.map +1 -1
  71. package/dist/discovery/cache.d.ts.map +1 -1
  72. package/dist/discovery/fold-import.d.ts +15 -5
  73. package/dist/discovery/fold-import.d.ts.map +1 -1
  74. package/dist/discovery/param-deps.d.ts +16 -0
  75. package/dist/discovery/param-deps.d.ts.map +1 -1
  76. package/dist/discovery/sandbox/policy-wire.d.ts.map +1 -1
  77. package/dist/discovery/specifier-resolve.d.ts +37 -0
  78. package/dist/discovery/specifier-resolve.d.ts.map +1 -0
  79. package/dist/errors.d.ts.map +1 -1
  80. package/dist/fold/fold.d.ts +3 -1
  81. package/dist/fold/fold.d.ts.map +1 -1
  82. package/dist/graph-detail.d.ts.map +1 -1
  83. package/dist/graph-ir.d.ts.map +1 -1
  84. package/dist/graph-layout.d.ts.map +1 -1
  85. package/dist/import/base-parser.d.ts.map +1 -1
  86. package/dist/import/embedded.d.ts +7 -0
  87. package/dist/import/embedded.d.ts.map +1 -1
  88. package/dist/import/parser-options.d.ts +14 -0
  89. package/dist/import/parser-options.d.ts.map +1 -0
  90. package/dist/import/parser.d.ts +17 -0
  91. package/dist/import/parser.d.ts.map +1 -1
  92. package/dist/kubectl-context.d.ts.map +1 -1
  93. package/dist/lexicon-output.d.ts.map +1 -1
  94. package/dist/lexicon-schema.d.ts +3 -3
  95. package/dist/lexicon.d.ts +78 -3
  96. package/dist/lexicon.d.ts.map +1 -1
  97. package/dist/lifecycle/assert-live.d.ts.map +1 -1
  98. package/dist/lifecycle/gate-ledger.d.ts +15 -0
  99. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  100. package/dist/lifecycle/git.d.ts.map +1 -1
  101. package/dist/lifecycle/member-ledger.d.ts.map +1 -1
  102. package/dist/lifecycle/observe.d.ts.map +1 -1
  103. package/dist/lifecycle/plan-digest.d.ts +26 -5
  104. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  105. package/dist/lifecycle/plan-ledger.d.ts.map +1 -1
  106. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  107. package/dist/lifecycle/scenario.d.ts.map +1 -1
  108. package/dist/lifecycle/status.d.ts.map +1 -1
  109. package/dist/lifecycle/work-lease.d.ts +61 -2
  110. package/dist/lifecycle/work-lease.d.ts.map +1 -1
  111. package/dist/lint/config.d.ts +4 -4
  112. package/dist/lint/engine.d.ts +6 -0
  113. package/dist/lint/engine.d.ts.map +1 -1
  114. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  115. package/dist/op/activities/activity-contracts.d.ts +12 -11
  116. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  117. package/dist/op/activities/apply.d.ts +79 -9
  118. package/dist/op/activities/apply.d.ts.map +1 -1
  119. package/dist/op/activities/decide.d.ts.map +1 -1
  120. package/dist/op/activities/index.d.ts +1 -1
  121. package/dist/op/activities/index.d.ts.map +1 -1
  122. package/dist/op/activities/lexicon-upgrade.d.ts +44 -12
  123. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
  124. package/dist/op/activities/propose-upgrade.d.ts +2 -0
  125. package/dist/op/activities/propose-upgrade.d.ts.map +1 -1
  126. package/dist/op/activity-contract.d.ts +1 -1
  127. package/dist/op/activity-failure.d.ts.map +1 -1
  128. package/dist/op/activity-profiles.d.ts.map +1 -1
  129. package/dist/op/builders.d.ts.map +1 -1
  130. package/dist/op/composites/apply-op.d.ts +12 -5
  131. package/dist/op/composites/apply-op.d.ts.map +1 -1
  132. package/dist/op/composites/lexicon-upgrade-op.d.ts +22 -6
  133. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -1
  134. package/dist/op/decide-backend.d.ts.map +1 -1
  135. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  136. package/dist/op/emulator-lifecycle.d.ts +21 -2
  137. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  138. package/dist/op/gate-approval.d.ts +5 -1
  139. package/dist/op/gate-approval.d.ts.map +1 -1
  140. package/dist/op/gate-wait.d.ts.map +1 -1
  141. package/dist/op/gate.d.ts +19 -3
  142. package/dist/op/gate.d.ts.map +1 -1
  143. package/dist/op/index.d.ts +3 -1
  144. package/dist/op/index.d.ts.map +1 -1
  145. package/dist/op/local-executor.d.ts +6 -0
  146. package/dist/op/local-executor.d.ts.map +1 -1
  147. package/dist/op/operator.d.ts.map +1 -1
  148. package/dist/op/run-context.d.ts +39 -0
  149. package/dist/op/run-context.d.ts.map +1 -0
  150. package/dist/op/run-live.d.ts.map +1 -1
  151. package/dist/op/runtimes/local.d.ts.map +1 -1
  152. package/dist/op/source-archive.d.ts.map +1 -1
  153. package/dist/op/steward-beside.d.ts.map +1 -1
  154. package/dist/op/steward-points.d.ts.map +1 -1
  155. package/dist/op/steward.d.ts.map +1 -1
  156. package/dist/op/types.d.ts +8 -3
  157. package/dist/op/types.d.ts.map +1 -1
  158. package/dist/op/work-lease-run.d.ts +24 -4
  159. package/dist/op/work-lease-run.d.ts.map +1 -1
  160. package/dist/project-root.d.ts.map +1 -1
  161. package/dist/provenance.d.ts +21 -0
  162. package/dist/provenance.d.ts.map +1 -1
  163. package/dist/pseudo-parameter.d.ts.map +1 -1
  164. package/dist/reconcile.d.ts.map +1 -1
  165. package/dist/secret-materialization.d.ts.map +1 -1
  166. package/dist/serializer.d.ts +8 -0
  167. package/dist/serializer.d.ts.map +1 -1
  168. package/dist/telemetry-attribution.d.ts +77 -0
  169. package/dist/telemetry-attribution.d.ts.map +1 -0
  170. package/dist/terraform/emit-source.d.ts.map +1 -1
  171. package/dist/terraform/parse.d.ts.map +1 -1
  172. package/dist/testing.d.ts.map +1 -1
  173. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
  174. package/dist/workspace/__fixtures__/decide-workspace.d.ts +1 -1
  175. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -1
  176. package/dist/workspace/agent-cli.d.ts +88 -0
  177. package/dist/workspace/agent-cli.d.ts.map +1 -0
  178. package/dist/workspace/box-factory.d.ts +73 -0
  179. package/dist/workspace/box-factory.d.ts.map +1 -0
  180. package/dist/workspace/changes-cli.d.ts.map +1 -1
  181. package/dist/workspace/changes.d.ts +8 -1
  182. package/dist/workspace/changes.d.ts.map +1 -1
  183. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -1
  184. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  185. package/dist/workspace/checks/links.d.ts +1 -0
  186. package/dist/workspace/checks/links.d.ts.map +1 -1
  187. package/dist/workspace/checks/live.d.ts +40 -0
  188. package/dist/workspace/checks/live.d.ts.map +1 -0
  189. package/dist/workspace/checks.d.ts +26 -2
  190. package/dist/workspace/checks.d.ts.map +1 -1
  191. package/dist/workspace/compose-graph.d.ts +73 -0
  192. package/dist/workspace/compose-graph.d.ts.map +1 -1
  193. package/dist/workspace/conformance/index.d.ts +67 -6
  194. package/dist/workspace/conformance/index.d.ts.map +1 -1
  195. package/dist/workspace/conformance/vitest.d.ts +12 -3
  196. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  197. package/dist/workspace/conformance/writer.d.ts +368 -0
  198. package/dist/workspace/conformance/writer.d.ts.map +1 -0
  199. package/dist/workspace/decide.d.ts +1 -1
  200. package/dist/workspace/decide.d.ts.map +1 -1
  201. package/dist/workspace/declaration.d.ts +119 -0
  202. package/dist/workspace/declaration.d.ts.map +1 -1
  203. package/dist/workspace/declaration.schema.json +368 -4
  204. package/dist/workspace/export-cli.d.ts +12 -0
  205. package/dist/workspace/export-cli.d.ts.map +1 -0
  206. package/dist/workspace/export.d.ts +145 -0
  207. package/dist/workspace/export.d.ts.map +1 -0
  208. package/dist/workspace/generated-files.d.ts.map +1 -1
  209. package/dist/workspace/graph-cache.d.ts.map +1 -1
  210. package/dist/workspace/graph-cli.d.ts +2 -0
  211. package/dist/workspace/graph-cli.d.ts.map +1 -1
  212. package/dist/workspace/identity.d.ts +111 -0
  213. package/dist/workspace/identity.d.ts.map +1 -0
  214. package/dist/workspace/import.d.ts +73 -0
  215. package/dist/workspace/import.d.ts.map +1 -0
  216. package/dist/workspace/intent-cli.d.ts +7 -4
  217. package/dist/workspace/intent-cli.d.ts.map +1 -1
  218. package/dist/workspace/intent-record.d.ts +8 -1
  219. package/dist/workspace/intent-record.d.ts.map +1 -1
  220. package/dist/workspace/intent.d.ts +149 -5
  221. package/dist/workspace/intent.d.ts.map +1 -1
  222. package/dist/workspace/kinds.d.ts +24 -6
  223. package/dist/workspace/kinds.d.ts.map +1 -1
  224. package/dist/workspace/lineage-adopt-cli.d.ts +15 -0
  225. package/dist/workspace/lineage-adopt-cli.d.ts.map +1 -0
  226. package/dist/workspace/lineage-adopt.d.ts +106 -0
  227. package/dist/workspace/lineage-adopt.d.ts.map +1 -0
  228. package/dist/workspace/lineage-check.d.ts +9 -2
  229. package/dist/workspace/lineage-check.d.ts.map +1 -1
  230. package/dist/workspace/lineage-cli.d.ts +6 -1
  231. package/dist/workspace/lineage-cli.d.ts.map +1 -1
  232. package/dist/workspace/lineage-hash-index.d.ts +110 -0
  233. package/dist/workspace/lineage-hash-index.d.ts.map +1 -0
  234. package/dist/workspace/lineage-init.d.ts +10 -0
  235. package/dist/workspace/lineage-init.d.ts.map +1 -1
  236. package/dist/workspace/lineage-lock.d.ts +149 -2
  237. package/dist/workspace/lineage-lock.d.ts.map +1 -1
  238. package/dist/workspace/lineage-migrations.d.ts +15 -3
  239. package/dist/workspace/lineage-migrations.d.ts.map +1 -1
  240. package/dist/workspace/lineage-provenance.d.ts +18 -0
  241. package/dist/workspace/lineage-provenance.d.ts.map +1 -0
  242. package/dist/workspace/lineage-upgrade-cli.d.ts +2 -0
  243. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
  244. package/dist/workspace/lineage-upgrade.d.ts +30 -1
  245. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  246. package/dist/workspace/lineage-versions.d.ts +116 -0
  247. package/dist/workspace/lineage-versions.d.ts.map +1 -0
  248. package/dist/workspace/links.d.ts +31 -5
  249. package/dist/workspace/links.d.ts.map +1 -1
  250. package/dist/workspace/ls-generated.d.ts +37 -0
  251. package/dist/workspace/ls-generated.d.ts.map +1 -0
  252. package/dist/workspace/ls.d.ts +4 -0
  253. package/dist/workspace/ls.d.ts.map +1 -1
  254. package/dist/workspace/member-commands.d.ts.map +1 -1
  255. package/dist/workspace/nested-graph.d.ts +56 -0
  256. package/dist/workspace/nested-graph.d.ts.map +1 -0
  257. package/dist/workspace/nesting.d.ts +21 -0
  258. package/dist/workspace/nesting.d.ts.map +1 -0
  259. package/dist/workspace/pin-cli.d.ts +10 -0
  260. package/dist/workspace/pin-cli.d.ts.map +1 -0
  261. package/dist/workspace/pin-integrity.d.ts +51 -0
  262. package/dist/workspace/pin-integrity.d.ts.map +1 -0
  263. package/dist/workspace/points.d.ts.map +1 -1
  264. package/dist/workspace/principal-classes.d.ts +114 -0
  265. package/dist/workspace/principal-classes.d.ts.map +1 -0
  266. package/dist/workspace/reason-codes.d.ts +47 -3
  267. package/dist/workspace/reason-codes.d.ts.map +1 -1
  268. package/dist/workspace/record-assets.d.ts.map +1 -1
  269. package/dist/workspace/record-sessions.d.ts +7 -11
  270. package/dist/workspace/record-sessions.d.ts.map +1 -1
  271. package/dist/workspace/records-checkout.d.ts +54 -0
  272. package/dist/workspace/records-checkout.d.ts.map +1 -0
  273. package/dist/workspace/records-cli.d.ts +57 -2
  274. package/dist/workspace/records-cli.d.ts.map +1 -1
  275. package/dist/workspace/records-close.d.ts +5 -2
  276. package/dist/workspace/records-close.d.ts.map +1 -1
  277. package/dist/workspace/records-since.d.ts.map +1 -1
  278. package/dist/workspace/records-write.d.ts +23 -4
  279. package/dist/workspace/records-write.d.ts.map +1 -1
  280. package/dist/workspace/records.d.ts +58 -5
  281. package/dist/workspace/records.d.ts.map +1 -1
  282. package/dist/workspace/returns.d.ts +129 -0
  283. package/dist/workspace/returns.d.ts.map +1 -0
  284. package/dist/workspace/runs-cli.d.ts +164 -0
  285. package/dist/workspace/runs-cli.d.ts.map +1 -0
  286. package/dist/workspace/runs.d.ts +423 -0
  287. package/dist/workspace/runs.d.ts.map +1 -0
  288. package/dist/workspace/source-block.d.ts +1 -1
  289. package/dist/workspace/source-block.d.ts.map +1 -1
  290. package/dist/workspace/status-gates.d.ts +15 -3
  291. package/dist/workspace/status-gates.d.ts.map +1 -1
  292. package/dist/workspace/status.d.ts +10 -0
  293. package/dist/workspace/status.d.ts.map +1 -1
  294. package/dist/workspace/symbols.d.ts +52 -0
  295. package/dist/workspace/symbols.d.ts.map +1 -0
  296. package/dist/workspace/template-manifest.d.ts +11 -3
  297. package/dist/workspace/template-manifest.d.ts.map +1 -1
  298. package/dist/workspace/trailer-joins.d.ts +46 -0
  299. package/dist/workspace/trailer-joins.d.ts.map +1 -0
  300. package/dist/workspace/trailers.d.ts +89 -0
  301. package/dist/workspace/trailers.d.ts.map +1 -0
  302. package/dist/workspace/trust/attestor.d.ts +8 -0
  303. package/dist/workspace/trust/attestor.d.ts.map +1 -1
  304. package/dist/workspace/trust/dsse.d.ts +58 -0
  305. package/dist/workspace/trust/dsse.d.ts.map +1 -0
  306. package/dist/workspace/trust/evidence-cli.d.ts +66 -0
  307. package/dist/workspace/trust/evidence-cli.d.ts.map +1 -0
  308. package/dist/workspace/trust/evidence.d.ts +93 -0
  309. package/dist/workspace/trust/evidence.d.ts.map +1 -0
  310. package/dist/workspace/trust/policy.d.ts +54 -1
  311. package/dist/workspace/trust/policy.d.ts.map +1 -1
  312. package/dist/workspace/trust/provenance.d.ts +21 -1
  313. package/dist/workspace/trust/provenance.d.ts.map +1 -1
  314. package/dist/workspace/trust/rotation.d.ts +132 -0
  315. package/dist/workspace/trust/rotation.d.ts.map +1 -0
  316. package/dist/workspace/trust/seal.d.ts +33 -0
  317. package/dist/workspace/trust/seal.d.ts.map +1 -1
  318. package/dist/workspace/trust/signers-cli.d.ts +49 -0
  319. package/dist/workspace/trust/signers-cli.d.ts.map +1 -0
  320. package/dist/workspace/trust/ssh-commit.d.ts +12 -0
  321. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  322. package/dist/workspace/trust/test-repo.d.ts +13 -0
  323. package/dist/workspace/trust/test-repo.d.ts.map +1 -1
  324. package/dist/workspace/trust/verify.d.ts +15 -0
  325. package/dist/workspace/trust/verify.d.ts.map +1 -1
  326. package/dist/workspace/work-cli.d.ts +39 -3
  327. package/dist/workspace/work-cli.d.ts.map +1 -1
  328. package/dist/workspace/work-evidence.d.ts +1 -1
  329. package/dist/workspace/work-evidence.d.ts.map +1 -1
  330. package/dist/workspace/work.d.ts +26 -1
  331. package/dist/workspace/work.d.ts.map +1 -1
  332. package/dist/workspace/workspace-principals.schema.json +60 -0
  333. package/dist/workspace/write-scope.d.ts +259 -0
  334. package/dist/workspace/write-scope.d.ts.map +1 -0
  335. package/dist/yaml.d.ts +21 -39
  336. package/dist/yaml.d.ts.map +1 -1
  337. package/package.json +2 -2
  338. package/src/audit/prior-art.ts +15 -0
  339. package/src/build.ts +8 -0
  340. package/src/cli/build-options.ts +4 -1
  341. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +1 -1
  342. package/src/cli/commands/build.ts +2 -0
  343. package/src/cli/commands/carve-emit-state.test.ts +4 -0
  344. package/src/cli/commands/carve-emit.ts +5 -19
  345. package/src/cli/commands/check-lexicon.test.ts +62 -1
  346. package/src/cli/commands/check-lexicon.ts +36 -6
  347. package/src/cli/commands/import-live.test.ts +87 -1
  348. package/src/cli/commands/import-parser-options.test.ts +133 -0
  349. package/src/cli/commands/import.ts +69 -25
  350. package/src/cli/commands/init-lexicon/templates/project.ts +1 -1
  351. package/src/cli/commands/init.test.ts +8 -1
  352. package/src/cli/commands/init.ts +23 -8
  353. package/src/cli/commands/lint.test.ts +37 -0
  354. package/src/cli/commands/onboard.test.ts +53 -3
  355. package/src/cli/commands/onboard.ts +52 -4
  356. package/src/cli/commands/pinned-upgrade.ts +73 -33
  357. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +2 -2
  358. package/src/cli/handlers/components.ts +2 -2
  359. package/src/cli/handlers/dev.ts +8 -6
  360. package/src/cli/handlers/fan-out.test.ts +6 -6
  361. package/src/cli/handlers/fan-out.ts +2 -1
  362. package/src/cli/handlers/graph.test.ts +42 -0
  363. package/src/cli/handlers/graph.ts +22 -0
  364. package/src/cli/handlers/lifecycle.test.ts +67 -0
  365. package/src/cli/handlers/lifecycle.ts +6 -5
  366. package/src/cli/handlers/misc.ts +2 -0
  367. package/src/cli/handlers/operator.ts +88 -4
  368. package/src/cli/handlers/run.test.ts +111 -0
  369. package/src/cli/handlers/run.ts +85 -24
  370. package/src/cli/handlers/scenario.ts +2 -2
  371. package/src/cli/main.test.ts +33 -0
  372. package/src/cli/main.ts +168 -17
  373. package/src/cli/mcp/workspace-tools.test.ts +5 -2
  374. package/src/cli/mcp/workspace-tools.ts +62 -4
  375. package/src/cli/registry.ts +33 -2
  376. package/src/cli/serve-mcp-workspace.test.ts +1 -1
  377. package/src/codegen/pinned-upgrade-multi.test.ts +157 -0
  378. package/src/codegen/pinned-upgrade-tags.test.ts +112 -0
  379. package/src/codegen/pinned-upgrade.test.ts +104 -0
  380. package/src/codegen/pinned-upgrade.ts +250 -56
  381. package/src/codegen/release-wiring.test.ts +5 -1
  382. package/src/codegen/testdata/postgres-tags.json +242 -0
  383. package/src/components/fan-out-output.test.ts +1 -1
  384. package/src/components/fan-out.test.ts +1 -1
  385. package/src/components/promote.test.ts +1 -1
  386. package/src/config.test.ts +21 -0
  387. package/src/config.ts +27 -0
  388. package/src/content-digest.test.ts +2 -2
  389. package/src/declarable.test.ts +21 -0
  390. package/src/declarable.ts +19 -0
  391. package/src/discovery/fold-composite.test.ts +41 -5
  392. package/src/discovery/fold-import.test.ts +129 -0
  393. package/src/discovery/fold-import.ts +643 -131
  394. package/src/discovery/fold-package-composite.test.ts +264 -0
  395. package/src/discovery/fold-specifiers.test.ts +191 -0
  396. package/src/discovery/param-deps.test.ts +66 -1
  397. package/src/discovery/param-deps.ts +45 -0
  398. package/src/discovery/specifier-resolve.ts +344 -0
  399. package/src/fold/fold.test.ts +39 -1
  400. package/src/fold/fold.ts +83 -9
  401. package/src/fold-provenance.test.ts +140 -0
  402. package/src/graph-ir.test.ts +83 -0
  403. package/src/graph-ir.ts +29 -0
  404. package/src/import/embedded.test.ts +20 -0
  405. package/src/import/embedded.ts +13 -0
  406. package/src/import/parser-options.ts +46 -0
  407. package/src/import/parser.ts +20 -0
  408. package/src/lexicon.ts +79 -3
  409. package/src/lifecycle/gate-ledger.test.ts +14 -0
  410. package/src/lifecycle/gate-ledger.ts +18 -1
  411. package/src/lifecycle/observe.test.ts +18 -0
  412. package/src/lifecycle/observe.ts +6 -4
  413. package/src/lifecycle/plan-digest.test.ts +54 -3
  414. package/src/lifecycle/plan-digest.ts +38 -8
  415. package/src/lifecycle/snapshot.ts +2 -2
  416. package/src/lifecycle/work-lease.test.ts +1 -1
  417. package/src/lifecycle/work-lease.ts +93 -2
  418. package/src/lint/engine.test.ts +55 -0
  419. package/src/lint/engine.ts +30 -4
  420. package/src/lint/output-docs.test.ts +59 -56
  421. package/src/op/activities/activity-contracts.ts +1 -0
  422. package/src/op/activities/apply.test.ts +189 -5
  423. package/src/op/activities/apply.ts +230 -8
  424. package/src/op/activities/index.ts +3 -0
  425. package/src/op/activities/lexicon-upgrade.test.ts +160 -6
  426. package/src/op/activities/lexicon-upgrade.ts +154 -51
  427. package/src/op/activities/propose-upgrade.ts +8 -5
  428. package/src/op/composites/apply-op.ts +12 -5
  429. package/src/op/composites/composites.test.ts +30 -3
  430. package/src/op/composites/lexicon-upgrade-op.test.ts +7 -2
  431. package/src/op/composites/lexicon-upgrade-op.ts +29 -8
  432. package/src/op/emulator-lifecycle.test.ts +23 -0
  433. package/src/op/emulator-lifecycle.ts +52 -14
  434. package/src/op/gate-approval.test.ts +91 -0
  435. package/src/op/gate-approval.ts +5 -1
  436. package/src/op/gate.ts +34 -8
  437. package/src/op/index.ts +3 -1
  438. package/src/op/local-executor.test.ts +15 -0
  439. package/src/op/local-executor.ts +40 -12
  440. package/src/op/local-output.ts +1 -1
  441. package/src/op/operator.ts +4 -0
  442. package/src/op/run-context.test.ts +141 -0
  443. package/src/op/run-context.ts +51 -0
  444. package/src/op/runtimes/local.test.ts +14 -0
  445. package/src/op/runtimes/local.ts +2 -0
  446. package/src/op/steward-beside.ts +2 -0
  447. package/src/op/types.ts +8 -3
  448. package/src/op/work-lease-run.test.ts +47 -0
  449. package/src/op/work-lease-run.ts +73 -11
  450. package/src/provenance.ts +35 -0
  451. package/src/serializer.ts +9 -0
  452. package/src/telemetry-attribution.test.ts +91 -0
  453. package/src/telemetry-attribution.ts +145 -0
  454. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  455. package/src/workspace/agent-cli.ts +138 -0
  456. package/src/workspace/agent.schema.json +378 -0
  457. package/src/workspace/behold-kinds.test.ts +1 -1
  458. package/src/workspace/box-factory.test.ts +320 -0
  459. package/src/workspace/box-factory.ts +103 -0
  460. package/src/workspace/changes-cli.ts +5 -0
  461. package/src/workspace/changes.schema.json +171 -1
  462. package/src/workspace/changes.ts +61 -4
  463. package/src/workspace/check-live.test.ts +192 -0
  464. package/src/workspace/check.schema.json +64 -0
  465. package/src/workspace/checks/boxes.test.ts +2 -0
  466. package/src/workspace/checks/links.ts +23 -2
  467. package/src/workspace/checks/live.ts +113 -0
  468. package/src/workspace/checks.test.ts +2 -2
  469. package/src/workspace/checks.ts +49 -8
  470. package/src/workspace/compose-graph.test.ts +47 -0
  471. package/src/workspace/compose-graph.ts +127 -3
  472. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +7 -1
  473. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +5 -0
  474. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +27 -0
  475. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +95 -0
  476. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +132 -0
  477. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +48 -0
  478. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +108 -0
  479. package/src/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
  480. package/src/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
  481. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +578 -0
  482. package/src/workspace/conformance/conformance.e2e.test.ts +35 -3
  483. package/src/workspace/conformance/index.mjs +30 -3
  484. package/src/workspace/conformance/index.ts +103 -15
  485. package/src/workspace/conformance/vitest.ts +44 -2
  486. package/src/workspace/conformance/writer-conformance.e2e.test.ts +131 -0
  487. package/src/workspace/conformance/writer.test.ts +156 -0
  488. package/src/workspace/conformance/writer.ts +880 -0
  489. package/src/workspace/decide.ts +7 -1
  490. package/src/workspace/declaration.schema.json +368 -4
  491. package/src/workspace/declaration.ts +270 -1
  492. package/src/workspace/declared-kinds.test.ts +33 -0
  493. package/src/workspace/evidence.schema.json +279 -0
  494. package/src/workspace/export-cli.ts +180 -0
  495. package/src/workspace/export-import.test.ts +282 -0
  496. package/src/workspace/export.ts +486 -0
  497. package/src/workspace/graph-cli.ts +51 -2
  498. package/src/workspace/graph-contract.test.ts +89 -4
  499. package/src/workspace/graph.schema.json +453 -1
  500. package/src/workspace/identity.test.ts +196 -0
  501. package/src/workspace/identity.ts +186 -0
  502. package/src/workspace/import.ts +325 -0
  503. package/src/workspace/intent-cli.ts +64 -10
  504. package/src/workspace/intent-record.schema.json +208 -0
  505. package/src/workspace/intent-record.ts +35 -1
  506. package/src/workspace/intent-why.test.ts +274 -0
  507. package/src/workspace/intent.schema.json +718 -7
  508. package/src/workspace/intent.test.ts +11 -2
  509. package/src/workspace/intent.ts +495 -32
  510. package/src/workspace/kinds.test.ts +5 -5
  511. package/src/workspace/kinds.ts +62 -33
  512. package/src/workspace/lineage-adopt-cli.ts +103 -0
  513. package/src/workspace/lineage-adopt.test.ts +552 -0
  514. package/src/workspace/lineage-adopt.ts +452 -0
  515. package/src/workspace/lineage-check.ts +26 -5
  516. package/src/workspace/lineage-cli.ts +12 -2
  517. package/src/workspace/lineage-hash-index.ts +305 -0
  518. package/src/workspace/lineage-init.test.ts +9 -0
  519. package/src/workspace/lineage-init.ts +29 -9
  520. package/src/workspace/lineage-lock.ts +54 -0
  521. package/src/workspace/lineage-migrations.test.ts +27 -0
  522. package/src/workspace/lineage-migrations.ts +44 -14
  523. package/src/workspace/lineage-provenance.ts +40 -0
  524. package/src/workspace/lineage-upgrade-cli.ts +9 -5
  525. package/src/workspace/lineage-upgrade.test.ts +92 -1
  526. package/src/workspace/lineage-upgrade.ts +111 -16
  527. package/src/workspace/lineage-versions.ts +348 -0
  528. package/src/workspace/links.test.ts +121 -2
  529. package/src/workspace/links.ts +97 -6
  530. package/src/workspace/ls-contract.test.ts +84 -1
  531. package/src/workspace/ls-generated.ts +111 -0
  532. package/src/workspace/ls.schema.json +19 -0
  533. package/src/workspace/ls.ts +8 -1
  534. package/src/workspace/member-commands.ts +3 -1
  535. package/src/workspace/nested-graph.test.ts +176 -0
  536. package/src/workspace/nested-graph.ts +169 -0
  537. package/src/workspace/nesting.ts +37 -0
  538. package/src/workspace/pin-cli.test.ts +71 -0
  539. package/src/workspace/pin-cli.ts +57 -0
  540. package/src/workspace/pin-integrity.test.ts +121 -0
  541. package/src/workspace/pin-integrity.ts +104 -0
  542. package/src/workspace/points-write.schema.json +2 -0
  543. package/src/workspace/principal-classes.test.ts +175 -0
  544. package/src/workspace/principal-classes.ts +307 -0
  545. package/src/workspace/read-contract.test.ts +24 -0
  546. package/src/workspace/reason-codes.test.ts +20 -2
  547. package/src/workspace/reason-codes.ts +53 -3
  548. package/src/workspace/record-sessions.ts +12 -14
  549. package/src/workspace/records-amend.schema.json +6 -0
  550. package/src/workspace/records-checkout.ts +106 -0
  551. package/src/workspace/records-cli.ts +195 -22
  552. package/src/workspace/records-close.schema.json +7 -2
  553. package/src/workspace/records-close.ts +10 -2
  554. package/src/workspace/records-formats.test.ts +13 -5
  555. package/src/workspace/records-new.schema.json +6 -0
  556. package/src/workspace/records-review.schema.json +6 -0
  557. package/src/workspace/records-sessions.test.ts +23 -6
  558. package/src/workspace/records-uncommitted.test.ts +171 -0
  559. package/src/workspace/records-write.test.ts +65 -8
  560. package/src/workspace/records-write.ts +92 -6
  561. package/src/workspace/records.schema.json +127 -4
  562. package/src/workspace/records.ts +123 -5
  563. package/src/workspace/returns.ts +328 -0
  564. package/src/workspace/runs-cli.ts +381 -0
  565. package/src/workspace/runs-write.schema.json +583 -0
  566. package/src/workspace/runs.schema.json +810 -0
  567. package/src/workspace/runs.test.ts +242 -0
  568. package/src/workspace/runs.ts +709 -0
  569. package/src/workspace/signers.schema.json +206 -0
  570. package/src/workspace/status-contract.test.ts +5 -0
  571. package/src/workspace/status-gates.ts +24 -9
  572. package/src/workspace/status.schema.json +127 -2
  573. package/src/workspace/status.ts +21 -1
  574. package/src/workspace/symbols.ts +180 -0
  575. package/src/workspace/template-manifest.ts +22 -4
  576. package/src/workspace/trailer-joins.test.ts +171 -0
  577. package/src/workspace/trailer-joins.ts +82 -0
  578. package/src/workspace/trailers.ts +148 -0
  579. package/src/workspace/trust/attestor.ts +15 -0
  580. package/src/workspace/trust/dsse.ts +134 -0
  581. package/src/workspace/trust/evidence-cli.ts +195 -0
  582. package/src/workspace/trust/evidence.test.ts +241 -0
  583. package/src/workspace/trust/evidence.ts +207 -0
  584. package/src/workspace/trust/policy.ts +110 -3
  585. package/src/workspace/trust/provenance.ts +41 -4
  586. package/src/workspace/trust/record-seal.test.ts +1 -1
  587. package/src/workspace/trust/rotation.test.ts +258 -0
  588. package/src/workspace/trust/rotation.ts +336 -0
  589. package/src/workspace/trust/seal.ts +63 -1
  590. package/src/workspace/trust/signers-cli.ts +178 -0
  591. package/src/workspace/trust/ssh-commit.ts +53 -4
  592. package/src/workspace/trust/test-repo.ts +18 -0
  593. package/src/workspace/trust/trust.test.ts +8 -2
  594. package/src/workspace/trust/verify-cli.ts +1 -0
  595. package/src/workspace/trust/verify.ts +22 -0
  596. package/src/workspace/work-cli.e2e.test.ts +28 -1
  597. package/src/workspace/work-cli.ts +133 -8
  598. package/src/workspace/work-evidence.schema.json +6 -0
  599. package/src/workspace/work-history.schema.json +73 -1
  600. package/src/workspace/work-history.test.ts +48 -5
  601. package/src/workspace/work-kind-fields.test.ts +154 -0
  602. package/src/workspace/work-lease.schema.json +14 -1
  603. package/src/workspace/work.ts +82 -0
  604. package/src/workspace/workspace-principals.schema.json +60 -0
  605. package/src/workspace/write-scope.test.ts +466 -0
  606. package/src/workspace/write-scope.ts +573 -0
  607. package/src/yaml.test.ts +111 -1
  608. package/src/yaml.ts +141 -549
@@ -10,8 +10,11 @@
10
10
  * the ownership tag on `arm` (chant #1448 — previously `--mode Complete`, which
11
11
  * removed resources chant never applied), the ownership label on `gcp`
12
12
  * (chant #1449), the metadata marker (machines) and managed app
13
- * (volumes/ips/certs/secrets) on `fly` (chant #1449), and the stack itself on
14
- * `cloudformation`, which a resource CFN did not create is not in. All of
13
+ * (volumes/ips/certs/secrets) on `fly` (chant #1449), the ownership labels on
14
+ * `grafana` (chant #3011), the comment marker on `clickhouse` (chant #3208)
15
+ * and `postgres` (chant #3280),
16
+ * and the stack itself on `cloudformation`, which a
17
+ * resource CFN did not create is not in. All of
15
18
  * them are owned-only. See {@link DeleteMode}.
16
19
  *
17
20
  * A gate is a fact, not a wait (#2119): the local executor reads the gate
@@ -52,14 +55,18 @@ import { defaultOutput, hasNativeRollback, type ApplyTarget, type DeleteMode } f
52
55
  export interface ApplyOpConfig {
53
56
  /** Op name (kebab-case). */
54
57
  name: string;
55
- /** Environment — CFN stack name / ARM resource group / kube context env;
56
- * a log label on `gcp` and `fly`. */
58
+ /** Environment — CFN stack name / ARM resource group / kube context env /
59
+ * the `grafana.profiles.<env>` entry on `grafana` / the `sql.profiles.<env>`
60
+ * entry on `clickhouse` and `postgres`; a log label on `gcp` and
61
+ * `fly`. */
57
62
  env: string;
58
63
  /** Native apply mechanism. Default: "kubectl". */
59
64
  target?: ApplyTarget;
60
65
  /** Built manifest/template path. Default per target: `dist` (dir) for kubectl,
61
66
  * `template.json` (file) for CloudFormation/ARM, `dist/gcp.yaml` for gcp,
62
- * `dist/fly.json` for fly. Must match your build output. */
67
+ * `dist/fly.json` for fly, `dist/grafana.json` (the build index) for
68
+ * grafana, `dist/schema.json` for clickhouse and postgres. Must match your
69
+ * build output. */
63
70
  output?: string;
64
71
  /** Project directory to build. Default: ".". */
65
72
  path?: string;
@@ -157,6 +157,33 @@ describe("ApplyOp: shape", () => {
157
157
  expect(applyStep.fn).toBe("nativeApply");
158
158
  expect(applyStep.args).toEqual({ target: "kubectl", env: "prod", output: "dist", deleteMode: "never" });
159
159
  });
160
+
161
+ test("target clickhouse applies the sql build output, dist/schema.json by default (#3208)", () => {
162
+ const { op } = ApplyOp({ name: "p", env: "prod", target: "clickhouse", delete: "owned-only" });
163
+ const phases = getProps(op).phases as Array<Record<string, unknown>>;
164
+ const applyStep = (phases[2].steps as Array<Record<string, unknown>>)[0];
165
+ expect(applyStep.fn).toBe("nativeApply");
166
+ expect(applyStep.args).toEqual({ target: "clickhouse", env: "prod", output: "dist/schema.json", deleteMode: "owned-only" });
167
+ expect(getProps(op).onFailure).toBeUndefined();
168
+ });
169
+
170
+ test("target postgres applies the sql build output, dist/schema.json by default (#3280)", () => {
171
+ const { op } = ApplyOp({ name: "p", env: "prod", target: "postgres", delete: "owned-only" });
172
+ const phases = getProps(op).phases as Array<Record<string, unknown>>;
173
+ const applyStep = (phases[2].steps as Array<Record<string, unknown>>)[0];
174
+ expect(applyStep.fn).toBe("nativeApply");
175
+ expect(applyStep.args).toEqual({ target: "postgres", env: "prod", output: "dist/schema.json", deleteMode: "owned-only" });
176
+ expect(getProps(op).onFailure).toBeUndefined();
177
+ });
178
+
179
+ test("target grafana applies the build index, dist/grafana.json by default (#3011)", () => {
180
+ const { op } = ApplyOp({ name: "p", env: "prod", target: "grafana", delete: "owned-only" });
181
+ const phases = getProps(op).phases as Array<Record<string, unknown>>;
182
+ const applyStep = (phases[2].steps as Array<Record<string, unknown>>)[0];
183
+ expect(applyStep.fn).toBe("nativeApply");
184
+ expect(applyStep.args).toEqual({ target: "grafana", env: "prod", output: "dist/grafana.json", deleteMode: "owned-only" });
185
+ expect(getProps(op).onFailure).toBeUndefined();
186
+ });
160
187
  });
161
188
 
162
189
  describe("ApplyOp: gating + deletes", () => {
@@ -274,7 +301,7 @@ describe("ApplyOp: compensation (#125, total-or-refused in #1449)", () => {
274
301
  });
275
302
 
276
303
  test("compensate: true is refused at build time for every rollback-less target (#1449)", () => {
277
- for (const target of ["kubectl", "kustomize", "arm", "gcp", "fly"] as const) {
304
+ for (const target of ["kubectl", "kustomize", "arm", "gcp", "fly", "grafana", "clickhouse", "postgres"] as const) {
278
305
  expect(() => ApplyOp({ name: "p", env: "prod", target, compensate: true })).toThrow(
279
306
  new RegExp(`ApplyOp "p": compensate is enabled, but target "${target}" has no automatic rollback`),
280
307
  );
@@ -296,7 +323,7 @@ describe("ApplyOp: compensation (#125, total-or-refused in #1449)", () => {
296
323
  });
297
324
 
298
325
  test("an object without a command is refused the same way as true", () => {
299
- for (const target of ["kubectl", "kustomize", "arm", "gcp", "fly"] as const) {
326
+ for (const target of ["kubectl", "kustomize", "arm", "gcp", "fly", "grafana", "clickhouse", "postgres"] as const) {
300
327
  expect(() => ApplyOp({ name: "p", env: "prod", target, compensate: {} })).toThrow(
301
328
  /has no automatic rollback/,
302
329
  );
@@ -304,7 +331,7 @@ describe("ApplyOp: compensation (#125, total-or-refused in #1449)", () => {
304
331
  });
305
332
 
306
333
  test("a command lifts the refusal on every target", () => {
307
- for (const target of ["kubectl", "kustomize", "arm", "gcp", "fly", "cloudformation"] as const) {
334
+ for (const target of ["kubectl", "kustomize", "arm", "gcp", "fly", "grafana", "clickhouse", "postgres", "cloudformation"] as const) {
308
335
  const { op } = ApplyOp({
309
336
  name: "p",
310
337
  env: "prod",
@@ -49,17 +49,22 @@ describe("LexiconUpgradeOp composite (#527)", () => {
49
49
  expect(phases[0].steps[0].outcomeAttribute).toEqual({ name: "HasUpgrade", from: "hasUpgrade" });
50
50
  });
51
51
 
52
- test("in-scope set is exactly the 9 lexicons (helm/forgejo excluded)", () => {
52
+ test("known set is exactly the 11 lexicons (helm/forgejo excluded)", () => {
53
53
  // cedar joined with #1650: it pins `CEDAR_WASM_VERSION`, a dependency
54
54
  // rather than a downloaded spec, but the pinned-constant machinery the Op
55
55
  // drives is identical. helm/forgejo still have no upstream at all.
56
56
  expect([...IN_SCOPE_LEXICONS].sort()).toEqual(
57
- ["aws", "azure", "cedar", "docker", "fly", "gcp", "github", "gitlab", "k8s"].sort(),
57
+ ["aws", "azure", "cedar", "docker", "fly", "gcp", "github", "gitlab", "k3s", "k8s", "sql"].sort(),
58
58
  );
59
59
  expect(IN_SCOPE_LEXICONS).not.toContain("helm");
60
60
  expect(IN_SCOPE_LEXICONS).not.toContain("forgejo");
61
61
  });
62
62
 
63
+ test("accepts a pinned lexicon by name, discovered at run time", () => {
64
+ expect(() => LexiconUpgradeOp({ lexicon: "sql" })).not.toThrow();
65
+ expect(() => LexiconUpgradeOp({ lexicon: "k3s" })).not.toThrow();
66
+ });
67
+
63
68
  test("rejects out-of-scope lexicons at construction", () => {
64
69
  for (const l of ["helm", "forgejo"]) {
65
70
  expect(() => LexiconUpgradeOp({ lexicon: l as never })).toThrow(/not in scope/);
@@ -13,9 +13,13 @@
13
13
  * lands on the Op itself (#2120).
14
14
  *
15
15
  * Lexicon classification (which check the activity dispatches to):
16
- * PINNED {k8s, gcp, docker, gitlab} → checkPinnedUpgrade
17
- * ROLLING {aws, azure, github} → checkRollingUpgrade
18
- * helm / forgejo are excluded — no upstream spec.
16
+ * ROLLING {aws, azure, github, fly} → checkRollingUpgrade
17
+ * PINNED any other lexicon whose plugin declares `upstreamPin`
18
+ * (k8s, k3s, gcp, docker, gitlab, cedar, sql, ...) → checkPinnedUpgrade
19
+ * The pinned set is discovered from the plugin at run time, the way `chant dev
20
+ * pinned-upgrade` does. A pin that moves with other constants (`alsoMoves`, as
21
+ * sql's version plus digest) is report-only. helm / forgejo are excluded: no
22
+ * upstream spec.
19
23
  *
20
24
  * Finding-modes mirror ReconcileOp: `report` (default, no external services)
21
25
  * | `issue` | `pull-request`. For the epic goal, `pull-request` opens/updates a
@@ -43,9 +47,15 @@ import { Op, phase } from "../builders";
43
47
  import type { OpResource } from "../resource";
44
48
  import type { LexiconUpgradeMode, SupportedLexicon } from "../activities/lexicon-upgrade";
45
49
 
46
- /** The in-scope lexicons. helm / forgejo are excluded (no upstream spec). */
50
+ /**
51
+ * The lexicons known to be in scope. This is not a gate: a lexicon that declares
52
+ * `upstreamPin` is dispatched without being listed. helm / forgejo are excluded
53
+ * (no upstream spec).
54
+ */
47
55
  export const IN_SCOPE_LEXICONS: readonly SupportedLexicon[] = [
48
56
  "k8s",
57
+ "k3s",
58
+ "sql",
49
59
  "gcp",
50
60
  "docker",
51
61
  "gitlab",
@@ -74,6 +84,12 @@ export interface LexiconUpgradeOpConfig {
74
84
  * `<cwd>/lexicons/<lexicon>` at run time.
75
85
  */
76
86
  lexiconDir?: string;
87
+ /**
88
+ * Label of one pin to check, for a lexicon whose plugin declares several
89
+ * (`upstreamPins`; sql has `clickhouse` and `postgres-14` to `postgres-18`).
90
+ * Omit to check every pin.
91
+ */
92
+ pin?: string;
77
93
  /**
78
94
  * What to produce on findings. Default: "report".
79
95
  * @default "report"
@@ -86,15 +102,19 @@ export interface LexiconUpgradeOpResources {
86
102
  op: InstanceType<typeof OpResource>;
87
103
  }
88
104
 
105
+ const EXCLUDED_LEXICONS: ReadonlySet<string> = new Set(["helm", "forgejo"]);
106
+
89
107
  /**
90
- * Build a LexiconUpgradeOp for one lexicon. Rejects out-of-scope lexicons at
91
- * construction time so a typo never silently produces a no-op Op.
108
+ * Build a LexiconUpgradeOp for one lexicon. Rejects the lexicons with no
109
+ * upstream spec at construction time. Whether any other name declares a pin is
110
+ * only known by loading its plugin, so the activity checks that at run time and
111
+ * fails with a clear message for a name that has none.
92
112
  */
93
113
  export function LexiconUpgradeOp(config: LexiconUpgradeOpConfig): LexiconUpgradeOpResources {
94
- if (!IN_SCOPE_LEXICONS.includes(config.lexicon)) {
114
+ if (EXCLUDED_LEXICONS.has(config.lexicon)) {
95
115
  throw new Error(
96
116
  `LexiconUpgradeOp: "${config.lexicon}" is not in scope. ` +
97
- `In-scope lexicons: ${IN_SCOPE_LEXICONS.join(", ")} ` +
117
+ `Known lexicons: ${IN_SCOPE_LEXICONS.join(", ")}, and any lexicon declaring upstreamPin ` +
98
118
  `(helm, forgejo have no upstream spec).`,
99
119
  );
100
120
  }
@@ -119,6 +139,7 @@ export function LexiconUpgradeOp(config: LexiconUpgradeOpConfig): LexiconUpgrade
119
139
  args: {
120
140
  lexicon: config.lexicon,
121
141
  ...(config.lexiconDir ? { lexiconDir: config.lexiconDir } : {}),
142
+ ...(config.pin ? { pin: config.pin } : {}),
122
143
  mode: onFinding,
123
144
  },
124
145
  // Surface whether a PR-worthy upgrade was found as the run's
@@ -39,3 +39,26 @@ describe("emulatorLifecycle command builders", () => {
39
39
  expect(emu.endpoint(4599)).toBe("http://localhost:4599");
40
40
  });
41
41
  });
42
+
43
+ describe("an emulator reached by its own client, not over HTTP", () => {
44
+ const pg = emulatorLifecycle({
45
+ name: "chant-postgres",
46
+ image: "postgres:18",
47
+ containerPort: 5432,
48
+ readyCommand: ["pg_isready", "-h", "127.0.0.1"],
49
+ endpoint: (port) => `postgres://postgres@localhost:${port}/postgres`,
50
+ });
51
+
52
+ test("readiness is a command run in the container", () => {
53
+ expect(pg.readyExecCommand("chant-postgres")).toBe("docker exec chant-postgres pg_isready -h 127.0.0.1");
54
+ expect(emulatorLifecycle({ name: "x", image: "x:1", containerPort: 1, healthPath: "/h" }).readyExecCommand("x")).toBeUndefined();
55
+ });
56
+
57
+ test("the endpoint is the declared one, not an http URL", () => {
58
+ expect(pg.endpoint(55432)).toBe("postgres://postgres@localhost:55432/postgres");
59
+ });
60
+
61
+ test("a spec with neither a health path nor a ready command is refused", () => {
62
+ expect(() => emulatorLifecycle({ name: "x", image: "x:1", containerPort: 1 })).toThrow(/neither a healthPath nor a readyCommand/);
63
+ });
64
+ });
@@ -16,8 +16,25 @@ export interface EmulatorSpec {
16
16
  image: string;
17
17
  /** Port the emulator listens on inside the container (e.g. 4566, 4280). */
18
18
  containerPort: number;
19
- /** Health path on the host port (e.g. `/_localstack/health`, `/_mudflaps/health`). */
20
- healthPath: string;
19
+ /**
20
+ * Health path on the host port (e.g. `/_localstack/health`, `/_mudflaps/health`).
21
+ * Required unless {@link readyCommand} is set: an emulator is reached over
22
+ * HTTP or checks itself inside its container, one or the other.
23
+ */
24
+ healthPath?: string;
25
+ /**
26
+ * A command run inside the container (`docker exec <name> ...`) whose exit
27
+ * status 0 means ready, for an emulator with no HTTP health endpoint: a
28
+ * database server answers its own client (`pg_isready`), not a URL. When
29
+ * set, it replaces the HTTP poll of {@link healthPath}.
30
+ */
31
+ readyCommand?: readonly string[];
32
+ /**
33
+ * The endpoint a tool connects to, for an emulator that is not reached over
34
+ * HTTP, e.g. a `postgres://` URL for a database server.
35
+ * Default: `http://localhost:<port>`.
36
+ */
37
+ endpoint?: (port: number) => string;
21
38
  /** Readiness predicate over the health body. Default: any 200 response is ready. */
22
39
  ready?: (healthBody: string) => boolean;
23
40
  /** Extra `docker run` args inserted before the image (e.g. a socket mount). */
@@ -102,6 +119,8 @@ export interface EmulatorLifecycle {
102
119
  existsCommand(name: string): string;
103
120
  rmCommand(name: string): string;
104
121
  healthUrl(port: number): string;
122
+ /** `docker exec <name> <readyCommand...>`, or undefined for an emulator polled over HTTP. */
123
+ readyExecCommand(name: string): string | undefined;
105
124
  endpoint(port: number): string;
106
125
  up(args?: EmulatorUpArgs, signal?: AbortSignal): Promise<{ endpoint: string }>;
107
126
  down(args?: { name?: string }, signal?: AbortSignal): Promise<void>;
@@ -116,6 +135,9 @@ export interface EmulatorLifecycle {
116
135
  */
117
136
  export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
118
137
  const ready = spec.ready ?? (() => true);
138
+ if (spec.healthPath === undefined && !spec.readyCommand?.length) {
139
+ throw new Error(`emulator "${spec.name}" declares neither a healthPath nor a readyCommand, so nothing can tell when it is ready`);
140
+ }
119
141
 
120
142
  const runCommand = (args: EmulatorUpArgs = {}): string => {
121
143
  const name = args.name ?? spec.name;
@@ -128,8 +150,30 @@ export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
128
150
  };
129
151
  const existsCommand = (name: string): string => `docker ps -q -f name=${name}`;
130
152
  const rmCommand = (name: string): string => `docker rm -f ${name}`;
131
- const healthUrl = (port: number): string => `http://localhost:${port}${spec.healthPath}`;
132
- const endpoint = (port: number): string => `http://localhost:${port}`;
153
+ const healthUrl = (port: number): string => `http://localhost:${port}${spec.healthPath ?? ""}`;
154
+ const endpoint = (port: number): string => (spec.endpoint ? spec.endpoint(port) : `http://localhost:${port}`);
155
+ const readyExecCommand = (name: string): string | undefined =>
156
+ spec.readyCommand?.length ? ["docker", "exec", name, ...spec.readyCommand].join(" ") : undefined;
157
+
158
+ /** One readiness probe: the in-container command when declared, else the HTTP health path. */
159
+ async function probe(name: string, port: number, signal?: AbortSignal): Promise<boolean> {
160
+ const command = readyExecCommand(name);
161
+ if (command) {
162
+ try {
163
+ await execAsync(command, { signal });
164
+ return true;
165
+ } catch {
166
+ return false;
167
+ }
168
+ }
169
+ try {
170
+ const res = await fetch(healthUrl(port), { signal });
171
+ return res.ok && ready(await res.text());
172
+ } catch {
173
+ // Not up yet (connection refused / non-2xx) — retry.
174
+ return false;
175
+ }
176
+ }
133
177
 
134
178
  async function up(args: EmulatorUpArgs = {}, signal?: AbortSignal): Promise<{ endpoint: string }> {
135
179
  const name = args.name ?? spec.name;
@@ -153,19 +197,13 @@ export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
153
197
  await execAsync(runCommand({ ...args, name, port }), { signal });
154
198
  }
155
199
 
156
- const url = healthUrl(port);
157
200
  const deadline = Date.now() + timeoutMs;
158
201
  let ok = false;
159
202
  while (Date.now() < deadline) {
160
203
  if (signal?.aborted) throw new Error(`emulator "${name}" wait aborted`);
161
- try {
162
- const res = await fetch(url, { signal });
163
- if (res.ok && ready(await res.text())) {
164
- ok = true;
165
- break;
166
- }
167
- } catch {
168
- // Not up yet (connection refused / non-2xx) — retry.
204
+ if (await probe(name, port, signal)) {
205
+ ok = true;
206
+ break;
169
207
  }
170
208
  await sleep(intervalMs, signal);
171
209
  }
@@ -187,5 +225,5 @@ export function emulatorLifecycle(spec: EmulatorSpec): EmulatorLifecycle {
187
225
  }
188
226
  }
189
227
 
190
- return { runCommand, existsCommand, rmCommand, healthUrl, endpoint, up, down };
228
+ return { runCommand, existsCommand, rmCommand, healthUrl, readyExecCommand, endpoint, up, down };
191
229
  }
@@ -107,6 +107,23 @@ describe("gate approval — quorum (#2508)", () => {
107
107
  expect(port.appended).toHaveLength(0);
108
108
  });
109
109
 
110
+ // #2547: a gate still pending, and an approval given, under the bare sha256: prefix
111
+ // keep working once the run computes the same plan's digest as jcs1-sha256:.
112
+ test("a pending gate and an approval recorded under the bare prefix match the jcs1 digest of the same plan", async () => {
113
+ const jcs = `jcs1-${PLAN_A}`;
114
+ const block: ResolvedGateApproval = { quorum: { count: 1 }, mode: "log-only" };
115
+ const run = async (resolutions: GateResolutionRecord[]) => {
116
+ const port = memoryGateLedgerPort({ resolutions, pending: [{ ...PENDING, approval: block }] });
117
+ return { port, check: await evaluateGate(port, { op: "release", gate: "ship", planDigest: jcs, approval: block, now: NOW }) };
118
+ };
119
+ const approved = await run([approval({ resolvedBy: "alex", timestamp: "2026-09-01T01:00:00.000Z" })]);
120
+ expect(approved.check.satisfied).toBe(true);
121
+ // A standing fact is not rewritten just because the prefix moved.
122
+ const waiting = await run([]);
123
+ expect(waiting.check.satisfied).toBe(false);
124
+ expect(waiting.port.appended).toHaveLength(0);
125
+ });
126
+
110
127
  test("an approval older than the standing pending fact does not count", async () => {
111
128
  const { check } = await decide(
112
129
  [approval({ resolvedBy: "alex", timestamp: "2026-08-31T00:00:00.000Z" })],
@@ -174,6 +191,80 @@ describe("gate approval — policy decisions (#2508)", () => {
174
191
  expect(check.satisfied).toBe(false);
175
192
  });
176
193
 
194
+ test("log-only then enforce: a permit recorded under log-only does not pass the enforced gate (#2512)", async () => {
195
+ const logOnly: ResolvedGateApproval = { policy: POLICY, mode: "log-only", context: { risk: "low" } };
196
+ const enforce: ResolvedGateApproval = { ...logOnly, mode: "enforce" };
197
+ // The pending fact was recorded while the gate was log-only, and the agent's allow was recorded against it.
198
+ const port = memoryGateLedgerPort({
199
+ resolutions: [approval({ resolvedBy: "release-bot", timestamp: "2026-09-01T01:00:00.000Z", approver: { kind: "agent" }, policyDecision: allow("log-only") })],
200
+ pending: [{ ...PENDING, approval: logOnly }],
201
+ });
202
+ const check = await evaluateGate(port, { op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: NOW });
203
+ expect(check.satisfied).toBe(false);
204
+
205
+ const tally = tallyGateApprovals(
206
+ [approval({ resolvedBy: "release-bot", timestamp: "2026-09-01T01:00:00.000Z", approver: { kind: "agent" }, policyDecision: allow("log-only") })],
207
+ "ship", PENDING.timestamp, PLAN_A, enforce,
208
+ );
209
+ expect(tally.permit).toBeUndefined();
210
+ });
211
+
212
+ test("log-only then enforce: a human's approval recorded under log-only still counts toward the quorum (#2512)", async () => {
213
+ const logOnly: ResolvedGateApproval = { quorum: { count: 1 }, policy: POLICY, mode: "log-only" };
214
+ const enforce: ResolvedGateApproval = { ...logOnly, mode: "enforce" };
215
+ const port = memoryGateLedgerPort({
216
+ resolutions: [approval({ resolvedBy: "alex", timestamp: "2026-09-01T01:00:00.000Z", policyDecision: allow("log-only") })],
217
+ pending: [{ ...PENDING, approval: logOnly }],
218
+ });
219
+ const check = await evaluateGate(port, { op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: NOW });
220
+ expect(check.satisfied).toBe(true);
221
+ if (!check.satisfied) return;
222
+ expect(check.via).toBe("quorum");
223
+ expect(check.resolution.resolvedBy).toBe("alex");
224
+ });
225
+
226
+ test("log-only then enforce: the gated run re-records the pending fact under enforce, and only a fresh permit passes (#2512)", async () => {
227
+ const logOnly: ResolvedGateApproval = { policy: POLICY, mode: "log-only", context: { risk: "low" } };
228
+ const enforce: ResolvedGateApproval = { ...logOnly, mode: "enforce" };
229
+ const stale = approval({ resolvedBy: "release-bot", timestamp: "2026-09-01T01:00:00.000Z", approver: { kind: "agent" }, policyDecision: allow("log-only") });
230
+ const port = memoryGateLedgerPort({ resolutions: [stale], pending: [{ ...PENDING, approval: logOnly }] });
231
+
232
+ const first = await evaluateGate(port, { op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: NOW });
233
+ expect(first.satisfied).toBe(false);
234
+ if (first.satisfied) return;
235
+ // The standing fact described the log-only gate, so a fresh one carries
236
+ // enforce, and `chant approve` evaluates the next agent approval under it.
237
+ expect(first.recorded).toBe(true);
238
+ expect(first.pending.approval?.mode).toBe("enforce");
239
+
240
+ // Re-running without a new approval still waits: the stale permit predates the new pending fact as well.
241
+ const again = await evaluateGate(port, { op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: "2026-09-01T12:30:00.000Z" });
242
+ expect(again.satisfied).toBe(false);
243
+
244
+ const fresh = approval({ resolvedBy: "release-bot", timestamp: "2026-09-01T13:00:00.000Z", approver: { kind: "agent" }, policyDecision: allow("enforce") });
245
+ const after = await evaluateGate(
246
+ memoryGateLedgerPort({ resolutions: [stale, fresh], pending: [first.pending] }),
247
+ { op: "release", gate: "ship", planDigest: PLAN_A, approval: enforce, now: "2026-09-01T14:00:00.000Z" },
248
+ );
249
+ expect(after.satisfied).toBe(true);
250
+ if (!after.satisfied) return;
251
+ expect(after.via).toBe("policy");
252
+ expect(after.resolution.timestamp).toBe(fresh.timestamp);
253
+ });
254
+
255
+ test("enforce: a recorded decision with no mode does not pass the gate (#2512)", async () => {
256
+ const block: ResolvedGateApproval = { policy: POLICY, mode: "enforce" };
257
+ const { mode: _mode, ...noMode } = allow()!;
258
+ const { check } = await decide(
259
+ [approval({
260
+ resolvedBy: "release-bot", timestamp: "2026-09-01T01:00:00.000Z", approver: { kind: "agent" },
261
+ policyDecision: noMode as GateResolutionRecord["policyDecision"],
262
+ })],
263
+ block,
264
+ );
265
+ expect(check.satisfied).toBe(false);
266
+ });
267
+
177
268
  test("enforce: a deny-by-default policy keeps the old behaviour, where only a human's approval counts", async () => {
178
269
  const block: ResolvedGateApproval = { policy: POLICY, mode: "enforce" };
179
270
  const deny = { ...allow()!, decision: "deny" as const, determining: ["floor"] };
@@ -119,7 +119,11 @@ export interface GatePolicyAnswer {
119
119
  export interface GatePolicyDecision extends GatePolicyAnswer {
120
120
  policy: string;
121
121
  version: string;
122
- /** The gate's mode when the decision was recorded. A run reads the gate's current mode, not this. */
122
+ /**
123
+ * The gate's mode when the decision was recorded. A permit passes a gate
124
+ * only when both this and the gate's current mode are `enforce` (#2512), so
125
+ * an allow recorded under `log-only` never binds after a switch.
126
+ */
123
127
  mode: GateApprovalMode;
124
128
  }
125
129
 
package/src/op/gate.ts CHANGED
@@ -44,7 +44,7 @@ import {
44
44
  type PendingGateInput,
45
45
  type PendingGateRecord,
46
46
  } from "../lifecycle/gate-ledger";
47
- import { describePlanDigest } from "../lifecycle/plan-digest";
47
+ import { describePlanDigest, samePlanDigest } from "../lifecycle/plan-digest";
48
48
  import { isModelAuthored } from "../lifecycle/gate-origin";
49
49
  import { sortedJsonReplacer } from "../utils";
50
50
  import type { GateApprover, ResolvedGateApproval } from "./gate-approval";
@@ -74,6 +74,19 @@ export interface PendingGatePush {
74
74
  export interface GateLedgerPort {
75
75
  read(op: string): Promise<{ resolutions: GateResolutionRecord[]; pending: PendingGateRecord[] }>;
76
76
  appendPending(input: PendingGateInput): Promise<PendingGatePush>;
77
+ /**
78
+ * The rule a workspace sets for approvals of `gate` (#3163, ws-080), or
79
+ * null when none applies: the declaration's `identity.gates`, read at base.
80
+ * Absent, every approval is judged as before.
81
+ */
82
+ approvalRule?(gate: string): Promise<GateApprovalRule | null>;
83
+ }
84
+
85
+ /** A workspace's rule for which approvals of a gate count (#3163): a signed approval, from a class when it names one. */
86
+ export interface GateApprovalRule {
87
+ requirement: { gate: string; class: string | null };
88
+ /** Why `approval` doesn't count, or null when it does. */
89
+ refuses(approval: GateResolutionRecord): string | null;
77
90
  }
78
91
 
79
92
  /**
@@ -102,6 +115,11 @@ export function gitGateLedgerPort(opts?: { cwd?: string }): GateLedgerPort {
102
115
  const { resolutions, pending } = await readGateLedger(op, opts);
103
116
  return { resolutions, pending };
104
117
  },
118
+ async approvalRule(gate) {
119
+ // Loaded on demand: the rule lives in the workspace modules, which import the Op modules.
120
+ const { gateAdmission } = await import("../workspace/identity");
121
+ return gateAdmission(opts?.cwd ?? process.cwd(), gate);
122
+ },
105
123
  async appendPending(input) {
106
124
  const { record } = await appendPendingGate(input, opts);
107
125
  try {
@@ -288,7 +306,7 @@ export interface GateTally {
288
306
  counted: GateResolutionRecord[];
289
307
  /** The quorum's count, 1 when the gate declares none. */
290
308
  need: number;
291
- /** The newest approval whose recorded permit passes the gate on its own. Only in `enforce` mode, and only under the gate's current policy version. */
309
+ /** The newest approval whose recorded permit passes the gate on its own. Only in `enforce` mode, only for a permit recorded under `enforce`, and only under the gate's current policy version. */
292
310
  permit?: GateResolutionRecord;
293
311
  /** The newest approval of this gate for a different plan, when the gate binds one. */
294
312
  mismatched?: GateResolutionRecord;
@@ -305,8 +323,9 @@ export interface GateTally {
305
323
  * Of those, only a human's counts toward the quorum, once per `resolvedBy`,
306
324
  * and only with one of the quorum's roles when it names any. An agent counts
307
325
  * only through a recorded `allow` in `enforce` mode, evaluated under the
308
- * policy version the gate declares now. A `log-only` decision never changes
309
- * the outcome, and a `deny` never removes a human's approval: a policy can add
326
+ * policy version the gate declares now and recorded while the gate was in
327
+ * `enforce` (#2512). A `log-only` decision never changes the outcome, even
328
+ * after the gate switches to `enforce`, and a `deny` never removes a human's approval: a policy can add
310
329
  * a way through the gate but cannot take one away.
311
330
  */
312
331
  export function tallyGateApprovals(
@@ -325,14 +344,18 @@ export function tallyGateApprovals(
325
344
  let mismatched: GateResolutionRecord | undefined;
326
345
  for (const r of records) {
327
346
  if (r.gate !== gate || at(r) < since) continue;
328
- if (planDigest !== undefined && r.planDigest !== planDigest) {
347
+ if (planDigest !== undefined && !samePlanDigest(r.planDigest, planDigest)) {
329
348
  if (!mismatched || at(r) >= at(mismatched)) mismatched = r;
330
349
  continue;
331
350
  }
332
351
 
333
352
  const decision = r.policyDecision;
353
+ // #2512: the decision must also have been recorded under `enforce`. An
354
+ // allow recorded while the gate was log-only was never binding, and a
355
+ // later switch to enforce does not make it binding.
334
356
  if (
335
357
  approval.mode === "enforce" && approval.policy && decision?.decision === "allow" &&
358
+ decision.mode === "enforce" &&
336
359
  decision.version === approval.policy.version && (!permit || at(r) >= at(permit))
337
360
  ) {
338
361
  permit = r;
@@ -394,10 +417,13 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
394
417
  // a mismatch with a message instead of silently not counting.
395
418
  const env = input.environment;
396
419
  const pending = env === undefined ? ledger.pending : ledger.pending.filter((p) => p.environment === env);
397
- const resolutions =
420
+ // #3163: a gate the workspace's identity.gates names counts only signed approvals from its class.
421
+ const rule = (await port.approvalRule?.(input.gate)) ?? null;
422
+ const resolutions = (
398
423
  env === undefined
399
424
  ? ledger.resolutions
400
- : ledger.resolutions.filter((r) => r.environment === undefined || r.environment === env);
425
+ : ledger.resolutions.filter((r) => r.environment === undefined || r.environment === env)
426
+ ).filter((r) => rule === null || r.gate !== input.gate || rule.refuses(r) === null);
401
427
 
402
428
  const standing = latestPendingGate(pending, input.gate);
403
429
  const since = standing?.timestamp ?? EPOCH;
@@ -447,7 +473,7 @@ export async function evaluateGate(port: GateLedgerPort, input: GateCheckInput):
447
473
  // under another policy version or another context would have `chant
448
474
  // approve` evaluate the policy against something this run no longer has.
449
475
  if (
450
- standing && !isPendingGateExpired(standing, now) && standing.planDigest === input.planDigest &&
476
+ standing && !isPendingGateExpired(standing, now) && samePlanDigest(standing.planDigest, input.planDigest) &&
451
477
  sameApproval(standing.approval, input.approval)
452
478
  ) {
453
479
  return { satisfied: false, pending: standing, recorded: false, ...asMismatch };
package/src/op/index.ts CHANGED
@@ -60,6 +60,8 @@ export type { ActivityProfile, ActivityProfileName } from "./activity-profiles";
60
60
  export { NonRetryableActivityError, nonRetryableFailure } from "./activity-failure";
61
61
  export { runOpLocally, parseDuration, OpRunFailure } from "./local-executor";
62
62
  export type { StepRecord, OpRunResult, RunOpOptions } from "./local-executor";
63
+ export { currentOpRun, withOpRunContext } from "./run-context";
64
+ export type { OpRunContext, PassedGate } from "./run-context";
63
65
  export { evaluateGate, gitGateLedgerPort, memoryGateLedgerPort, approveCommand, describeGateMismatch } from "./gate";
64
66
  export type { GateLedgerPort, GateCheck, GateCheckInput, PendingGatePush, GateDigestMismatch, GateQuorumProgress, GateTally } from "./gate";
65
67
  export { tallyGateApprovals, approverOf } from "./gate";
@@ -71,7 +73,7 @@ export type {
71
73
  GatePolicyEvaluator, GatePolicyRef, GatePolicyRequest, GateQuorum, ResolvedGateApproval,
72
74
  } from "./gate-approval";
73
75
  export {
74
- computePlanDigest, isPlanDigest, describePlanDigest, PLAN_DIGEST_ALGORITHM,
76
+ computePlanDigest, isPlanDigest, describePlanDigest, samePlanDigest, PLAN_DIGEST_ALGORITHM, PLAN_DIGEST_PREFIX,
75
77
  } from "../lifecycle/plan-digest";
76
78
  export { gateName, usesDeprecatedGateKey, DEPRECATED_GATE_KEY_WARNING } from "./gate-name";
77
79
  export type { GateNamed } from "./gate-name";
@@ -232,6 +232,21 @@ describe("runOpLocally — outcomeAttribute", () => {
232
232
  expect(result.records[0].outcome).toEqual({ name: "Drift", value: true });
233
233
  });
234
234
 
235
+ test("an attribute whose path the result does not hold is left off, not recorded as undefined", async () => {
236
+ const done: ActivityFn = async () => ({ state: "done", switched: false });
237
+ const config = op({
238
+ phases: [{ name: "Switch", steps: [
239
+ { kind: "activity", fn: "migrationSwitch", outcomeAttribute: [{ name: "OldColumn", from: "oldColumn" }, { name: "State", from: "state" }] },
240
+ { kind: "activity", fn: "migrationRetain", outcomeAttribute: { name: "RetainUntil", from: "retainUntil" } },
241
+ ] }],
242
+ });
243
+ const result = await runOpLocally(config, new Map([["migrationSwitch", done], ["migrationRetain", done]]), PROFILES);
244
+ expect(result.records[0].outcomes).toEqual([{ name: "State", value: "done" }]);
245
+ expect(result.records[0].outcome).toEqual({ name: "State", value: "done" });
246
+ expect(result.records[1].outcomes).toBeUndefined();
247
+ expect(result.records[1].outcome).toBeUndefined();
248
+ });
249
+
235
250
  test("a step with no attribute leaves both fields absent", async () => {
236
251
  const noop: ActivityFn = async () => ({ ok: true });
237
252
  const config = op({ phases: [{ name: "Plan", steps: [{ kind: "activity", fn: "noop" }] }] });