@intentius/chant 0.101.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 (427) 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/builder.d.ts.map +1 -1
  9. package/dist/cli/commands/carve-emit.d.ts.map +1 -1
  10. package/dist/cli/commands/check-lexicon.d.ts +16 -0
  11. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  12. package/dist/cli/commands/import.d.ts +8 -0
  13. package/dist/cli/commands/import.d.ts.map +1 -1
  14. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  15. package/dist/cli/commands/init.d.ts +19 -0
  16. package/dist/cli/commands/init.d.ts.map +1 -1
  17. package/dist/cli/commands/onboard.d.ts +9 -0
  18. package/dist/cli/commands/onboard.d.ts.map +1 -1
  19. package/dist/cli/commands/pinned-upgrade.d.ts +19 -5
  20. package/dist/cli/commands/pinned-upgrade.d.ts.map +1 -1
  21. package/dist/cli/handlers/dev.d.ts.map +1 -1
  22. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  23. package/dist/cli/handlers/misc.d.ts.map +1 -1
  24. package/dist/cli/handlers/operator.d.ts +9 -0
  25. package/dist/cli/handlers/operator.d.ts.map +1 -1
  26. package/dist/cli/handlers/run.d.ts.map +1 -1
  27. package/dist/cli/lsp/server.d.ts.map +1 -1
  28. package/dist/cli/main.d.ts.map +1 -1
  29. package/dist/cli/mcp/server.d.ts.map +1 -1
  30. package/dist/cli/mcp/tools/build.d.ts.map +1 -1
  31. package/dist/cli/mcp/tools/composites.d.ts.map +1 -1
  32. package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
  33. package/dist/cli/mcp/tools/import.d.ts.map +1 -1
  34. package/dist/cli/mcp/tools/lint.d.ts.map +1 -1
  35. package/dist/cli/mcp/tools/scaffold.d.ts.map +1 -1
  36. package/dist/cli/mcp/tools/search.d.ts.map +1 -1
  37. package/dist/cli/mcp/workspace-tools.d.ts +1 -1
  38. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  39. package/dist/cli/registry.d.ts +11 -2
  40. package/dist/cli/registry.d.ts.map +1 -1
  41. package/dist/codegen/fetch.d.ts.map +1 -1
  42. package/dist/codegen/naming.d.ts.map +1 -1
  43. package/dist/codegen/pinned-upgrade.d.ts +67 -3
  44. package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
  45. package/dist/codegen/registry.d.ts.map +1 -1
  46. package/dist/components/capability-plugin-loader.d.ts.map +1 -1
  47. package/dist/components/capability.d.ts.map +1 -1
  48. package/dist/components/driver.d.ts.map +1 -1
  49. package/dist/components/fan-out.d.ts.map +1 -1
  50. package/dist/components/starter-plugin.d.ts.map +1 -1
  51. package/dist/components/verbs/process-runner.d.ts.map +1 -1
  52. package/dist/components/verbs/r2-sync.d.ts.map +1 -1
  53. package/dist/components/verbs/run-agent.d.ts.map +1 -1
  54. package/dist/components/verbs/sbom-generator.d.ts.map +1 -1
  55. package/dist/components/verbs/sign.d.ts.map +1 -1
  56. package/dist/components/verbs/verify.d.ts.map +1 -1
  57. package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
  58. package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
  59. package/dist/components/verbs/wrangler.d.ts.map +1 -1
  60. package/dist/composite.d.ts.map +1 -1
  61. package/dist/config.d.ts +19 -7
  62. package/dist/config.d.ts.map +1 -1
  63. package/dist/declarable.d.ts +16 -0
  64. package/dist/declarable.d.ts.map +1 -1
  65. package/dist/discovery/cache.d.ts.map +1 -1
  66. package/dist/discovery/fold-import.d.ts +15 -5
  67. package/dist/discovery/fold-import.d.ts.map +1 -1
  68. package/dist/discovery/param-deps.d.ts +16 -0
  69. package/dist/discovery/param-deps.d.ts.map +1 -1
  70. package/dist/discovery/sandbox/policy-wire.d.ts.map +1 -1
  71. package/dist/discovery/specifier-resolve.d.ts +37 -0
  72. package/dist/discovery/specifier-resolve.d.ts.map +1 -0
  73. package/dist/errors.d.ts.map +1 -1
  74. package/dist/fold/fold.d.ts +3 -1
  75. package/dist/fold/fold.d.ts.map +1 -1
  76. package/dist/graph-detail.d.ts.map +1 -1
  77. package/dist/graph-ir.d.ts.map +1 -1
  78. package/dist/graph-layout.d.ts.map +1 -1
  79. package/dist/import/base-parser.d.ts.map +1 -1
  80. package/dist/import/embedded.d.ts +7 -0
  81. package/dist/import/embedded.d.ts.map +1 -1
  82. package/dist/import/parser-options.d.ts +14 -0
  83. package/dist/import/parser-options.d.ts.map +1 -0
  84. package/dist/import/parser.d.ts +17 -0
  85. package/dist/import/parser.d.ts.map +1 -1
  86. package/dist/kubectl-context.d.ts.map +1 -1
  87. package/dist/lexicon-output.d.ts.map +1 -1
  88. package/dist/lexicon-schema.d.ts +3 -3
  89. package/dist/lexicon.d.ts +55 -3
  90. package/dist/lexicon.d.ts.map +1 -1
  91. package/dist/lifecycle/assert-live.d.ts.map +1 -1
  92. package/dist/lifecycle/gate-ledger.d.ts +15 -0
  93. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  94. package/dist/lifecycle/git.d.ts.map +1 -1
  95. package/dist/lifecycle/member-ledger.d.ts.map +1 -1
  96. package/dist/lifecycle/observe.d.ts.map +1 -1
  97. package/dist/lifecycle/plan-ledger.d.ts.map +1 -1
  98. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  99. package/dist/lifecycle/scenario.d.ts.map +1 -1
  100. package/dist/lifecycle/status.d.ts.map +1 -1
  101. package/dist/lifecycle/work-lease.d.ts +61 -2
  102. package/dist/lifecycle/work-lease.d.ts.map +1 -1
  103. package/dist/lint/config.d.ts +8 -8
  104. package/dist/lint/engine.d.ts +6 -0
  105. package/dist/lint/engine.d.ts.map +1 -1
  106. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  107. package/dist/op/activities/activity-contracts.d.ts +11 -11
  108. package/dist/op/activities/apply.d.ts +79 -9
  109. package/dist/op/activities/apply.d.ts.map +1 -1
  110. package/dist/op/activities/decide.d.ts.map +1 -1
  111. package/dist/op/activities/index.d.ts +1 -1
  112. package/dist/op/activities/index.d.ts.map +1 -1
  113. package/dist/op/activities/lexicon-upgrade.d.ts +44 -12
  114. package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
  115. package/dist/op/activity-contract.d.ts +1 -1
  116. package/dist/op/activity-failure.d.ts.map +1 -1
  117. package/dist/op/activity-profiles.d.ts.map +1 -1
  118. package/dist/op/builders.d.ts.map +1 -1
  119. package/dist/op/composites/apply-op.d.ts +12 -5
  120. package/dist/op/composites/apply-op.d.ts.map +1 -1
  121. package/dist/op/composites/lexicon-upgrade-op.d.ts +22 -6
  122. package/dist/op/composites/lexicon-upgrade-op.d.ts.map +1 -1
  123. package/dist/op/decide-backend.d.ts.map +1 -1
  124. package/dist/op/decide-read-inputs.d.ts.map +1 -1
  125. package/dist/op/emulator-lifecycle.d.ts +21 -2
  126. package/dist/op/emulator-lifecycle.d.ts.map +1 -1
  127. package/dist/op/gate-approval.d.ts +5 -1
  128. package/dist/op/gate-approval.d.ts.map +1 -1
  129. package/dist/op/gate-wait.d.ts.map +1 -1
  130. package/dist/op/gate.d.ts +19 -3
  131. package/dist/op/gate.d.ts.map +1 -1
  132. package/dist/op/index.d.ts +2 -0
  133. package/dist/op/index.d.ts.map +1 -1
  134. package/dist/op/local-executor.d.ts +6 -0
  135. package/dist/op/local-executor.d.ts.map +1 -1
  136. package/dist/op/operator.d.ts.map +1 -1
  137. package/dist/op/run-context.d.ts +39 -0
  138. package/dist/op/run-context.d.ts.map +1 -0
  139. package/dist/op/run-live.d.ts.map +1 -1
  140. package/dist/op/runtimes/local.d.ts.map +1 -1
  141. package/dist/op/source-archive.d.ts.map +1 -1
  142. package/dist/op/steward-beside.d.ts.map +1 -1
  143. package/dist/op/steward-points.d.ts.map +1 -1
  144. package/dist/op/steward.d.ts.map +1 -1
  145. package/dist/op/types.d.ts +8 -3
  146. package/dist/op/types.d.ts.map +1 -1
  147. package/dist/op/work-lease-run.d.ts +24 -4
  148. package/dist/op/work-lease-run.d.ts.map +1 -1
  149. package/dist/project-root.d.ts.map +1 -1
  150. package/dist/provenance.d.ts +21 -0
  151. package/dist/provenance.d.ts.map +1 -1
  152. package/dist/pseudo-parameter.d.ts.map +1 -1
  153. package/dist/reconcile.d.ts.map +1 -1
  154. package/dist/secret-materialization.d.ts.map +1 -1
  155. package/dist/terraform/emit-source.d.ts.map +1 -1
  156. package/dist/terraform/parse.d.ts.map +1 -1
  157. package/dist/testing.d.ts.map +1 -1
  158. package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
  159. package/dist/workspace/__fixtures__/decide-workspace.d.ts +1 -1
  160. package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -1
  161. package/dist/workspace/agent-cli.d.ts +5 -0
  162. package/dist/workspace/agent-cli.d.ts.map +1 -1
  163. package/dist/workspace/box-factory.d.ts +73 -0
  164. package/dist/workspace/box-factory.d.ts.map +1 -0
  165. package/dist/workspace/changes.d.ts.map +1 -1
  166. package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -1
  167. package/dist/workspace/checks/boxes.d.ts.map +1 -1
  168. package/dist/workspace/checks.d.ts +5 -0
  169. package/dist/workspace/checks.d.ts.map +1 -1
  170. package/dist/workspace/compose-graph.d.ts +10 -0
  171. package/dist/workspace/compose-graph.d.ts.map +1 -1
  172. package/dist/workspace/conformance/index.d.ts +67 -6
  173. package/dist/workspace/conformance/index.d.ts.map +1 -1
  174. package/dist/workspace/conformance/vitest.d.ts +12 -3
  175. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  176. package/dist/workspace/conformance/writer.d.ts +368 -0
  177. package/dist/workspace/conformance/writer.d.ts.map +1 -0
  178. package/dist/workspace/decide.d.ts +1 -1
  179. package/dist/workspace/decide.d.ts.map +1 -1
  180. package/dist/workspace/declaration.d.ts +91 -4
  181. package/dist/workspace/declaration.d.ts.map +1 -1
  182. package/dist/workspace/declaration.schema.json +234 -5
  183. package/dist/workspace/generated-files.d.ts.map +1 -1
  184. package/dist/workspace/graph-cache.d.ts.map +1 -1
  185. package/dist/workspace/graph-cli.d.ts +2 -0
  186. package/dist/workspace/graph-cli.d.ts.map +1 -1
  187. package/dist/workspace/identity.d.ts +111 -0
  188. package/dist/workspace/identity.d.ts.map +1 -0
  189. package/dist/workspace/intent-cli.d.ts +7 -4
  190. package/dist/workspace/intent-cli.d.ts.map +1 -1
  191. package/dist/workspace/intent-record.d.ts +8 -1
  192. package/dist/workspace/intent-record.d.ts.map +1 -1
  193. package/dist/workspace/intent.d.ts +149 -5
  194. package/dist/workspace/intent.d.ts.map +1 -1
  195. package/dist/workspace/kinds.d.ts +18 -4
  196. package/dist/workspace/kinds.d.ts.map +1 -1
  197. package/dist/workspace/lineage-hash-index.d.ts.map +1 -1
  198. package/dist/workspace/lineage-lock.d.ts +2 -2
  199. package/dist/workspace/lineage-lock.d.ts.map +1 -1
  200. package/dist/workspace/links.d.ts.map +1 -1
  201. package/dist/workspace/points.d.ts.map +1 -1
  202. package/dist/workspace/principal-classes.d.ts +114 -0
  203. package/dist/workspace/principal-classes.d.ts.map +1 -0
  204. package/dist/workspace/reason-codes.d.ts +21 -1
  205. package/dist/workspace/reason-codes.d.ts.map +1 -1
  206. package/dist/workspace/record-assets.d.ts.map +1 -1
  207. package/dist/workspace/records-checkout.d.ts +54 -0
  208. package/dist/workspace/records-checkout.d.ts.map +1 -0
  209. package/dist/workspace/records-cli.d.ts +27 -1
  210. package/dist/workspace/records-cli.d.ts.map +1 -1
  211. package/dist/workspace/records-close.d.ts +1 -1
  212. package/dist/workspace/records-close.d.ts.map +1 -1
  213. package/dist/workspace/records-since.d.ts.map +1 -1
  214. package/dist/workspace/records-write.d.ts +5 -4
  215. package/dist/workspace/records-write.d.ts.map +1 -1
  216. package/dist/workspace/records.d.ts +15 -0
  217. package/dist/workspace/records.d.ts.map +1 -1
  218. package/dist/workspace/runs-cli.d.ts +164 -0
  219. package/dist/workspace/runs-cli.d.ts.map +1 -0
  220. package/dist/workspace/runs.d.ts +423 -0
  221. package/dist/workspace/runs.d.ts.map +1 -0
  222. package/dist/workspace/source-block.d.ts +1 -1
  223. package/dist/workspace/source-block.d.ts.map +1 -1
  224. package/dist/workspace/status-gates.d.ts +15 -3
  225. package/dist/workspace/status-gates.d.ts.map +1 -1
  226. package/dist/workspace/status.d.ts +10 -0
  227. package/dist/workspace/status.d.ts.map +1 -1
  228. package/dist/workspace/symbols.d.ts +52 -0
  229. package/dist/workspace/symbols.d.ts.map +1 -0
  230. package/dist/workspace/trailer-joins.d.ts +46 -0
  231. package/dist/workspace/trailer-joins.d.ts.map +1 -0
  232. package/dist/workspace/trailers.d.ts +89 -0
  233. package/dist/workspace/trailers.d.ts.map +1 -0
  234. package/dist/workspace/trust/attestor.d.ts.map +1 -1
  235. package/dist/workspace/trust/rotation.d.ts.map +1 -1
  236. package/dist/workspace/trust/seal.d.ts +33 -0
  237. package/dist/workspace/trust/seal.d.ts.map +1 -1
  238. package/dist/workspace/trust/test-repo.d.ts.map +1 -1
  239. package/dist/workspace/work-cli.d.ts +39 -3
  240. package/dist/workspace/work-cli.d.ts.map +1 -1
  241. package/dist/workspace/work-evidence.d.ts +1 -1
  242. package/dist/workspace/work-evidence.d.ts.map +1 -1
  243. package/dist/workspace/work.d.ts +26 -1
  244. package/dist/workspace/work.d.ts.map +1 -1
  245. package/dist/workspace/workspace-principals.schema.json +60 -0
  246. package/dist/workspace/write-scope.d.ts +79 -19
  247. package/dist/workspace/write-scope.d.ts.map +1 -1
  248. package/dist/yaml.d.ts +21 -39
  249. package/dist/yaml.d.ts.map +1 -1
  250. package/package.json +2 -2
  251. package/src/audit/prior-art.ts +15 -0
  252. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +1 -1
  253. package/src/cli/commands/carve-emit-state.test.ts +4 -0
  254. package/src/cli/commands/carve-emit.ts +5 -19
  255. package/src/cli/commands/check-lexicon.test.ts +62 -1
  256. package/src/cli/commands/check-lexicon.ts +36 -6
  257. package/src/cli/commands/import-live.test.ts +18 -0
  258. package/src/cli/commands/import-parser-options.test.ts +133 -0
  259. package/src/cli/commands/import.ts +21 -3
  260. package/src/cli/commands/init-lexicon/templates/project.ts +1 -1
  261. package/src/cli/commands/init.test.ts +8 -1
  262. package/src/cli/commands/init.ts +23 -8
  263. package/src/cli/commands/lint.test.ts +37 -0
  264. package/src/cli/commands/onboard.test.ts +53 -3
  265. package/src/cli/commands/onboard.ts +52 -4
  266. package/src/cli/commands/pinned-upgrade.ts +73 -33
  267. package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +2 -2
  268. package/src/cli/handlers/components.ts +2 -2
  269. package/src/cli/handlers/dev.ts +8 -6
  270. package/src/cli/handlers/lifecycle.test.ts +67 -0
  271. package/src/cli/handlers/lifecycle.ts +6 -5
  272. package/src/cli/handlers/misc.ts +2 -0
  273. package/src/cli/handlers/operator.ts +87 -3
  274. package/src/cli/handlers/run.test.ts +111 -0
  275. package/src/cli/handlers/run.ts +85 -24
  276. package/src/cli/handlers/scenario.ts +2 -2
  277. package/src/cli/main.test.ts +1 -0
  278. package/src/cli/main.ts +65 -11
  279. package/src/cli/mcp/workspace-tools.test.ts +5 -2
  280. package/src/cli/mcp/workspace-tools.ts +29 -2
  281. package/src/cli/registry.ts +11 -2
  282. package/src/codegen/pinned-upgrade-multi.test.ts +157 -0
  283. package/src/codegen/pinned-upgrade-tags.test.ts +112 -0
  284. package/src/codegen/pinned-upgrade.test.ts +104 -0
  285. package/src/codegen/pinned-upgrade.ts +250 -56
  286. package/src/codegen/testdata/postgres-tags.json +242 -0
  287. package/src/config.test.ts +21 -0
  288. package/src/config.ts +15 -0
  289. package/src/declarable.test.ts +21 -0
  290. package/src/declarable.ts +19 -0
  291. package/src/discovery/fold-composite.test.ts +41 -5
  292. package/src/discovery/fold-import.test.ts +129 -0
  293. package/src/discovery/fold-import.ts +643 -131
  294. package/src/discovery/fold-package-composite.test.ts +264 -0
  295. package/src/discovery/fold-specifiers.test.ts +191 -0
  296. package/src/discovery/param-deps.test.ts +66 -1
  297. package/src/discovery/param-deps.ts +45 -0
  298. package/src/discovery/specifier-resolve.ts +344 -0
  299. package/src/fold/fold.test.ts +39 -1
  300. package/src/fold/fold.ts +83 -9
  301. package/src/fold-provenance.test.ts +140 -0
  302. package/src/graph-ir.test.ts +83 -0
  303. package/src/graph-ir.ts +29 -0
  304. package/src/import/embedded.test.ts +20 -0
  305. package/src/import/embedded.ts +13 -0
  306. package/src/import/parser-options.ts +46 -0
  307. package/src/import/parser.ts +20 -0
  308. package/src/lexicon.ts +55 -3
  309. package/src/lifecycle/gate-ledger.ts +16 -0
  310. package/src/lifecycle/observe.test.ts +18 -0
  311. package/src/lifecycle/observe.ts +6 -4
  312. package/src/lifecycle/snapshot.ts +2 -2
  313. package/src/lifecycle/work-lease.test.ts +1 -1
  314. package/src/lifecycle/work-lease.ts +93 -2
  315. package/src/lint/engine.test.ts +55 -0
  316. package/src/lint/engine.ts +30 -4
  317. package/src/lint/output-docs.test.ts +59 -56
  318. package/src/op/activities/apply.test.ts +189 -5
  319. package/src/op/activities/apply.ts +230 -8
  320. package/src/op/activities/index.ts +3 -0
  321. package/src/op/activities/lexicon-upgrade.test.ts +160 -6
  322. package/src/op/activities/lexicon-upgrade.ts +154 -51
  323. package/src/op/composites/apply-op.ts +12 -5
  324. package/src/op/composites/composites.test.ts +30 -3
  325. package/src/op/composites/lexicon-upgrade-op.test.ts +7 -2
  326. package/src/op/composites/lexicon-upgrade-op.ts +29 -8
  327. package/src/op/emulator-lifecycle.test.ts +23 -0
  328. package/src/op/emulator-lifecycle.ts +52 -14
  329. package/src/op/gate-approval.test.ts +74 -0
  330. package/src/op/gate-approval.ts +5 -1
  331. package/src/op/gate.ts +31 -5
  332. package/src/op/index.ts +2 -0
  333. package/src/op/local-executor.test.ts +15 -0
  334. package/src/op/local-executor.ts +40 -12
  335. package/src/op/local-output.ts +1 -1
  336. package/src/op/operator.ts +4 -0
  337. package/src/op/run-context.test.ts +141 -0
  338. package/src/op/run-context.ts +51 -0
  339. package/src/op/runtimes/local.test.ts +14 -0
  340. package/src/op/runtimes/local.ts +2 -0
  341. package/src/op/steward-beside.ts +2 -0
  342. package/src/op/types.ts +8 -3
  343. package/src/op/work-lease-run.test.ts +47 -0
  344. package/src/op/work-lease-run.ts +73 -11
  345. package/src/provenance.ts +35 -0
  346. package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
  347. package/src/workspace/agent-cli.ts +7 -3
  348. package/src/workspace/agent.schema.json +22 -0
  349. package/src/workspace/box-factory.test.ts +320 -0
  350. package/src/workspace/box-factory.ts +103 -0
  351. package/src/workspace/changes.schema.json +12 -20
  352. package/src/workspace/changes.ts +4 -1
  353. package/src/workspace/checks/boxes.test.ts +2 -0
  354. package/src/workspace/checks.ts +29 -5
  355. package/src/workspace/compose-graph.ts +7 -0
  356. package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +27 -0
  357. package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +95 -0
  358. package/src/workspace/conformance/__writer_fixture__/decisions/points.json +132 -0
  359. package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +48 -0
  360. package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +108 -0
  361. package/src/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
  362. package/src/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
  363. package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +578 -0
  364. package/src/workspace/conformance/conformance.e2e.test.ts +35 -3
  365. package/src/workspace/conformance/index.mjs +30 -3
  366. package/src/workspace/conformance/index.ts +103 -15
  367. package/src/workspace/conformance/vitest.ts +44 -2
  368. package/src/workspace/conformance/writer-conformance.e2e.test.ts +131 -0
  369. package/src/workspace/conformance/writer.test.ts +156 -0
  370. package/src/workspace/conformance/writer.ts +880 -0
  371. package/src/workspace/decide.ts +7 -1
  372. package/src/workspace/declaration.schema.json +234 -5
  373. package/src/workspace/declaration.ts +174 -10
  374. package/src/workspace/graph-cli.ts +5 -2
  375. package/src/workspace/graph.schema.json +247 -0
  376. package/src/workspace/identity.test.ts +196 -0
  377. package/src/workspace/identity.ts +186 -0
  378. package/src/workspace/intent-cli.ts +64 -10
  379. package/src/workspace/intent-record.schema.json +208 -0
  380. package/src/workspace/intent-record.ts +35 -1
  381. package/src/workspace/intent-why.test.ts +274 -0
  382. package/src/workspace/intent.schema.json +718 -7
  383. package/src/workspace/intent.test.ts +11 -2
  384. package/src/workspace/intent.ts +495 -32
  385. package/src/workspace/kinds.ts +44 -37
  386. package/src/workspace/points-write.schema.json +1 -0
  387. package/src/workspace/principal-classes.test.ts +175 -0
  388. package/src/workspace/principal-classes.ts +307 -0
  389. package/src/workspace/reason-codes.test.ts +11 -3
  390. package/src/workspace/reason-codes.ts +24 -1
  391. package/src/workspace/records-amend.schema.json +2 -0
  392. package/src/workspace/records-checkout.ts +106 -0
  393. package/src/workspace/records-cli.ts +103 -11
  394. package/src/workspace/records-close.schema.json +1 -0
  395. package/src/workspace/records-new.schema.json +2 -0
  396. package/src/workspace/records-review.schema.json +2 -0
  397. package/src/workspace/records-uncommitted.test.ts +171 -0
  398. package/src/workspace/records-write.ts +28 -6
  399. package/src/workspace/records.schema.json +72 -1
  400. package/src/workspace/records.ts +27 -0
  401. package/src/workspace/runs-cli.ts +381 -0
  402. package/src/workspace/runs-write.schema.json +583 -0
  403. package/src/workspace/runs.schema.json +810 -0
  404. package/src/workspace/runs.test.ts +242 -0
  405. package/src/workspace/runs.ts +709 -0
  406. package/src/workspace/status-contract.test.ts +5 -0
  407. package/src/workspace/status-gates.ts +22 -8
  408. package/src/workspace/status.schema.json +127 -2
  409. package/src/workspace/status.ts +21 -1
  410. package/src/workspace/symbols.ts +180 -0
  411. package/src/workspace/trailer-joins.test.ts +171 -0
  412. package/src/workspace/trailer-joins.ts +82 -0
  413. package/src/workspace/trailers.ts +148 -0
  414. package/src/workspace/trust/seal.ts +52 -1
  415. package/src/workspace/work-cli.e2e.test.ts +28 -1
  416. package/src/workspace/work-cli.ts +133 -8
  417. package/src/workspace/work-evidence.schema.json +2 -0
  418. package/src/workspace/work-history.schema.json +73 -1
  419. package/src/workspace/work-history.test.ts +48 -5
  420. package/src/workspace/work-kind-fields.test.ts +154 -0
  421. package/src/workspace/work-lease.schema.json +14 -1
  422. package/src/workspace/work.ts +82 -0
  423. package/src/workspace/workspace-principals.schema.json +60 -0
  424. package/src/workspace/write-scope.test.ts +128 -2
  425. package/src/workspace/write-scope.ts +170 -45
  426. package/src/yaml.test.ts +111 -1
  427. package/src/yaml.ts +141 -549
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * `chant dev pinned-upgrade <lexicon-dir>` command implementation.
3
3
  *
4
- * Detects whether a pinned-version lexicon (k8s, gcp, docker, gitlab) has a
4
+ * Detects whether a lexicon that declares `upstreamPin` has a
5
5
  * newer stable upstream release. When it does, it dry-run bumps the pinned
6
6
  * version constant, regenerates + surface-diffs (via #524's regenLexicon), then
7
7
  * reverts the bump — leaving the working tree unchanged.
@@ -10,20 +10,21 @@
10
10
  */
11
11
 
12
12
  import { existsSync } from "fs";
13
- import { basename, resolve } from "path";
13
+ import { resolve } from "path";
14
14
  import {
15
- checkPinnedUpgrade,
15
+ checkPinnedUpgrades,
16
+ lexiconNameOf,
16
17
  type LexiconId,
17
18
  type UpgradeCheckResult,
18
19
  } from "../../codegen/pinned-upgrade";
19
20
 
20
- const PINNED_LEXICONS: readonly LexiconId[] = ["k8s", "gcp", "docker", "gitlab"];
21
-
22
21
  export interface PinnedUpgradeOptions {
23
22
  /** Resolved path to the lexicon directory. */
24
23
  lexiconDir: string;
25
24
  /** Explicit lexicon id; inferred from the directory name when omitted. */
26
25
  lexicon?: LexiconId;
26
+ /** Label of one pin to check, for a lexicon that declares several. Omitted: all of them. */
27
+ pin?: string;
27
28
  /** Force re-fetch of the upstream spec during regen. */
28
29
  force?: boolean;
29
30
  /** Print subprocess output while running. */
@@ -31,12 +32,13 @@ export interface PinnedUpgradeOptions {
31
32
  }
32
33
 
33
34
  /**
34
- * Infer the lexicon id from a directory name. Returns null when the directory
35
- * basename is not one of the pinned lexicons.
35
+ * Infer the lexicon id from a directory: its package name
36
+ * (`@intentius/chant-lexicon-<name>`), else the directory name. Whether the
37
+ * lexicon is pinned is the plugin's `upstreamPin`, checked by
38
+ * `checkPinnedUpgrade`, not a list here.
36
39
  */
37
- export function inferLexicon(lexiconDir: string): LexiconId | null {
38
- const name = basename(lexiconDir);
39
- return (PINNED_LEXICONS as readonly string[]).includes(name) ? (name as LexiconId) : null;
40
+ export function inferLexicon(lexiconDir: string): LexiconId {
41
+ return lexiconNameOf(lexiconDir);
40
42
  }
41
43
 
42
44
  /**
@@ -52,37 +54,38 @@ export function semverLabel(result: UpgradeCheckResult): "minor" | "major" | "no
52
54
 
53
55
  /**
54
56
  * Run the pinned-upgrade check and return the structured result.
55
- * Never throws — a missing dir or an unrecognised lexicon becomes a fetchError.
57
+ * Never throws — a missing dir or a lexicon with no `upstreamPin` becomes a fetchError.
56
58
  */
57
59
  export async function runPinnedUpgrade(opts: PinnedUpgradeOptions): Promise<UpgradeCheckResult> {
60
+ return (await runPinnedUpgrades(opts))[0]!;
61
+ }
62
+
63
+ /**
64
+ * Run the check for every pin the plugin declares (or the one in `opts.pin`),
65
+ * one result each. A lexicon with one pin gives one result.
66
+ */
67
+ export async function runPinnedUpgrades(opts: PinnedUpgradeOptions): Promise<UpgradeCheckResult[]> {
58
68
  const dir = resolve(opts.lexiconDir);
59
69
 
60
70
  const lexicon = opts.lexicon ?? inferLexicon(dir);
61
- if (!lexicon) {
62
- return {
63
- lexicon: "k8s",
64
- hasUpgrade: false,
65
- from: "(unknown)",
66
- to: null,
67
- validation: null,
68
- fetchError: `Not a pinned lexicon directory: ${dir}. Expected one of: ${PINNED_LEXICONS.join(", ")}.`,
69
- };
70
- }
71
71
 
72
72
  if (!existsSync(dir)) {
73
- return {
74
- lexicon,
75
- hasUpgrade: false,
76
- from: "(unknown)",
77
- to: null,
78
- validation: null,
79
- fetchError: `Lexicon directory not found: ${dir}`,
80
- };
73
+ return [
74
+ {
75
+ lexicon,
76
+ hasUpgrade: false,
77
+ from: "(unknown)",
78
+ to: null,
79
+ validation: null,
80
+ fetchError: `Lexicon directory not found: ${dir}`,
81
+ },
82
+ ];
81
83
  }
82
84
 
83
- return checkPinnedUpgrade({
85
+ return checkPinnedUpgrades({
84
86
  lexiconDir: dir,
85
87
  lexicon,
88
+ pin: opts.pin,
86
89
  force: opts.force,
87
90
  verbose: opts.verbose,
88
91
  });
@@ -112,12 +115,14 @@ function c(text: string, code: string): string {
112
115
  */
113
116
  export function printPinnedUpgradeResult(result: UpgradeCheckResult, json: boolean): void {
114
117
  const label = semverLabel(result);
118
+ const name = result.pin ? `${result.lexicon}/${result.pin}` : result.lexicon;
115
119
 
116
120
  if (json) {
117
121
  console.log(
118
122
  JSON.stringify(
119
123
  {
120
124
  lexicon: result.lexicon,
125
+ ...(result.pin ? { pin: result.pin } : {}),
121
126
  hasUpgrade: result.hasUpgrade,
122
127
  from: result.from,
123
128
  to: result.to,
@@ -128,6 +133,7 @@ export function printPinnedUpgradeResult(result: UpgradeCheckResult, json: boole
128
133
  validationOk: result.validation?.ok ?? null,
129
134
  failures: result.validation?.failures ?? [],
130
135
  fetchError: result.fetchError,
136
+ manualPin: result.manualPin ?? null,
131
137
  },
132
138
  null,
133
139
  2,
@@ -137,22 +143,31 @@ export function printPinnedUpgradeResult(result: UpgradeCheckResult, json: boole
137
143
  }
138
144
 
139
145
  if (result.fetchError) {
140
- console.log(c(`[${result.lexicon}] error: ${result.fetchError}`, COLORS.red));
146
+ console.log(c(`[${name}] error: ${result.fetchError}`, COLORS.red));
141
147
  return;
142
148
  }
143
149
 
144
150
  if (!result.hasUpgrade) {
145
151
  console.log(
146
- c(`[${result.lexicon}] up to date`, COLORS.green) +
152
+ c(`[${name}] up to date`, COLORS.green) +
147
153
  ` pinned=${result.from}` +
148
154
  (result.to ? ` latest=${result.to}` : ""),
149
155
  );
150
156
  return;
151
157
  }
152
158
 
159
+ if (result.manualPin) {
160
+ console.log(
161
+ c(`[${name}] upgrade available`, COLORS.bold) + ` ${result.from} -> ${result.to}`,
162
+ );
163
+ console.log(` The pin in ${result.manualPin.file} cannot be rewritten generically, so nothing was edited or regenerated.`);
164
+ console.log(` ${result.manualPin.instructions}`);
165
+ return;
166
+ }
167
+
153
168
  // Upgrade available
154
169
  console.log(
155
- c(`[${result.lexicon}] upgrade available`, COLORS.bold) +
170
+ c(`[${name}] upgrade available`, COLORS.bold) +
156
171
  ` ${result.from} -> ${result.to}`,
157
172
  );
158
173
 
@@ -202,3 +217,28 @@ export function printPinnedUpgradeResult(result: UpgradeCheckResult, json: boole
202
217
  `Validation: ${validationLabel} Semver: ${c(label, labelColor)} ${result.from} -> ${result.to}`,
203
218
  );
204
219
  }
220
+
221
+ /**
222
+ * Print one result per pin. One result prints exactly as `printPinnedUpgradeResult`;
223
+ * several print in turn, and `json` emits an array of the same objects.
224
+ */
225
+ export function printPinnedUpgradeResults(results: UpgradeCheckResult[], json: boolean): void {
226
+ if (results.length === 1) {
227
+ printPinnedUpgradeResult(results[0]!, json);
228
+ return;
229
+ }
230
+ if (json) {
231
+ // Each result prints as its own JSON object; collect them into one array.
232
+ const lines: string[] = [];
233
+ const log = console.log;
234
+ console.log = (...a: unknown[]) => void lines.push(a.join(" "));
235
+ try {
236
+ for (const r of results) printPinnedUpgradeResult(r, true);
237
+ } finally {
238
+ console.log = log;
239
+ }
240
+ console.log(`[${lines.join(",\n")}]`);
241
+ return;
242
+ }
243
+ for (const r of results) printPinnedUpgradeResult(r, false);
244
+ }
@@ -14,7 +14,7 @@ jobs:
14
14
  runs-on: ubuntu-latest
15
15
  container: node:22-slim
16
16
  steps:
17
- - uses: actions/checkout@v4
17
+ - uses: actions/checkout@v7
18
18
  - run: chant run --components shared-alb --env staging --dump-outputs shared-alb.outputs.json
19
19
  - name: Upload shared-alb outputs
20
20
  uses: actions/upload-artifact@v4
@@ -27,7 +27,7 @@ jobs:
27
27
  - shared-alb
28
28
  container: node:22-slim
29
29
  steps:
30
- - uses: actions/checkout@v4
30
+ - uses: actions/checkout@v7
31
31
  - name: Download shared-alb outputs
32
32
  uses: actions/download-artifact@v4
33
33
  with:
@@ -48,7 +48,7 @@ import type { ComponentBomSummary } from "../../lifecycle/build-ledger";
48
48
  import type { BuildArchiveManifest } from "../../components/verbs/build-archive";
49
49
  import { loadChantConfig } from "../../config";
50
50
  import { applyLiveEndpoint } from "../../live-endpoint";
51
- import { isResourceDeclarable } from "../../declarable";
51
+ import { isObservableDeclarable } from "../../declarable";
52
52
  import { build } from "../../build";
53
53
  import { discoverComponents } from "../../components/discover";
54
54
  import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
@@ -643,7 +643,7 @@ export async function runComponentsStatus(ctx: CommandContext): Promise<number>
643
643
  const entities = new Map<string, { entityType: string; props: Record<string, unknown> }>();
644
644
  for (const [name, entity] of buildResult.entities) {
645
645
  // Resource declarables only (see lifecycle/observe.ts).
646
- if (entity.lexicon === plugin.name && isResourceDeclarable(entity)) {
646
+ if (entity.lexicon === plugin.name && isObservableDeclarable(entity)) {
647
647
  declared.add(name);
648
648
  entities.set(name, {
649
649
  entityType: entity.entityType,
@@ -85,16 +85,18 @@ export async function runDevSurfaceDiff(ctx: CommandContext): Promise<number> {
85
85
 
86
86
  export async function runDevPinnedUpgrade(ctx: CommandContext): Promise<number> {
87
87
  const dir = ctx.args.extraPositional ?? ".";
88
- const { runPinnedUpgrade, printPinnedUpgradeResult } = await import("../commands/pinned-upgrade");
89
- const result = await runPinnedUpgrade({
88
+ const { runPinnedUpgrades, printPinnedUpgradeResults } = await import("../commands/pinned-upgrade");
89
+ // The optional second positional is a pin label, for a lexicon that declares several pins.
90
+ const results = await runPinnedUpgrades({
90
91
  lexiconDir: resolve(dir),
92
+ pin: ctx.args.extraPositional2,
91
93
  force: ctx.args.force,
92
94
  verbose: ctx.args.verbose,
93
95
  });
94
- printPinnedUpgradeResult(result, ctx.args.format === "json");
95
- // Exit non-zero when the upstream query failed or the regen validation broke.
96
- if (result.fetchError) return 1;
97
- if (result.validation && !result.validation.ok) return 1;
96
+ printPinnedUpgradeResults(results, ctx.args.format === "json");
97
+ // Exit non-zero when an upstream query failed or a regen validation broke.
98
+ if (results.some((r) => r.fetchError)) return 1;
99
+ if (results.some((r) => r.validation && !r.validation.ok)) return 1;
98
100
  return 0;
99
101
  }
100
102
 
@@ -97,6 +97,19 @@ function makeBuildResult(entitiesByLexicon: Record<string, string[]>): BuildResu
97
97
  } as unknown as BuildResult;
98
98
  }
99
99
 
100
+ /**
101
+ * #3001 — a build holding one resource and one property-kind declarable that
102
+ * the resource inlines, the shape every grafana dashboard with exported panels
103
+ * has. Only the resource has a live counterpart of its own.
104
+ */
105
+ function buildWithPropertyKind(lexicon: string): BuildResult {
106
+ const build = makeBuildResult({ [lexicon]: ["board"] });
107
+ const panel = { lexicon, entityType: `${lexicon}::Panel`, kind: "property", props: { title: "CPU" } };
108
+ build.entities.set("board", { lexicon, entityType: `${lexicon}::Board`, kind: "resource", props: { panels: [panel] } } as never);
109
+ build.entities.set("cpuPanel", panel as never);
110
+ return build;
111
+ }
112
+
100
113
  const meta = (overrides: Partial<ResourceMetadata> = {}): ResourceMetadata => ({
101
114
  type: "AWS::S3::Bucket",
102
115
  status: "CREATE_COMPLETE",
@@ -537,6 +550,37 @@ describe("runLifecycleDiff --live", () => {
537
550
  });
538
551
 
539
552
  // #1014 — property-level drift, gated purely on the deep capability.
553
+ test("hands only resource-kind entities to describeResources and observeResourcesDeep (#3001)", async () => {
554
+ buildMock.mockResolvedValue(buildWithPropertyKind("graf"));
555
+ fetchLifecycleMock.mockResolvedValue(undefined);
556
+ readSnapshotMock.mockResolvedValue(null);
557
+ const seen: { thin?: string[]; deep?: string[] } = {};
558
+ const plugins: LexiconPlugin[] = [
559
+ createMockPlugin({
560
+ name: "graf",
561
+ describeResources: async (opts) => {
562
+ seen.thin = [...opts.entities.keys()];
563
+ return { board: meta({ type: "graf::Board", physicalId: "uid-1" }) };
564
+ },
565
+ observeResourcesDeep: async (opts) => {
566
+ seen.deep = [...opts.entities.keys()];
567
+ return deepObservation({ board: { type: "graf::Board", properties: { panels: [{ title: "CPU" }] } } });
568
+ },
569
+ }),
570
+ ];
571
+ const exit = await runLifecycleDiff({
572
+ args: makeArgs({ path: "diff", extraPositional: "prod", live: true }),
573
+ plugins,
574
+ serializers: plugins.map((p) => p.serializer),
575
+ });
576
+ expect(exit).toBe(0);
577
+ expect(seen.thin).toEqual(["board"]);
578
+ expect(seen.deep).toEqual(["board"]);
579
+ const stdout = stdoutBuf.join("\n");
580
+ expect(stdout).not.toContain("cpuPanel");
581
+ expect(stdout).toContain("0 missing");
582
+ });
583
+
540
584
  describe("deep observation (#1014)", () => {
541
585
  const withDeep = (over: Parameters<typeof createMockPlugin>[0] = {}) =>
542
586
  createMockPlugin({
@@ -872,6 +916,29 @@ describe("runLifecyclePlan", () => {
872
916
  expect(stdoutBuf.join("\n")).toContain("bucket");
873
917
  });
874
918
 
919
+ test("proposes no create for a property-kind declarable its parent inlines (#3001)", async () => {
920
+ buildMock.mockResolvedValue(buildWithPropertyKind("graf"));
921
+ let names: string[] = [];
922
+ const plugins: LexiconPlugin[] = [
923
+ createMockPlugin({
924
+ name: "graf",
925
+ describeResources: async (opts) => {
926
+ names = opts.entityNames;
927
+ return { board: meta({ type: "graf::Board", physicalId: "uid-1" }) };
928
+ },
929
+ }),
930
+ ];
931
+ const exit = await runLifecyclePlan({
932
+ args: makeArgs({ path: "plan", extraPositional: "prod", json: true }),
933
+ plugins,
934
+ serializers: plugins.map((p) => p.serializer),
935
+ });
936
+ expect(exit).toBe(0);
937
+ expect(names).toEqual(["board"]);
938
+ const plan = JSON.parse(stdoutBuf.join("\n"));
939
+ expect(plan.entries.map((e: { name: string }) => e.name)).not.toContain("cpuPanel");
940
+ });
941
+
875
942
  // #1983 — `--report markdown` emits the reviewer-facing projection instead
876
943
  // of the human render, same as `--report gitlab-mr` swaps in the widget JSON.
877
944
  test("--report markdown emits the markdown projection instead of the human render", async () => {
@@ -42,7 +42,7 @@ import { unknownEnvError, isProdLikeEnvironment } from "../../env";
42
42
  import { planTeardown, executeTeardown, type TeardownPlan, type TeardownReport } from "../../lifecycle/teardown";
43
43
  import { collectBuildRootContributors } from "../plugins";
44
44
  import { applyLiveEndpoint } from "../../live-endpoint";
45
- import { isResourceDeclarable } from "../../declarable";
45
+ import { isObservableDeclarable } from "../../declarable";
46
46
  import { formatError, formatWarning, formatSuccess, formatBold } from "../format";
47
47
  import type { CommandContext } from "../registry";
48
48
  import type { LifecycleSnapshot } from "../../lifecycle/types";
@@ -812,13 +812,14 @@ async function runLifecycleDiffLive(args: LiveDiffArgs): Promise<LiveDiffOutcome
812
812
  }
813
813
 
814
814
  // Build per-lexicon entity index. Resource declarables only — outputs,
815
- // parameters and serializer directives have no live counterpart, and a
815
+ // parameters, serializer directives and property-kind declarables (read
816
+ // inside their parent, #3001) have no live counterpart of their own, and a
816
817
  // declared name the reader can never resolve reads as missing (see
817
818
  // lifecycle/observe.ts).
818
819
  const declared = new Set<string>();
819
820
  const entities: DeclaredEntities = new Map();
820
821
  for (const [name, entity] of args.buildResult.entities) {
821
- if (entity.lexicon === lexiconName && isResourceDeclarable(entity)) {
822
+ if (entity.lexicon === lexiconName && isObservableDeclarable(entity)) {
822
823
  declared.add(name);
823
824
  // Path origins are carried across explicitly (#1443): this map is plain
824
825
  // objects, and the symbol-keyed provenance channel does not survive it.
@@ -1289,11 +1290,11 @@ export async function runLifecyclePlan(ctx: CommandContext): Promise<number> {
1289
1290
  // A receipt has no `props` payload of its own but is declared, diffed,
1290
1291
  // and observed like any resource (#1832) — it joins the declared axis
1291
1292
  // so its lexicon's observation can confirm presence or absence.
1292
- if (entity.lexicon === lexiconName && (isResourceDeclarable(entity) || isEffectReceipt(entity))) {
1293
+ if (entity.lexicon === lexiconName && (isObservableDeclarable(entity) || isEffectReceipt(entity))) {
1293
1294
  declared.add(name);
1294
1295
  entities.set(name, {
1295
1296
  entityType: entity.entityType,
1296
- props: (isResourceDeclarable(entity) && entity.props != null ? entity.props : {}) as Record<string, unknown>,
1297
+ props: (isObservableDeclarable(entity) && entity.props != null ? entity.props : {}) as Record<string, unknown>,
1297
1298
  });
1298
1299
  }
1299
1300
  }
@@ -324,6 +324,7 @@ export async function runImport(ctx: CommandContext): Promise<number> {
324
324
  lexicon: "k8s",
325
325
  output: args.output,
326
326
  force: args.force,
327
+ parserOptions: args.parserOption,
327
328
  });
328
329
  printImportResult(result);
329
330
  return result.success ? 0 : 1;
@@ -381,6 +382,7 @@ export async function runImport(ctx: CommandContext): Promise<number> {
381
382
  output: ctx.args.output,
382
383
  force: ctx.args.force,
383
384
  lexicon: ctx.args.lexicon,
385
+ parserOptions: ctx.args.parserOption,
384
386
  });
385
387
 
386
388
  printImportResult(result);
@@ -9,7 +9,7 @@
9
9
  import { lexiconNames } from "../../lexicon-module";
10
10
  import { loadChantConfig } from "../../config";
11
11
  import { build } from "../../build";
12
- import { isResourceDeclarable } from "../../declarable";
12
+ import { isObservableDeclarable } from "../../declarable";
13
13
  import { collectBuildRootContributors, collectChangeSubscribers } from "../plugins";
14
14
  import { discoverOps, discoverStewards } from "../../op/discover";
15
15
  import { pickSteward, stewardBesideOf, stewardFormFor, stewardLeaseName, stewardTurnOps, DEFAULT_STEWARD_ENV } from "../../op/steward";
@@ -116,7 +116,7 @@ async function collectOperatorSubscribers(
116
116
  // subscription may watch.
117
117
  const entities = new Map<string, Map<string, { entityType: string; props: Record<string, unknown> }>>();
118
118
  for (const [name, entity] of buildResult.entities) {
119
- if (!isResourceDeclarable(entity)) continue;
119
+ if (!isObservableDeclarable(entity)) continue;
120
120
  let perLexicon = entities.get(entity.lexicon);
121
121
  if (!perLexicon) entities.set(entity.lexicon, (perLexicon = new Map()));
122
122
  perLexicon.set(name, {
@@ -902,6 +902,7 @@ export async function runApprove(ctx: CommandContext): Promise<number> {
902
902
  allowSameOrigin: ctx.args.allowSameOrigin,
903
903
  ...(ctx.args.roles ? { roles: ctx.args.roles } : {}),
904
904
  ...(ctx.args.agent ? { agent: true } : {}),
905
+ ...(ctx.args.sign !== undefined ? { sign: ctx.args.sign } : {}),
905
906
  });
906
907
  if (!outcome.ok) return 1;
907
908
 
@@ -959,6 +960,15 @@ export interface GateApprovalOptions {
959
960
  * model-authored channel it is one whether or not this is set.
960
961
  */
961
962
  agent?: boolean;
963
+ /**
964
+ * `--sign [<key file>]` (#3163, ws-080): seal the approval with an ssh key,
965
+ * a key file or true for git's `user.signingkey`. The seal attests the
966
+ * approver when the signers file at base lists the key for them, and a gate
967
+ * the declaration's `identity.gates` names counts only sealed approvals.
968
+ */
969
+ sign?: string | true;
970
+ /** Where the workspace and the key file are looked up from. Defaults to the process's directory. */
971
+ cwd?: string;
962
972
  /** Replaces the lexicon evaluator a gate policy is loaded from. For tests. */
963
973
  evaluator?: GatePolicyEvaluator;
964
974
  }
@@ -1107,6 +1117,78 @@ export async function recordGateApproval(
1107
1117
  ...(opts.roles && opts.roles.length > 0 ? { roles: [...new Set(opts.roles)] } : {}),
1108
1118
  };
1109
1119
 
1120
+ // #3163, ws-080: the declaration at base may require a checkable approver
1121
+ // (identity.attribution "identified") and a signed approval for this gate
1122
+ // (identity.gates). Both are judged before anything is written.
1123
+ const cwd = opts.cwd ?? process.cwd();
1124
+ const identity = await import("../../workspace/identity");
1125
+ const { scopeSource } = await import("../../workspace/write-scope");
1126
+ const scope = scopeSource(cwd);
1127
+ try {
1128
+ identity.refuseUnidentified(scope, [resolvedBy], opts.actor !== undefined ? "--actor" : "the approver");
1129
+ } catch (err) {
1130
+ if (!(err instanceof identity.IdentityError)) throw err;
1131
+ console.error(formatError({
1132
+ message: `Gate "${gate}" on "${opName}" was not approved: ${err.message}`,
1133
+ hint: "Pass --actor <forge identity or signer>, as the surface that signed the person in maps them, for example --actor github:<login>.",
1134
+ }));
1135
+ return { ok: false };
1136
+ }
1137
+ const rule = identity.gateAdmissionFrom(scope, gate);
1138
+ if (rule && opts.sign === undefined) {
1139
+ console.error(formatError({
1140
+ message:
1141
+ `Gate "${gate}" needs a signed approval: chant.workspace.json at base names it in identity.gates` +
1142
+ (rule.requirement.class !== null ? `, from the class ${rule.requirement.class}` : "") +
1143
+ `, and this approval is not signed`,
1144
+ hint: `chant approve ${opName} ${gate} --actor <principal> --sign [<key file>], with a key ${scope.policy.signersPath} at base lists for that principal.`,
1145
+ }));
1146
+ return { ok: false };
1147
+ }
1148
+ const timestamp = new Date().toISOString();
1149
+ let seal: GateResolutionRecord["seal"];
1150
+ if (opts.sign !== undefined) {
1151
+ const { resolveSigningKey, sealGateApproval, SealError } = await import("../../workspace/trust/seal");
1152
+ try {
1153
+ const key = resolveSigningKey(opts.sign, cwd);
1154
+ try {
1155
+ seal = sealGateApproval(key.file, {
1156
+ op: opName,
1157
+ gate,
1158
+ ...(environment !== undefined ? { environment } : {}),
1159
+ ...(planDigest !== undefined ? { planDigest } : {}),
1160
+ resolvedBy,
1161
+ timestamp,
1162
+ });
1163
+ } finally {
1164
+ key.cleanup();
1165
+ }
1166
+ } catch (err) {
1167
+ if (!(err instanceof SealError)) throw err;
1168
+ console.error(formatError({ message: `Gate "${gate}" on "${opName}" was not approved: ${err.message}` }));
1169
+ return { ok: false };
1170
+ }
1171
+ }
1172
+ if (rule) {
1173
+ const why = rule.refuses({
1174
+ op: opName,
1175
+ gate,
1176
+ ...(environment !== undefined ? { environment } : {}),
1177
+ ...(planDigest !== undefined ? { planDigest } : {}),
1178
+ resolvedBy,
1179
+ timestamp,
1180
+ seal,
1181
+ });
1182
+ if (why !== null) {
1183
+ console.error(formatError({
1184
+ message: `Gate "${gate}" on "${opName}" was not approved: ${why}`,
1185
+ hint: `identity.gates.${gate} in chant.workspace.json at base counts only an approval signed by a key ${scope.policy.signersPath} lists for the approver` +
1186
+ (rule.requirement.class !== null ? `, who holds the role of the class ${rule.requirement.class}` : "") + ".",
1187
+ }));
1188
+ return { ok: false };
1189
+ }
1190
+ }
1191
+
1110
1192
  // #2508: a gate with a policy has it evaluated for this approval now, while
1111
1193
  // the approver is known, against the context the gated run recorded. The
1112
1194
  // decision is written next to the approval, so a pass can be traced to the
@@ -1148,7 +1230,8 @@ export async function recordGateApproval(
1148
1230
  op: opName,
1149
1231
  gate,
1150
1232
  resolvedBy,
1151
- timestamp: new Date().toISOString(),
1233
+ timestamp,
1234
+ ...(seal ? { seal } : {}),
1152
1235
  approver,
1153
1236
  ...(policyDecision ? { policyDecision } : {}),
1154
1237
  ...(opts.note ? { note: opts.note } : {}),
@@ -1166,6 +1249,7 @@ export async function recordGateApproval(
1166
1249
  console.error(formatSuccess(
1167
1250
  `Gate "${gate}" on "${opName}" resolved by ${record.resolvedBy} at ${record.timestamp}` +
1168
1251
  (record.url ? ` (${record.url})` : "") +
1252
+ (record.seal ? `, signed with ${record.seal.key}` : "") +
1169
1253
  (pushed ? "" : " (local only — the push did not land)"),
1170
1254
  ));
1171
1255
  if (record.planDigest) {
@@ -424,6 +424,30 @@ describe("runOp: a steward's turn (#2750)", () => {
424
424
  expect(acquireStewardTurnMock).not.toHaveBeenCalled();
425
425
  });
426
426
 
427
+ test("fountain form with run.on: fountain and no flag: runs on fountain like --on fountain (#2523)", async () => {
428
+ const runtime = {
429
+ name: "fountain",
430
+ start: vi.fn(async (op: { name: string }) => ({
431
+ op: op.name,
432
+ runId: "f-1",
433
+ result: async () => ({ op: op.name, runId: "f-1", state: "completed" as const, startedAt: "t", endedAt: "t" }),
434
+ })),
435
+ status: vi.fn(), log: vi.fn(), list: vi.fn(), cancel: vi.fn(),
436
+ };
437
+ discoverOpsMock.mockResolvedValue({ ops: new Map([localOp("release", [])]), errors: [] });
438
+ discoverStewardsMock.mockResolvedValueOnce(stewardOwning("release", "box-steward", "fountain"));
439
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["fountain"], run: { on: "fountain" } } });
440
+ makeStdoutSpy();
441
+ const exit = await runOp({
442
+ args: makeArgs({ path: "release" }),
443
+ plugins: [{ name: "fountain", opRuntime: runtime } as never], serializers: [],
444
+ });
445
+
446
+ expect(exit).toBe(0);
447
+ expect(runtime.start).toHaveBeenCalledTimes(1);
448
+ expect(acquireStewardTurnMock).not.toHaveBeenCalled();
449
+ });
450
+
427
451
  test("local form, the turn lease's own acquire throws (a stale .lock): refused as a lease error, not an uncaught exception, and nothing runs", async () => {
428
452
  discoverOpsMock.mockResolvedValue({
429
453
  ops: new Map([localOp("release", [{ kind: "activity", fn: "shellCmd", args: { cmd: "true" } }])]),
@@ -899,6 +923,93 @@ describe("run subcommands on the resolved runtime", () => {
899
923
  expect(stderr.join("\n")).toContain('lexicon "plain" does not host Op runs');
900
924
  });
901
925
 
926
+ test("run.on in chant.config.ts is the default runtime when --on is not given (#2523)", async () => {
927
+ const runtime = makeStubRuntime();
928
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
929
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["stub"], run: { on: "stub" } } });
930
+ makeStdoutSpy();
931
+ makeStderrSpy();
932
+ const ctx = (over: Partial<ParsedArgs>) => ({ args: makeArgs(over), plugins: [stubPlugin(runtime)], serializers: [] });
933
+
934
+ expect(await runOp(ctx({ path: "hello" }))).toBe(0);
935
+ expect(await runOpStatus(ctx({ extraPositional: "hello" }))).toBe(0);
936
+ expect(await runOpLog(ctx({ extraPositional: "hello" }))).toBe(0);
937
+ expect(await runOpList(ctx({}))).toBe(0);
938
+ expect(runtime.start).toHaveBeenCalledTimes(1);
939
+ expect(runtime.status).toHaveBeenCalledWith("hello");
940
+ expect(runtime.log).toHaveBeenCalledTimes(1);
941
+ expect(runtime.list).toHaveBeenCalledTimes(1);
942
+ });
943
+
944
+ test("--on local overrides run.on and runs on the built-in runtime (#2523)", async () => {
945
+ const runtime = makeStubRuntime();
946
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
947
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["stub"], run: { on: "stub" } } });
948
+ makeStdoutSpy();
949
+ makeStderrSpy();
950
+ await runOpList({ args: makeArgs({ on: "local" }), plugins: [stubPlugin(runtime)], serializers: [] });
951
+ expect(runtime.list).not.toHaveBeenCalled();
952
+ });
953
+
954
+ test("run.on naming an unconfigured lexicon gets --on's refusal, naming run.on (#2523)", async () => {
955
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
956
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["aws"], run: { on: "nope" } } });
957
+ const stderr = makeStderrSpy();
958
+ const exit = await runOp({ args: makeArgs({ path: "hello" }), plugins: [], serializers: [] });
959
+ expect(exit).toBe(1);
960
+ expect(stderr.join("\n")).toContain('run.on "nope" in chant.config.ts: "nope" is not a configured lexicon');
961
+ expect(stderr.join("\n")).toContain("Configured lexicons: aws");
962
+ expect(stderr.join("\n")).toContain("--on local");
963
+ });
964
+
965
+ test("an explicit --on wins over run.on (#2523)", async () => {
966
+ const chosen = makeStubRuntime();
967
+ const other = makeStubRuntime();
968
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
969
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["stub", "other"], run: { on: "other" } } });
970
+ makeStdoutSpy();
971
+ makeStderrSpy();
972
+ const plugins = [stubPlugin(chosen), { name: "other", opRuntime: other } as never];
973
+ expect(await runOpList({ args: makeArgs({ on: "stub" }), plugins, serializers: [] })).toBe(0);
974
+ expect(chosen.list).toHaveBeenCalledTimes(1);
975
+ expect(other.list).not.toHaveBeenCalled();
976
+ });
977
+
978
+ test("run.on: local selects the built-in runtime (#2523)", async () => {
979
+ const runtime = makeStubRuntime();
980
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
981
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["stub"], run: { on: "local" } } });
982
+ makeStdoutSpy();
983
+ makeStderrSpy();
984
+ await runOpList({ args: makeArgs({}), plugins: [stubPlugin(runtime)], serializers: [] });
985
+ expect(runtime.list).not.toHaveBeenCalled();
986
+ });
987
+
988
+ test("an invalid run block is refused, not run locally (#2523)", async () => {
989
+ const { InvalidChantConfigError } = await vi.importActual<typeof import("../../config")>("../../config");
990
+ const runtime = makeStubRuntime();
991
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
992
+ loadChantConfigMock.mockRejectedValue(
993
+ new InvalidChantConfigError("Invalid chant config in /p/chant.config.ts: run.on: Expected string, received number", "run"),
994
+ );
995
+ const stderr = makeStderrSpy();
996
+ makeStdoutSpy();
997
+ expect(await runOpList({ args: makeArgs({}), plugins: [stubPlugin(runtime)], serializers: [] })).toBe(1);
998
+ expect(stderr.join("\n")).toContain("Invalid chant config in /p/chant.config.ts: run.on");
999
+ expect(runtime.list).not.toHaveBeenCalled();
1000
+ // --on local still runs: the flag needs nothing from the config.
1001
+ expect(await runOpList({ args: makeArgs({ on: "local" }), plugins: [stubPlugin(runtime)], serializers: [] })).toBe(0);
1002
+ });
1003
+
1004
+ test("run.on naming a lexicon with no opRuntime gets --on's refusal, naming run.on (#2523)", async () => {
1005
+ discoverOpsMock.mockResolvedValue({ ops: new Map([makeOp("hello")]), errors: [] });
1006
+ loadChantConfigMock.mockResolvedValue({ config: { lexicons: ["plain"], run: { on: "plain" } } });
1007
+ const stderr = makeStderrSpy();
1008
+ const exit = await runOp({ args: makeArgs({ path: "hello" }), plugins: [{ name: "plain" } as never], serializers: [] });
1009
+ expect(exit).toBe(1);
1010
+ expect(stderr.join("\n")).toContain('run.on "plain" in chant.config.ts: lexicon "plain" does not host Op runs');
1011
+ });
1012
+
902
1013
  test("--components on a runtime that cannot host them → one line, exit 1", async () => {
903
1014
  const runtime = makeStubRuntime();
904
1015
  const stderr = makeStderrSpy();