@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
@@ -370,14 +370,15 @@ export interface PackageKinds extends KindData {
370
370
  }
371
371
 
372
372
  /**
373
- * Where a package's `exports` send `./workspace-kinds`, or undefined when
374
- * the package doesn't export that subpath. Only the literal key counts: a
375
- * `./*` pattern maps the subpath to code, which chant never runs for kinds.
373
+ * Where a package's `exports` send `./workspace-kinds` (or another data
374
+ * subpath, such as `./workspace-principals`), or undefined when the package
375
+ * doesn't export it. Only the literal key counts: a `./*` pattern maps the
376
+ * subpath to code, which chant never runs for plugin data.
376
377
  */
377
- export function kindsExportTarget(pkg: Record<string, unknown>): string | undefined | { problem: string } {
378
+ export function kindsExportTarget(pkg: Record<string, unknown>, subpath: string = KINDS_SUBPATH): string | undefined | { problem: string } {
378
379
  const exports = pkg.exports;
379
380
  if (!exports || typeof exports !== "object" || Array.isArray(exports)) return undefined;
380
- const entry = (exports as Record<string, unknown>)[KINDS_SUBPATH];
381
+ const entry = (exports as Record<string, unknown>)[subpath];
381
382
  if (entry === undefined) return undefined;
382
383
  let target: unknown = entry;
383
384
  if (target && typeof target === "object" && !Array.isArray(target)) {
@@ -385,10 +386,10 @@ export function kindsExportTarget(pkg: Record<string, unknown>): string | undefi
385
386
  target = conditions.default ?? conditions.import ?? conditions.require;
386
387
  }
387
388
  if (typeof target !== "string") {
388
- return { problem: `exports["${KINDS_SUBPATH}"] must name one file, as a string or a "default" condition` };
389
+ return { problem: `exports["${subpath}"] must name one file, as a string or a "default" condition` };
389
390
  }
390
391
  if (!target.startsWith("./") || !target.endsWith(".json")) {
391
- return { problem: `exports["${KINDS_SUBPATH}"] is ${target}; it must name a .json file inside the package, since chant reads kinds as data and never runs a package's code` };
392
+ return { problem: `exports["${subpath}"] is ${target}; it must name a .json file inside the package, since chant reads plugin data as data and never runs a package's code` };
392
393
  }
393
394
  return target;
394
395
  }
@@ -499,6 +500,37 @@ export interface LoadedKinds {
499
500
  problems: KindLoadProblem[];
500
501
  }
501
502
 
503
+ /**
504
+ * The directory a pin's plugin data is read from: an installed package at
505
+ * the pinned version, looked up from `workspaceRoot`, or a path pin's
506
+ * directory, checked against its integrity digest when it has one. Null for
507
+ * a pin that names neither. Shared by the kinds and the principal classes
508
+ * (#3080), so both read a pin the same way.
509
+ */
510
+ export function pinnedPackageDir(pin: PinLike, workspaceRoot: string): { dir: string; source: string } | { problem: string } | null {
511
+ if (pin.package) {
512
+ const dir = findInstalledPackage(pin.package, workspaceRoot);
513
+ if (!dir) return { problem: `pinned package ${pin.package} is not installed; install it to read the kinds it supplies` };
514
+ let installed: unknown;
515
+ try {
516
+ installed = (JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")) as { version?: unknown }).version;
517
+ } catch {
518
+ installed = undefined;
519
+ }
520
+ if (pin.version && installed !== pin.version) return { problem: `${pin.package} is pinned at ${pin.version}, and ${String(installed ?? "an unknown version")} is installed` };
521
+ return { dir, source: pin.package };
522
+ }
523
+ if (pin.path) {
524
+ const dir = join(workspaceRoot, ...pin.path.split("/"));
525
+ if (pin.integrity) {
526
+ const checked = checkPinIntegrity(dir, pin.integrity, pin.path);
527
+ if (!checked.ok) return { problem: checked.message };
528
+ }
529
+ return { dir, source: pin.path };
530
+ }
531
+ return null;
532
+ }
533
+
502
534
  /**
503
535
  * Build the registry for a workspace: the built-in kinds plus the kinds
504
536
  * every pin publishes. `workspaceRoot` is the absolute workspace root; a
@@ -514,38 +546,13 @@ export function loadKindRegistry(pins: readonly PinLike[], workspaceRoot: string
514
546
  const owner = new Map<string, string>();
515
547
  const clashing = new Set<string>();
516
548
  pins.forEach((pin, i) => {
517
- let dir: string | undefined;
518
- let source: string;
519
- if (pin.package) {
520
- source = pin.package;
521
- dir = findInstalledPackage(pin.package, workspaceRoot);
522
- if (!dir) {
523
- problems.push({ pin: i, message: `pinned package ${pin.package} is not installed; install it to read the kinds it supplies` });
524
- return;
525
- }
526
- let installed: unknown;
527
- try {
528
- installed = (JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")) as { version?: unknown }).version;
529
- } catch {
530
- installed = undefined;
531
- }
532
- if (pin.version && installed !== pin.version) {
533
- problems.push({ pin: i, message: `${pin.package} is pinned at ${pin.version}, and ${String(installed ?? "an unknown version")} is installed` });
534
- return;
535
- }
536
- } else if (pin.path) {
537
- source = pin.path;
538
- dir = join(workspaceRoot, ...pin.path.split("/"));
539
- if (pin.integrity) {
540
- const checked = checkPinIntegrity(dir, pin.integrity, pin.path);
541
- if (!checked.ok) {
542
- problems.push({ pin: i, message: checked.message });
543
- return;
544
- }
545
- }
546
- } else {
549
+ const found = pinnedPackageDir(pin, workspaceRoot);
550
+ if (found === null) return;
551
+ if ("problem" in found) {
552
+ problems.push({ pin: i, message: found.problem });
547
553
  return;
548
554
  }
555
+ const { dir, source } = found;
549
556
  const read = readPackageKinds(dir, source);
550
557
  for (const p of read.problems) problems.push({ pin: i, message: p });
551
558
  for (const k of read.kinds) {
@@ -473,6 +473,7 @@
473
473
  "answer-not-candidate",
474
474
  "quorum-not-met",
475
475
  "answer-in-steward-turn",
476
+ "principal-unidentified",
476
477
  "record-not-found",
477
478
  "record-closed",
478
479
  "record-id-taken",
@@ -0,0 +1,175 @@
1
+ /**
2
+ * Principal classes a plugin supplies (#3080, ws-079): the data file, the
3
+ * registry, and how a principal's role grants at base put it in a class.
4
+ */
5
+
6
+ import { mkdirSync, writeFileSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { afterAll, describe, expect, test } from "vitest";
9
+ import { cleanScratch, scratchDir } from "./__fixtures__/contract-repo";
10
+ import {
11
+ CLASS_NAME,
12
+ classesOf,
13
+ coreClassRegistry,
14
+ loadClassRegistry,
15
+ parseClassData,
16
+ principalClass,
17
+ PRINCIPALS_SCHEMA_ID,
18
+ readPackageClasses,
19
+ treeFiles,
20
+ unknownScopeClasses,
21
+ } from "./principal-classes";
22
+ import { emptyPolicy } from "./trust/policy";
23
+ import type { WorkspaceTree } from "./tree";
24
+
25
+ afterAll(cleanScratch);
26
+
27
+ const data = (classes: unknown[]) => JSON.stringify({ schema: 1, classes });
28
+ const reviewer = { name: "reviewer", description: "people who review decisions", role: "reviewer" };
29
+ const operator = { name: "operator", description: "people who apply infrastructure", role: "ops" };
30
+
31
+ /** A plugin package directory on disk, publishing `classes` at ./workspace-principals. */
32
+ function plugin(root: string, dir: string, classes: unknown[], pkg: Record<string, unknown> = {}): void {
33
+ const abs = join(root, ...dir.split("/"));
34
+ mkdirSync(abs, { recursive: true });
35
+ writeFileSync(join(abs, "package.json"), JSON.stringify({ name: dir.replace(/\//g, "-"), version: "1.0.0", exports: { "./workspace-principals": "./principals.json" }, ...pkg }));
36
+ writeFileSync(join(abs, "principals.json"), data(classes));
37
+ }
38
+
39
+ describe("the principals file", () => {
40
+ test("has a versioned schema id and a name grammar shared with writeScope", () => {
41
+ expect(PRINCIPALS_SCHEMA_ID).toBe("https://intentius.io/chant/schemas/workspace/principals/v1/workspace-principals.schema.json");
42
+ expect(CLASS_NAME.test("reviewer")).toBe(true);
43
+ expect(CLASS_NAME.test("x-reviewer")).toBe(false);
44
+ expect(CLASS_NAME.test("Reviewer")).toBe(false);
45
+ });
46
+
47
+ test("a valid file yields its classes in file order, with the pin as source", () => {
48
+ expect(parseClassData(data([reviewer, operator]), "plugins/review")).toEqual({
49
+ classes: [
50
+ { ...reviewer, source: "plugins/review" },
51
+ { ...operator, source: "plugins/review" },
52
+ ],
53
+ problems: [],
54
+ });
55
+ });
56
+
57
+ test.each([
58
+ ["not JSON", "{", /not JSON/],
59
+ ["a missing role", data([{ name: "reviewer", description: "d" }]), /role/],
60
+ ["an unknown field", data([{ ...reviewer, members: ["app"] }]), /additional properties/],
61
+ ["an x- name", data([{ ...reviewer, name: "x-reviewer" }]), /pattern/],
62
+ ])("refuses %s", (_, text, message) => {
63
+ const read = parseClassData(text, "p");
64
+ expect(read.classes).toEqual([]);
65
+ expect(read.problems.join("\n")).toMatch(message);
66
+ });
67
+
68
+ test("leaves out a core name, a core role, a name twice and a role twice, keeping the rest", () => {
69
+ const read = parseClassData(
70
+ data([
71
+ { name: "human", description: "d", role: "people" },
72
+ { name: "bots", description: "d", role: "agent" },
73
+ reviewer,
74
+ { ...reviewer, role: "other" },
75
+ { name: "checker", description: "d", role: "reviewer" },
76
+ operator,
77
+ ]),
78
+ "p",
79
+ );
80
+ expect(read.classes.map((c) => c.name)).toEqual(["reviewer", "operator"]);
81
+ expect(read.problems).toEqual([
82
+ "p: class human is a core class and can't be supplied by a package",
83
+ "p: class bots names the role agent, which puts a principal in the core agent class",
84
+ "p: class reviewer is listed twice",
85
+ "p: class checker names the role reviewer, which another class in the file names; a role puts a principal in one class",
86
+ ]);
87
+ });
88
+ });
89
+
90
+ describe("reading a package's classes", () => {
91
+ test("a package exporting no ./workspace-principals supplies none, without a problem", () => {
92
+ const root = scratchDir();
93
+ plugin(root, "kinds-only", [reviewer], { exports: { "./workspace-kinds": "./kinds.json" } });
94
+ expect(loadClassRegistry([{ package: null, version: null, path: "kinds-only" }], root)).toMatchObject({ problems: [] });
95
+ });
96
+
97
+ test("the export must name a JSON file inside the package", () => {
98
+ const files = (pkg: object) => (p: string) => (p === "package.json" ? JSON.stringify(pkg) : undefined);
99
+ expect(readPackageClasses(files({ exports: { "./workspace-principals": "./index.js" } }), "p").problems[0]).toMatch(/must name a .json file/);
100
+ expect(readPackageClasses(files({ exports: { "./workspace-principals": "./../x.json" } }), "p").problems[0]).toMatch(/points outside the package/);
101
+ expect(readPackageClasses(files({ exports: { "./workspace-principals": "./gone.json" } }), "p").problems[0]).toMatch(/does not exist/);
102
+ });
103
+
104
+ test("a path pin is read through a tree when one is given, such as the base revision", () => {
105
+ const files: Record<string, string> = {
106
+ "plugins/review/package.json": JSON.stringify({ exports: { "./workspace-principals": "./principals.json" } }),
107
+ "plugins/review/principals.json": data([reviewer]),
108
+ };
109
+ const tree: WorkspaceTree = {
110
+ label: " at base",
111
+ stat: (p) => (p in files ? "file" : undefined),
112
+ list: () => undefined,
113
+ read: (p) => files[p],
114
+ };
115
+ expect(readPackageClasses(treeFiles(tree, "plugins/review"), "plugins/review").classes.map((c) => c.name)).toEqual(["reviewer"]);
116
+ // The working tree is not consulted: nothing exists on disk at this root.
117
+ const loaded = loadClassRegistry([{ package: null, version: null, path: "plugins/review" }], "/nonexistent", { tree });
118
+ expect(loaded.problems).toEqual([]);
119
+ expect(loaded.registry.names()).toEqual(["human", "agent", "runner", "service", "reviewer"]);
120
+ });
121
+
122
+ test("a class name or a role two pins supply is left out, with a problem on the second pin", () => {
123
+ const root = scratchDir();
124
+ plugin(root, "a", [reviewer, operator]);
125
+ plugin(root, "b", [{ ...reviewer, role: "approver" }]);
126
+ plugin(root, "c", [{ name: "applier", description: "d", role: "ops" }]);
127
+ const pins = ["a", "b", "c"].map((path) => ({ package: null, version: null, path }));
128
+ const { registry, problems } = loadClassRegistry(pins, root);
129
+ expect(registry.names()).toEqual(["human", "agent", "runner", "service"]);
130
+ expect(problems).toEqual([
131
+ { pin: 1, message: "class reviewer is supplied by both a and b; a class name has one source" },
132
+ { pin: 2, message: "class applier (c) names the role ops, which class operator (a) names; a role puts a principal in one class" },
133
+ ]);
134
+ });
135
+
136
+ test("a package pin that is not installed is a problem, and a pin naming neither is skipped", () => {
137
+ const root = scratchDir();
138
+ const { problems } = loadClassRegistry(
139
+ [
140
+ { package: "absent", version: "1.0.0", path: null },
141
+ { package: null, version: null, path: null },
142
+ ],
143
+ root,
144
+ );
145
+ expect(problems).toEqual([{ pin: 0, message: "pinned package absent is not installed; install it to read the kinds it supplies" }]);
146
+ });
147
+ });
148
+
149
+ describe("a principal's classes", () => {
150
+ const root = scratchDir();
151
+ plugin(root, "review", [reviewer, operator]);
152
+ const { registry } = loadClassRegistry([{ package: null, version: null, path: "review" }], root);
153
+ const policy = { ...emptyPolicy("x"), roles: { reviewer: ["Rev@example.com", "ci@example.com"], ops: ["rev@example.com"], runner: ["ci@example.com"] } };
154
+
155
+ test("come from role grants at base, core first, then the domain classes in order", () => {
156
+ expect(classesOf(registry, policy, "rev@example.com")).toEqual(["reviewer", "operator"]);
157
+ expect(classesOf(registry, policy, "ci@example.com")).toEqual(["runner", "reviewer"]);
158
+ expect(classesOf(registry, policy, "lex00")).toEqual([]);
159
+ expect(classesOf(registry, policy, null)).toEqual([]);
160
+ });
161
+
162
+ test("write scope judges a principal by the first, and human is the rest", () => {
163
+ expect(principalClass(policy, "rev@example.com", registry)).toBe("reviewer");
164
+ expect(principalClass(policy, "ci@example.com", registry)).toBe("runner");
165
+ expect(principalClass(policy, "lex00", registry)).toBe("human");
166
+ // Without the plugin's classes, a domain principal is a human (the behaviour before #3080).
167
+ expect(principalClass(policy, "rev@example.com")).toBe("human");
168
+ });
169
+
170
+ test("a writeScope key no class has is unknown", () => {
171
+ expect(unknownScopeClasses({ human: {}, reviewer: {}, auditor: {} }, registry)).toEqual(["auditor"]);
172
+ expect(unknownScopeClasses({ reviewer: {} }, coreClassRegistry())).toEqual(["reviewer"]);
173
+ expect(unknownScopeClasses(null, registry)).toEqual([]);
174
+ });
175
+ });
@@ -0,0 +1,307 @@
1
+ /**
2
+ * Principal classes (#2524 D5; #3080, ws-079).
3
+ *
4
+ * A principal class names a set of principals so the declaration can scope
5
+ * what they write. Four are core: `agent`, `runner` and `service`, each the
6
+ * holders of the role of that name in the trust policy at base, and `human`,
7
+ * everyone else. A plugin adds domain classes as data, the way it adds member
8
+ * kinds: a JSON file at its `./workspace-principals` subpath, read through
9
+ * the package's `package.json` and never imported. Each class it supplies
10
+ * names the role whose grant puts a principal in it.
11
+ *
12
+ * A principal's classes are read only from role grants at base, through
13
+ * {@link classesOf}. Core classes are tried first, then the domain classes
14
+ * in pin order and file order; the first that holds is the class write scope
15
+ * judges the principal as, and `human` is the rest. A gate or a record that
16
+ * asks whether a principal is in a class (#3163) asks {@link classesOf},
17
+ * which lists every class the principal's grants put it in.
18
+ *
19
+ * A path-pinned plugin's classes are read from the base revision when the
20
+ * caller has it (write scope does), so a change can't remap a class by
21
+ * editing its own copy of the plugin. A package pin is read from the
22
+ * installed package, which must be at the version the base pins.
23
+ */
24
+
25
+ import { readFileSync } from "node:fs";
26
+ import { createRequire } from "node:module";
27
+ import { join, posix } from "node:path";
28
+ import { kindsExportTarget, pinnedPackageDir, type KindLoadProblem, type PinLike } from "./kinds";
29
+ import { normalisePrincipal } from "./records";
30
+ import schema from "./workspace-principals.schema.json";
31
+ import type { TrustPolicy } from "./trust/policy";
32
+ import { joinPath, type WorkspaceTree } from "./tree";
33
+
34
+ export const PRINCIPALS_SCHEMA_ID = schema.$id;
35
+
36
+ /** The subpath a package publishes its principal classes at. */
37
+ export const PRINCIPALS_SUBPATH = "./workspace-principals";
38
+
39
+ /** The core classes, in the order a principal's roles are tried; `human` holds no role and is the rest. */
40
+ export const CORE_PRINCIPAL_CLASSES = ["human", "agent", "runner", "service"] as const;
41
+ export type CorePrincipalClass = (typeof CORE_PRINCIPAL_CLASSES)[number];
42
+
43
+ /** A class name: a core class, or a domain class a pinned package supplies. */
44
+ export type PrincipalClassName = string;
45
+
46
+ /** The grammar of a class name, as `workspace-principals.schema.json` and the declaration's writeScope keys say it. */
47
+ export const CLASS_NAME = /^(?!x-)[a-z][a-z0-9-]{0,39}$/;
48
+
49
+ export interface PrincipalClass {
50
+ name: string;
51
+ /** One line saying who is in the class. */
52
+ description: string;
53
+ /** The role whose grant at base puts a principal in the class, or null for `human`, the rest. */
54
+ role: string | null;
55
+ /** `core`, or the pin (package name or path) that supplies the class. */
56
+ source: string;
57
+ }
58
+
59
+ export const CORE_CLASSES: readonly PrincipalClass[] = [
60
+ { name: "human", description: "a principal holding none of the roles of another class", role: null, source: "core" },
61
+ { name: "agent", description: "a principal holding the agent role, or a write naming an agent session", role: "agent", source: "core" },
62
+ { name: "runner", description: "a principal holding the runner role, such as a CI job's signing identity", role: "runner", source: "core" },
63
+ { name: "service", description: "a principal holding the service role", role: "service", source: "core" },
64
+ ];
65
+
66
+ const CORE_ROLES = new Set(CORE_CLASSES.map((c) => c.role).filter((r): r is string => r !== null));
67
+
68
+ export interface ClassRegistry {
69
+ get(name: string): PrincipalClass | undefined;
70
+ /** Every class name, core first, for "unknown class" messages. */
71
+ names(): string[];
72
+ /** Every class, core first, then the domain classes in pin order and file order. */
73
+ all(): readonly PrincipalClass[];
74
+ }
75
+
76
+ export function createClassRegistry(extra: readonly PrincipalClass[] = []): ClassRegistry {
77
+ const all = [...CORE_CLASSES, ...extra];
78
+ const byName = new Map(all.map((c) => [c.name, c]));
79
+ return {
80
+ get: (name) => byName.get(name),
81
+ names: () => all.map((c) => c.name),
82
+ all: () => all,
83
+ };
84
+ }
85
+
86
+ /** The four core classes and nothing else: what a workspace whose pins supply no class has. */
87
+ export function coreClassRegistry(): ClassRegistry {
88
+ return createClassRegistry();
89
+ }
90
+
91
+ /**
92
+ * The role grants a class is read from. Every read of roles for a class goes
93
+ * through here, so where the grants live (`.chant/trust.json` today,
94
+ * possibly the declaration, #2547) is decided in one place: the policy.
95
+ */
96
+ export function roleGrants(policy: TrustPolicy): Record<string, string[]> {
97
+ return policy.roles;
98
+ }
99
+
100
+ /**
101
+ * Every class `principal`'s role grants at base put it in, in registry order
102
+ * (core first). Empty for a principal in no class but `human`, and for null.
103
+ * Names compare as {@link normalisePrincipal} folds them.
104
+ */
105
+ export function classesOf(classes: ClassRegistry, policy: TrustPolicy, principal: string | null): string[] {
106
+ if (principal === null) return [];
107
+ const name = normalisePrincipal(principal);
108
+ const grants = roleGrants(policy);
109
+ return classes
110
+ .all()
111
+ .filter((c) => c.role !== null && (grants[c.role] ?? []).some((p) => normalisePrincipal(p) === name))
112
+ .map((c) => c.name);
113
+ }
114
+
115
+ /** The class write scope judges `principal` as: the first class its grants put it in, or `human`. */
116
+ export function principalClass(policy: TrustPolicy, principal: string | null, classes: ClassRegistry = coreClassRegistry()): string {
117
+ return classesOf(classes, policy, principal)[0] ?? "human";
118
+ }
119
+
120
+ // ── Reading a plugin's classes ───────────────────────────────────────────────
121
+
122
+ interface AjvError {
123
+ instancePath: string;
124
+ message?: string;
125
+ }
126
+ type Validate = ((data: unknown) => boolean) & { errors?: AjvError[] | null };
127
+
128
+ let compiled: Validate | undefined;
129
+ function validator(): Validate {
130
+ if (compiled) return compiled;
131
+ const require = createRequire(import.meta.url);
132
+ // ajv is CommonJS; its class is the default export, or that export's own default.
133
+ const mod = require("ajv/dist/2020") as { default?: unknown };
134
+ const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): Validate };
135
+ compiled = new Ajv({ allErrors: true, strict: true }).compile(schema);
136
+ return compiled;
137
+ }
138
+
139
+ export interface ClassData {
140
+ classes: PrincipalClass[];
141
+ /** Why some of the data can't be used, one line each. Empty when all of it can. */
142
+ problems: string[];
143
+ }
144
+
145
+ /**
146
+ * Check the text of a principals file against
147
+ * `workspace-principals.schema.json` and the rules the schema can't say: no
148
+ * core name or core role, and no name or role twice. `source` names the pin
149
+ * in the classes and in the messages.
150
+ */
151
+ export function parseClassData(text: string, source: string): ClassData {
152
+ let raw: unknown;
153
+ try {
154
+ raw = JSON.parse(text);
155
+ } catch (err) {
156
+ return { classes: [], problems: [`${source}: the principals file is not JSON (${(err as Error).message})`] };
157
+ }
158
+ const validate = validator();
159
+ if (!validate(raw)) {
160
+ const problems = (validate.errors ?? []).map((e) => `${source}: ${e.instancePath || "the principals file"} ${e.message ?? "is invalid"}`);
161
+ return { classes: [], problems: [...new Set(problems)] };
162
+ }
163
+ const problems: string[] = [];
164
+ const classes: PrincipalClass[] = [];
165
+ const names = new Set<string>();
166
+ const roles = new Set<string>();
167
+ for (const c of (raw as { classes: { name: string; description: string; role: string }[] }).classes) {
168
+ if ((CORE_PRINCIPAL_CLASSES as readonly string[]).includes(c.name)) {
169
+ problems.push(`${source}: class ${c.name} is a core class and can't be supplied by a package`);
170
+ continue;
171
+ }
172
+ if (CORE_ROLES.has(c.role)) {
173
+ problems.push(`${source}: class ${c.name} names the role ${c.role}, which puts a principal in the core ${c.role} class`);
174
+ continue;
175
+ }
176
+ if (names.has(c.name)) {
177
+ problems.push(`${source}: class ${c.name} is listed twice`);
178
+ continue;
179
+ }
180
+ if (roles.has(c.role)) {
181
+ problems.push(`${source}: class ${c.name} names the role ${c.role}, which another class in the file names; a role puts a principal in one class`);
182
+ continue;
183
+ }
184
+ names.add(c.name);
185
+ roles.add(c.role);
186
+ classes.push({ name: c.name, description: c.description, role: c.role, source });
187
+ }
188
+ return { classes, problems };
189
+ }
190
+
191
+ /** A package's files, by a `/`-separated path inside it, or undefined when one is not there. */
192
+ export type PackageFiles = (path: string) => string | undefined;
193
+
194
+ /** A package directory on disk. */
195
+ export function diskFiles(dir: string): PackageFiles {
196
+ return (path) => {
197
+ try {
198
+ return readFileSync(join(dir, ...path.split("/")), "utf-8");
199
+ } catch {
200
+ return undefined;
201
+ }
202
+ };
203
+ }
204
+
205
+ /** A package directory at `dir` in `tree`, such as the workspace at the base revision. */
206
+ export function treeFiles(tree: WorkspaceTree, dir: string): PackageFiles {
207
+ return (path) => {
208
+ const full = joinPath(dir, path);
209
+ if (tree.stat(full) !== "file") return undefined;
210
+ try {
211
+ return tree.read(full);
212
+ } catch {
213
+ return undefined;
214
+ }
215
+ };
216
+ }
217
+
218
+ /** Read the classes a package publishes at `./workspace-principals`. Reads files only; imports nothing. A package that publishes none supplies none. */
219
+ export function readPackageClasses(files: PackageFiles, source: string): ClassData {
220
+ const manifest = files("package.json");
221
+ if (manifest === undefined) return { classes: [], problems: [`${source}: no readable package.json`] };
222
+ let pkg: Record<string, unknown>;
223
+ try {
224
+ pkg = JSON.parse(manifest) as Record<string, unknown>;
225
+ } catch {
226
+ return { classes: [], problems: [`${source}: package.json is not JSON`] };
227
+ }
228
+ const target = kindsExportTarget(pkg, PRINCIPALS_SUBPATH);
229
+ if (target === undefined) return { classes: [], problems: [] };
230
+ if (typeof target !== "string") return { classes: [], problems: [`${source}: ${target.problem}`] };
231
+ const inside = posix.normalize(target.slice(2));
232
+ if (inside.startsWith("../") || inside === "..") return { classes: [], problems: [`${source}: exports["${PRINCIPALS_SUBPATH}"] points outside the package`] };
233
+ const text = files(inside);
234
+ if (text === undefined) return { classes: [], problems: [`${source}: exports["${PRINCIPALS_SUBPATH}"] names ${target}, which does not exist`] };
235
+ return parseClassData(text, source);
236
+ }
237
+
238
+ export interface LoadedClasses {
239
+ registry: ClassRegistry;
240
+ problems: KindLoadProblem[];
241
+ }
242
+
243
+ /**
244
+ * The classes a workspace knows: the core four plus every class its pins
245
+ * supply. A package pin is read from the installed package, looked up from
246
+ * `workspaceRoot`, and must be at the pinned version. A path pin is read
247
+ * through `tree` when given (the base revision, for write scope), else from
248
+ * `workspaceRoot` on disk, checked against its integrity digest. A class
249
+ * name, or a role, two pins both supply is a problem, and every class
250
+ * involved is left out, so a writeScope entry for it reads as unknown and
251
+ * fails closed. Never throws.
252
+ */
253
+ export function loadClassRegistry(pins: readonly PinLike[], workspaceRoot: string, opts: { tree?: WorkspaceTree } = {}): LoadedClasses {
254
+ const problems: KindLoadProblem[] = [];
255
+ const extra: PrincipalClass[] = [];
256
+ const byName = new Map<string, string>();
257
+ const byRole = new Map<string, string>();
258
+ const clashing = new Set<string>();
259
+ pins.forEach((pin, i) => {
260
+ let files: PackageFiles;
261
+ let source: string;
262
+ if (pin.path && opts.tree) {
263
+ files = treeFiles(opts.tree, pin.path);
264
+ source = pin.path;
265
+ } else {
266
+ const found = pinnedPackageDir(pin, workspaceRoot);
267
+ if (found === null) return;
268
+ if ("problem" in found) {
269
+ problems.push({ pin: i, message: found.problem });
270
+ return;
271
+ }
272
+ files = diskFiles(found.dir);
273
+ source = found.source;
274
+ }
275
+ const read = readPackageClasses(files, source);
276
+ for (const p of read.problems) problems.push({ pin: i, message: p });
277
+ for (const c of read.classes) {
278
+ const named = byName.get(c.name);
279
+ const roled = byRole.get(c.role!);
280
+ if (named !== undefined || roled !== undefined) {
281
+ clashing.add(c.name);
282
+ if (named !== undefined) {
283
+ problems.push({ pin: i, message: `class ${c.name} is supplied by both ${named} and ${source}; a class name has one source` });
284
+ } else {
285
+ const other = extra.find((e) => e.role === c.role)!;
286
+ clashing.add(other.name);
287
+ problems.push({ pin: i, message: `class ${c.name} (${source}) names the role ${c.role}, which class ${other.name} (${roled}) names; a role puts a principal in one class` });
288
+ }
289
+ continue;
290
+ }
291
+ byName.set(c.name, source);
292
+ byRole.set(c.role!, source);
293
+ extra.push(c);
294
+ }
295
+ });
296
+ // A name or role two pins both supply is left out altogether: neither reading is safe.
297
+ return { registry: createClassRegistry(extra.filter((c) => !clashing.has(c.name))), problems };
298
+ }
299
+
300
+ /**
301
+ * The writeScope keys no class in `classes` has, in file order. A writer
302
+ * judged `human` may be in such a class, since nothing says which role puts
303
+ * a principal in it, so write scope refuses that writer (fail closed).
304
+ */
305
+ export function unknownScopeClasses(writeScope: Record<string, unknown> | null, classes: ClassRegistry): string[] {
306
+ return Object.keys(writeScope ?? {}).filter((name) => classes.get(name) === undefined);
307
+ }
@@ -14,7 +14,7 @@ import { MEMBER_RUN_REASON_CODES } from "./compose-graph";
14
14
  import { COMPOSITES_ENVIRONMENT_REASON_CODES, COMPOSITES_ERROR_CODES, COMPOSITES_REASON_CODES, COMPOSITES_RUNTIME_REASON_CODES } from "./composites";
15
15
  import { WORKSPACE_ERROR_CODES } from "./declaration";
16
16
  import { GRAPH_ERROR_CODES } from "./graph-cli";
17
- import { INTENT_ERROR_CODES, INTENT_FINDING_CODES, INTENT_REASON_CODES } from "./intent";
17
+ import { INTENT_ERROR_CODES, INTENT_FINDING_CODES, INTENT_REASON_CODES, INTENT_WHY_CODES } from "./intent";
18
18
  import { CHECK_CODES, CHECK_ERROR_CODES } from "./lineage-check";
19
19
  import { GROUP_REASON_CODES, MEMBER_REASON_CODES } from "./ls";
20
20
  import intentSchema from "./intent.schema.json";
@@ -29,6 +29,7 @@ import { WORK_WARNING_CODES } from "./work";
29
29
  import { BOX_FINDING_CODES } from "./checks/boxes";
30
30
  import { DIAGRAM_FINDING_CODES } from "./checks/diagrams";
31
31
  import { WORK_ERROR_CODES, WORK_HISTORY_ERROR_CODES, WORK_LEASE_REFUSALS } from "./work-cli";
32
+ import { RUNS_ERROR_CODES, RUNS_REASON_CODES, RUNS_WRITE_ERROR_CODES } from "./runs-cli";
32
33
  import { WORK_EVIDENCE_ERROR_CODES } from "./work-evidence";
33
34
  import { RECORD_FINDING_CODES } from "./checks/records";
34
35
  import { ANSWER_WARNING_CODES } from "./points";
@@ -36,7 +37,8 @@ import { POINTS_ERROR_CODES, POINTS_SOURCE_REASON_CODES } from "./points-cli";
36
37
  import { POINTS_WRITE_ERROR_CODES } from "./decide";
37
38
  import { PATCH_ERROR_CODES } from "./patch";
38
39
  import { AGENT_ERROR_CODES } from "./agent-cli";
39
- import { WRITE_SCOPE_CODES } from "./write-scope";
40
+ import { SCOPE_FINDING_CODES, WRITE_SCOPE_CODES } from "./write-scope";
41
+ import { PLANTABLE_REASON_CODES } from "./box-factory";
40
42
  import { ROTATION_REFUSAL_CODES } from "./trust/rotation";
41
43
  import { SIGNERS_ERROR_CODES } from "./trust/signers-cli";
42
44
  import { EVIDENCE_ERROR_CODES } from "./trust/evidence";
@@ -73,6 +75,7 @@ const PER_COMMAND: Record<string, readonly string[]> = {
73
75
  INTENT_ERROR_CODES,
74
76
  INTENT_FINDING_CODES,
75
77
  INTENT_REASON_CODES,
78
+ INTENT_WHY_CODES,
76
79
  CHANGES_FINDING_CODES,
77
80
  CHANGES_ERROR_CODES,
78
81
  COMPOSITES_ERROR_CODES,
@@ -82,6 +85,9 @@ const PER_COMMAND: Record<string, readonly string[]> = {
82
85
  WORK_ERROR_CODES,
83
86
  WORK_HISTORY_ERROR_CODES,
84
87
  WORK_LEASE_REFUSALS,
88
+ RUNS_ERROR_CODES,
89
+ RUNS_REASON_CODES,
90
+ RUNS_WRITE_ERROR_CODES,
85
91
  WORK_EVIDENCE_ERROR_CODES,
86
92
  RECORD_FINDING_CODES,
87
93
  ANSWER_WARNING_CODES,
@@ -91,6 +97,8 @@ const PER_COMMAND: Record<string, readonly string[]> = {
91
97
  PATCH_ERROR_CODES,
92
98
  AGENT_ERROR_CODES,
93
99
  WRITE_SCOPE_CODES,
100
+ SCOPE_FINDING_CODES,
101
+ PLANTABLE_REASON_CODES,
94
102
  ROTATION_REFUSAL_CODES,
95
103
  SIGNERS_ERROR_CODES,
96
104
  EVIDENCE_ERROR_CODES,
@@ -145,7 +153,7 @@ describe("the closed list of reason codes", () => {
145
153
 
146
154
  test("the output schemas name exactly these codes", () => {
147
155
  const named = new Set<string>();
148
- for (const f of readdirSync(HERE).filter((f) => f.endsWith(".schema.json") && !f.startsWith("declaration") && !f.startsWith("workspace-kinds"))) {
156
+ for (const f of readdirSync(HERE).filter((f) => f.endsWith(".schema.json") && !f.startsWith("declaration") && !f.startsWith("workspace-kinds") && !f.startsWith("workspace-principals"))) {
149
157
  for (const c of schemaCodes(JSON.parse(readFileSync(join(HERE, f), "utf-8")))) {
150
158
  expect(isReasonCode(c), `${f} names ${c}, which is not in reason-codes.ts`).toBe(true);
151
159
  named.add(c);