@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
@@ -92,6 +92,9 @@ export const REASONS = {
92
92
  "work-done-gap-open": "A work record is done, and the finding it came from still fires on its region. graph --intent raises it, and records does by walking that region.",
93
93
  "work-acceptance-unmet": "A work record is done, and one of its acceptance criteria has no passing evidence of the verification it expects. check fails on it (WSP117).",
94
94
  "work-acceptance-self-verified": "A passing manual verdict on a work record's criterion names the record's implementer, so it does not count: a manual verdict comes from someone else.",
95
+ "work-contract-unknown": "A work record names a contract that no record of its kind's contract kind has.",
96
+ "work-contract-undecided": "A work record names a contract whose state is not approved, such as a draft.",
97
+ "work-tier-unknown": "A work record names a builder tier that its kind's work.tier.tiers does not list.",
95
98
  // An answer to a decision point that is valid but warned about (records, points, ws-058, #2739).
96
99
  "answer-points-unreadable": "The points file the answer kind names can't be read, or is not valid.",
97
100
  "answer-point-unknown": "The answer's point is not declared in the points file the answer kind names.",
@@ -136,7 +139,10 @@ export const REASONS = {
136
139
  // Write scope (#2548): a records write that is refused, and a commit check --changes reports, outside the writer's scope.
137
140
  "write-scope-member": "The write is to a file, or to a record kind, of a member outside the writer's scope: an agent session writes only its own member, and writeScope.<class>.members leaves the member out.",
138
141
  "write-scope-kind": "The write is to a record kind writeScope.<class>.records does not list, with a verb it does not list for the kind, or deletes a record.",
142
+ "write-scope-protected": "The write is to a file writeScope.<class>.protected lists, or one under a directory it lists, outside any top-level keys the entry's except allows (#3146).",
143
+ "write-scope-class-unknown": "The declaration's writeScope at base names a principal class no pinned package supplies, and the writer is judged human, so it may be in that class; the write is refused until the package is installed at the pinned version or the entry removed (#3080).",
139
144
  "agent-unknown": "CHANT_AGENT, or a commit's Chant-Agent trailer, names an agent session the declaration at base does not declare.",
145
+ "principal-unidentified": "chant.workspace.json at base sets identity.attribution to identified, and the write names a person by a bare name: --by, an author field or a decision point's answerer must be a forge identity (github:<login>, gitlab:<login>, <forge>@<host>:<login>), a principal the signers file at base lists, or an agent, runner or service principal (#3163).",
140
146
  // A review given in a session (records review --session, #2693).
141
147
  "session-unknown": "--session names no session of a session kind whose subjects are the record's kind.",
142
148
  "session-not-open": "--session names a session in a closed state, which takes no more verdicts.",
@@ -156,12 +162,15 @@ export const REASONS = {
156
162
  // The intent graph (graph --intent, #2651): a read that fails.
157
163
  "intent-region-invalid": "The region's path, or its line range, does not exist in the tree read.",
158
164
  "intent-record-unknown": "graph --intent --record names an id that no record of a decision kind read has.",
165
+ "intent-symbol-unsupported": "The region names a symbol, path#symbol, in a file no symbol resolver reads; a line range still works.",
166
+ "intent-symbol-unknown": "The region names a symbol the file does not declare in the tree read.",
167
+ "intent-symbol-ambiguous": "The region names a symbol that matches more than one declaration in the file; its qualified name picks one.",
159
168
  // The intent graph: part of the walk that can't be read. The document is still printed.
160
169
  "intent-history-shallow": "The repository is a shallow clone, so the region's history stops at the clone's boundary.",
161
170
  "intent-plugin-failed": "A kind file's commitJoins, which joins commits to units, contracts and evidence, failed for a commit.",
162
171
  // The intent graph: findings, each a node in the graph.
163
172
  "intent-commit-undecided": "A commit changed the region when no decision constrained it at path granularity.",
164
- "intent-commit-bare": "A commit names no unit, no pull request and no decision covering the region at its time.",
173
+ "intent-commit-bare": "A commit names no unit, carries no record through its Chant-Record or Chant-Lease trailer, and has no pull request and no decision covering the region at its time.",
165
174
  "intent-pin-drifted": "A decision's pinned artifact no longer hashes to the pin.",
166
175
  "intent-pin-missing": "A decision's pinned artifact does not exist in the tree read.",
167
176
  "intent-pin-stale": "A current decision pins an artifact at the hash a record it supersedes pinned, and the artifact has not changed since: the decision moved on and the artifact did not.",
@@ -177,6 +186,11 @@ export const REASONS = {
177
186
  "intent-decision-unimplemented": "A decided decision constrains the region, no work item that is not dropped implements it, and no commit falls in its window.",
178
187
  "intent-work-blocked": "A work item constraining the region has commits in its window while a work item it needs is not done.",
179
188
  "intent-work-open-decided-code": "Commits in the region are a decision's own work while the work item implementing that decision is still open.",
189
+ // The intent graph's answer to why the region is like this (#3034): what it can't account for.
190
+ "intent-why-no-decision": "No current decision governs the region at any granularity, and none is carried out by the commits or runs that made its current lines.",
191
+ "intent-why-no-run": "None of the region's current lines was made by a commit an agent run is joined to.",
192
+ "intent-why-uncommitted": "Some of the region's lines are not committed yet.",
193
+ "intent-why-run-ambiguous": "Some lines come from a commit several agent runs made, and no run's recorded hunks say which wrote them.",
180
194
  // The forward coverage check (check --changes, #2773): findings, one per changed path.
181
195
  "change-uncovered": "A path the diff changes is covered by no current decided record and no open work item, by path or by its member.",
182
196
  "change-out-of-scope": "A record in hand for the change, such as the work item it is for or a decision that item implements, lists a path the diff changes in its out_of_scope.",
@@ -205,6 +219,9 @@ export const REASONS = {
205
219
  // A box's intent, the decision record its box block names (check, #2850). Each is also a WSP finding.
206
220
  "box-intent-unknown": "A box block names an intent, and no record of a declared kind named decision has that id.",
207
221
  "box-intent-unconstrained": "The decision record a box names as its intent constrains no member or path of this workspace: no member: entry for a declared member and no path: entry at, above or inside one's directory.",
222
+ // Why a workspace isn't plantable (status and graph, #3146): not a finding, since a workspace need not be.
223
+ "box-none": "No member's box block declares services, so the workspace has no box for a host to plant.",
224
+ "box-several": "More than one member's box block declares services, and a planted workspace runs one box.",
208
225
  // The work lease (chant workspace work claim|renew|release, #2732): why the command could not run.
209
226
  "work-kind-missing": "No work kind to find the item in: --kind names a kind with no work block, or the declaration names no work kind.",
210
227
  "work-kind-ambiguous": "More than one declared work kind has a record with the id, so --kind must name one.",
@@ -218,6 +235,12 @@ export const REASONS = {
218
235
  "lease-token-mismatch": "The live lease on the work item carries another fencing token than the one given.",
219
236
  "lease-race": "Another writer changed the lease between this command's read and its write.",
220
237
  "lease-push-rejected": "The remote refused the lease push: another clone claimed the item first, or the remote could not be reached.",
238
+ // The agent run record (chant workspace runs, #3033): why a write was refused, and what a read could not see. Nothing is written on a refusal.
239
+ "run-exists": "runs start or runs record was given a run id the ledger already has.",
240
+ "run-unknown": "runs end names a run the ledger has no start for.",
241
+ "run-ended": "runs end names a run whose end is already recorded.",
242
+ "runs-no-ledger": "The checkout has no chant/lifecycle branch, so there are no agent runs to read.",
243
+ "runs-ledger-malformed": "Some lines of the agent run ledger aren't run events; the rest are read.",
221
244
  // A box whose isolation fails (check, #2727). Each is also a WSP finding.
222
245
  "box-isolation-collision": "Two boxes on one host resolve to the same port, state path or cookie name, or two ports in one box share an offset.",
223
246
  "box-isolation-literal": "A host's stateRoot or a box's state entry is a literal machine path instead of one derived from an environment reference and the box's name.",
@@ -178,7 +178,9 @@
178
178
  "ratify-quorum-not-met",
179
179
  "write-scope-member",
180
180
  "write-scope-kind",
181
+ "write-scope-class-unknown",
181
182
  "agent-unknown",
183
+ "principal-unidentified",
182
184
  "record-unparseable",
183
185
  "record-schema-invalid",
184
186
  "record-id-duplicate",
@@ -0,0 +1,106 @@
1
+ /**
2
+ * The checkout a working-tree read is made in, and which records in it are
3
+ * uncommitted (#3160).
4
+ *
5
+ * Work in progress lives as uncommitted files in a work branch's working tree
6
+ * (#3158, #3145). A reader such as hud or studio shows "kept, not yet
7
+ * applied" from `records --json` alone: each record says whether it is
8
+ * committed, modified or new against `HEAD`, and the document names the
9
+ * branch, the commit `HEAD` names and the base that branch forked from. The
10
+ * reader never diffs git itself.
11
+ *
12
+ * Every git call here reads. None takes the index lock or refreshes the
13
+ * index (`--no-optional-locks`), so a read leaves the checkout as it was,
14
+ * `.git` included, which the reader conformance suite checks.
15
+ */
16
+
17
+ import { execFileSync } from "node:child_process";
18
+ import { resolveBase, type BaseSource } from "./trust/provenance";
19
+
20
+ /** Where a record's file stands against `HEAD`. */
21
+ export type WorktreeState = "committed" | "modified" | "new";
22
+
23
+ /** The branch, head and base of a checkout (#3160). `records` and `status` both print it. */
24
+ export interface CheckoutHead {
25
+ /** The checked-out branch's short name, such as `chant/work/w-12`, or null when `HEAD` is detached. */
26
+ branch: string | null;
27
+ /** The commit `HEAD` names, or null before the first commit. Each record's `worktree` is judged against it. */
28
+ head: string | null;
29
+ /** The merge base of `head` and the target branch tip: the commit the branch forked from. Null without a head, a target, or common history. */
30
+ base: string | null;
31
+ /** Where the target branch was found: `flag` for `--base`, else `origin/HEAD`, `main` or `master`. Null when there is none. */
32
+ baseFrom: BaseSource | null;
33
+ }
34
+
35
+ /** {@link CheckoutHead} for one record kind, with the record files `HEAD` has and the working tree does not. */
36
+ export interface CheckoutView extends CheckoutHead {
37
+ /** Record files of the kind at `head` that the working tree no longer has, from the repository root, sorted. */
38
+ deleted: string[];
39
+ }
40
+
41
+ function git(top: string, args: string[]): string {
42
+ return execFileSync("git", ["--no-optional-locks", "--literal-pathspecs", ...args], {
43
+ cwd: top,
44
+ encoding: "utf-8",
45
+ stdio: ["ignore", "pipe", "pipe"],
46
+ maxBuffer: 256 * 1024 * 1024,
47
+ });
48
+ }
49
+
50
+ function tryGit(top: string, args: string[]): string | undefined {
51
+ try {
52
+ return git(top, args);
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * The branch, head and base of the checkout at `top`. `explicitBase` is
60
+ * `--base`, as the trust policy takes it; otherwise the target branch is the
61
+ * one the policy is read at (`origin/HEAD`, then `main`, then `master`).
62
+ */
63
+ export function readCheckoutHead(top: string, explicitBase?: string): CheckoutHead {
64
+ const branch = tryGit(top, ["symbolic-ref", "--quiet", "--short", "HEAD"])?.trim() || null;
65
+ const head = tryGit(top, ["rev-parse", "--verify", "--quiet", "HEAD^{commit}"])?.trim() || null;
66
+ const target = resolveBase(top, explicitBase);
67
+ const base = head && target.commit ? tryGit(top, ["merge-base", head, target.commit])?.trim() || null : null;
68
+ return { branch, head, base, baseFrom: target.from };
69
+ }
70
+
71
+ /** NUL-separated git output as a list. */
72
+ function zList(out: string | undefined): string[] {
73
+ return (out ?? "").split("\0").filter(Boolean);
74
+ }
75
+
76
+ /**
77
+ * Each of `paths` (record files directly in `dir`, from the repository root)
78
+ * as committed, modified or new against `head`, and the files directly in
79
+ * `dir` at `head` whose names `match` and that `paths` leaves out: the
80
+ * records deleted in the working tree. With no head every path is new.
81
+ *
82
+ * Staged and unstaged changes count alike: anything `HEAD` does not hold is
83
+ * uncommitted. A rename is a new record and a deleted one.
84
+ */
85
+ export function worktreeStates(
86
+ top: string,
87
+ head: string | null,
88
+ dir: string,
89
+ paths: readonly string[],
90
+ match: RegExp,
91
+ ): { states: Map<string, WorktreeState>; deleted: string[] } {
92
+ const states = new Map<string, WorktreeState>();
93
+ if (head === null) {
94
+ for (const p of paths) states.set(p, "new");
95
+ return { states, deleted: [] };
96
+ }
97
+ const prefix = dir === "." ? "" : `${dir}/`;
98
+ const direct = (p: string) => p.startsWith(prefix) && !p.slice(prefix.length).includes("/");
99
+ const spec = dir === "." ? "." : dir;
100
+ const atHead = new Set(zList(tryGit(top, ["ls-tree", "-r", "-z", "--name-only", head, "--", spec])).filter(direct));
101
+ const changed = new Set(zList(tryGit(top, ["diff", "--name-only", "-z", "--no-renames", head, "--", spec])));
102
+ for (const p of paths) states.set(p, !atHead.has(p) ? "new" : changed.has(p) ? "modified" : "committed");
103
+ const present = new Set(paths);
104
+ const deleted = [...atHead].filter((p) => !present.has(p) && match.test(p.slice(prefix.length))).sort();
105
+ return { states, deleted };
106
+ }
@@ -15,6 +15,11 @@
15
15
  *
16
16
  * With `--since <rev>` it prints what changed between two revisions instead,
17
17
  * through `records-since.ts` (#2673).
18
+ *
19
+ * A working-tree read in git says, for each record, whether it is committed,
20
+ * modified or new against `HEAD`, and names the branch, head and base of the
21
+ * checkout; `--uncommitted` lists only the records `HEAD` doesn't hold
22
+ * (#3160, `records-checkout.ts`).
18
23
  */
19
24
 
20
25
  import { execFileSync } from "node:child_process";
@@ -48,8 +53,9 @@ import {
48
53
  type SealInput,
49
54
  type VerdictAttestation,
50
55
  } from "./records";
56
+ import { readCheckoutHead, worktreeStates, type CheckoutView, type WorktreeState } from "./records-checkout";
51
57
  import { gitTree, workingTree, type WorkspaceTree } from "./tree";
52
- import type { DecisionWork } from "./work";
58
+ import type { DecisionWork, WorkAnswer } from "./work";
53
59
  import { activeAttestors, type CommitAttestor, type ProvenanceLevel } from "./trust/attestor";
54
60
  import { policyAtBase, recordProvenance, resolveBase, type BaseSource, type RecordProvenance } from "./trust/provenance";
55
61
  import { returnedPolicy, type TrustPolicy } from "./trust/policy";
@@ -62,7 +68,7 @@ export const RECORDS_CONTRACT_VERSION = 1;
62
68
  export const RECORDS_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/records/v1/records.schema.json";
63
69
 
64
70
  const USAGE =
65
- "chant workspace records [--kind <kind file>] [--current] [--at <rev>] [--base <rev>] [--require attested] [--json] | chant workspace records [--kind <kind file>] --since <rev|session id> [--at <rev>] [--json] | chant workspace records pin <path> | chant workspace records new|amend|review|close (#2670, #2693)";
71
+ "chant workspace records [--kind <kind file>] [--current] [--uncommitted | --at <rev>] [--base <rev>] [--require attested] [--json] | chant workspace records [--kind <kind file>] --since <rev|session id> [--at <rev>] [--json] | chant workspace records pin <path> | chant workspace records new|amend|review|close (#2670, #2693)";
66
72
 
67
73
  /** Exit code when the read worked and a record falls below `--require`. */
68
74
  export const EXIT_BELOW_REQUIRED = 2;
@@ -81,6 +87,8 @@ export interface RecordsQuery {
81
87
  * false; the intent graph passes false, since it raises the warning itself.
82
88
  */
83
89
  workGaps?: boolean;
90
+ /** List only the records that are modified or new against `HEAD` (#3160). Needs git and a working-tree read. */
91
+ uncommitted?: boolean;
84
92
  }
85
93
 
86
94
  /**
@@ -108,6 +116,20 @@ export type RecordView = RecordEntry & {
108
116
  * (`record-decided.ts`). The intent graph's windows open at this commit.
109
117
  */
110
118
  decidedIn?: DecidedIn | null;
119
+ /**
120
+ * For a read in the working tree of a git repository (#3160): whether the
121
+ * record's file is as `HEAD` holds it (`committed`), changed from it
122
+ * (`modified`), or not in it (`new`), staged or not. Absent under `--at`.
123
+ */
124
+ worktree?: WorktreeState;
125
+ /**
126
+ * For a work kind whose work block names an answer kind (#3147): the
127
+ * decision-point answers about the item, the answer records whose
128
+ * `constrains` names its id, joined by id and never copied onto the item.
129
+ * Read at the same revision; in the working tree, with the questions a
130
+ * steward keeps on the lifecycle ledger (#2786) laid over it.
131
+ */
132
+ answers?: WorkAnswer[];
111
133
  };
112
134
 
113
135
  /** The role in the trust policy whose holders' verdicts the quorum does not count (#2671). */
@@ -134,6 +156,10 @@ export type RecordsDocument =
134
156
  /** The directory pinned paths resolve in, from the repository root: the workspace holding the kind file, or the repository root (#2549). */
135
157
  workspaceRoot: string;
136
158
  current: boolean;
159
+ /** Whether only the records `HEAD` doesn't hold were listed (`--uncommitted`, #3160). */
160
+ uncommitted: boolean;
161
+ /** For a read in the working tree of a git repository (#3160): the branch, head and base it is on, and the records deleted from it. Absent under `--at`. */
162
+ checkout?: CheckoutView;
137
163
  trust: TrustView;
138
164
  records: RecordView[];
139
165
  summary: { total: number; valid: number; invalid: number; superseded: number };
@@ -227,7 +253,8 @@ export async function queryDeclaredRecords(kinds: { declared: RecordKindDeclarat
227
253
  const doc = await queryRecords({ ...query, kind: k.file });
228
254
  out.push({ ...doc, declared: { member: k.declared.member, path: k.declared.path, name: k.declared.name } });
229
255
  }
230
- return { $schema: RECORDS_OUTPUT_SCHEMA_ID, contract: RECORDS_CONTRACT_VERSION, kinds: out, ...(query.current ? { spec: specOf(out) } : {}) };
256
+ // The spec is every current spec record; an --uncommitted read lists only some, so it has none.
257
+ return { $schema: RECORDS_OUTPUT_SCHEMA_ID, contract: RECORDS_CONTRACT_VERSION, kinds: out, ...(query.current && !query.uncommitted ? { spec: specOf(out) } : {}) };
231
258
  }
232
259
 
233
260
  /** A records read, before provenance. */
@@ -356,6 +383,8 @@ export async function readRecordsFor(
356
383
 
357
384
  /** Run the query and build the document `--json` prints. Never throws a {@link RecordReadError}. */
358
385
  export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument> {
386
+ // A revision has no working tree to hold anything uncommitted; the CLI and the MCP tool refuse the pair first.
387
+ if (query.uncommitted && query.at !== undefined) throw new Error("--uncommitted reads the working tree and takes no --at");
359
388
  try {
360
389
  const { loaded, root, top, at, workspaceRoot, tree, result } = await readRecordsFor(query);
361
390
  // Provenance, judged by the policy at base and never by the tree read (#2547).
@@ -395,6 +424,41 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
395
424
  const leases = await activeWorkLeases(loaded.file);
396
425
  for (const r of records) if (r.id !== null) r.lease = leases.get(r.id) ?? null;
397
426
  }
427
+ if (loaded.kind.work?.answers) {
428
+ // The answers about each item, joined by id (#3147). An answer kind that can't be read leaves answers out rather than failing the work read.
429
+ const [{ workAnswers }, { readLedgerAnswers, withLedgerAnswers }] = await Promise.all([import("./work"), import("./answers-ledger")]);
430
+ const answersRead = await readRecordsFor({
431
+ kind: resolve(dirname(loaded.file), loaded.kind.work.answers),
432
+ cwd: query.cwd,
433
+ ...(query.at !== undefined ? { at: query.at } : {}),
434
+ overlay: async (base, { loaded: l, root: r }) =>
435
+ withLedgerAnswers(base, await readLedgerAnswers({ file: l.file, name: l.kind.name, dirRel: relative(r, l.dir).split(sep).join("/") || "." })),
436
+ }).catch((err: unknown) => {
437
+ if (err instanceof RecordReadError) return undefined;
438
+ throw err;
439
+ });
440
+ if (answersRead) {
441
+ const byItem = workAnswers(answersRead.result.records, records.flatMap((r) => (r.id !== null ? [r.id] : [])));
442
+ for (const r of records) if (r.id !== null) r.answers = byItem.get(r.id) ?? [];
443
+ }
444
+ }
445
+ // Which records the checkout holds uncommitted, judged after every record is read, so links still resolve across the whole set (#3160).
446
+ let checkout: CheckoutView | undefined;
447
+ let listed = records;
448
+ let summary = result.summary;
449
+ if (at === null && top) {
450
+ const { head, ...rest } = readCheckoutHead(top, query.base);
451
+ const dir = relative(root, loaded.dir).split(sep).join("/") || ".";
452
+ const { states, deleted } = worktreeStates(top, head, dir, records.map((r) => r.path), new RegExp(loaded.kind.location.match));
453
+ for (const r of records) r.worktree = states.get(r.path) ?? "new";
454
+ checkout = { branch: rest.branch, head, base: rest.base, baseFrom: rest.baseFrom, deleted };
455
+ if (query.uncommitted) {
456
+ listed = records.filter((r) => r.worktree !== "committed");
457
+ summary = { ...summary, total: listed.length, valid: listed.filter((r) => r.valid).length, invalid: listed.filter((r) => !r.valid).length };
458
+ }
459
+ } else if (query.uncommitted && !top) {
460
+ throw new RecordReadError("not-a-git-repository", "--uncommitted compares the working tree with HEAD, and this directory is not in a git repository");
461
+ }
398
462
  return {
399
463
  $schema: RECORDS_OUTPUT_SCHEMA_ID,
400
464
  contract: RECORDS_CONTRACT_VERSION,
@@ -408,9 +472,11 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
408
472
  at,
409
473
  workspaceRoot,
410
474
  current: !!query.current,
475
+ uncommitted: !!query.uncommitted,
476
+ ...(checkout ? { checkout } : {}),
411
477
  trust: { base: base.commit, baseFrom: base.from, active: policy.active, signersPath: policy.signersPath, problems: policy.problems },
412
- records,
413
- summary: result.summary,
478
+ records: listed,
479
+ summary,
414
480
  ...(result.decisions ? { decisions: result.decisions } : {}),
415
481
  };
416
482
  } catch (err) {
@@ -561,6 +627,11 @@ export async function runWorkspaceRecords(ctx: CommandContext): Promise<number>
561
627
  console.error(formatError({ message: `chant workspace records takes no argument but pin, new, amend, review or close (got ${args.extraPositional})`, hint: USAGE }));
562
628
  return 1;
563
629
  }
630
+ const pairing = uncommittedPairing(args);
631
+ if (pairing) {
632
+ console.error(formatError({ message: pairing, hint: USAGE }));
633
+ return 1;
634
+ }
564
635
  if (!args.kind) return runDeclaredRecords(args);
565
636
  if (args.require !== undefined && args.require !== "attested") {
566
637
  console.error(formatError({ message: `--require takes one level, attested, not ${JSON.stringify(args.require)}`, hint: USAGE }));
@@ -579,13 +650,13 @@ export async function runWorkspaceRecords(ctx: CommandContext): Promise<number>
579
650
  else console.log(formatSince(since));
580
651
  return "error" in since ? 1 : 0;
581
652
  }
582
- const doc = await queryRecords({ kind: args.kind, current: args.current, at: args.at, base: args.base, cwd: process.cwd() });
653
+ const doc = await queryRecords({ kind: args.kind, current: args.current, uncommitted: args.uncommitted, at: args.at, base: args.base, cwd: process.cwd() });
583
654
  if (args.json) {
584
655
  console.log(JSON.stringify(doc, null, 2));
585
656
  } else if ("error" in doc) {
586
657
  console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
587
658
  } else {
588
- console.log(formatRecords(doc.records, doc.summary, doc.at));
659
+ console.log(formatRecords(doc.records, doc.summary, doc.at, doc.checkout));
589
660
  }
590
661
  if ("error" in doc) return 1;
591
662
  if (args.require) {
@@ -643,7 +714,7 @@ async function runDeclaredRecords(args: CommandContext["args"]): Promise<number>
643
714
  return 1;
644
715
  }
645
716
  if (args.since !== undefined) return runDeclaredSince(declared, args);
646
- const set = await queryDeclaredRecords(declared, { current: args.current, at: args.at, base: args.base, cwd: process.cwd() });
717
+ const set = await queryDeclaredRecords(declared, { current: args.current, uncommitted: args.uncommitted, at: args.at, base: args.base, cwd: process.cwd() });
647
718
  if (args.json) console.log(JSON.stringify(set, null, 2));
648
719
  for (const doc of args.json ? [] : set.kinds) {
649
720
  if ("error" in doc) {
@@ -651,7 +722,7 @@ async function runDeclaredRecords(args: CommandContext["args"]): Promise<number>
651
722
  continue;
652
723
  }
653
724
  console.log(`${doc.declared.name ?? doc.kind.name} (${doc.declared.path})`);
654
- console.log(formatRecords(doc.records, doc.summary, doc.at));
725
+ console.log(formatRecords(doc.records, doc.summary, doc.at, doc.checkout));
655
726
  }
656
727
  if (set.kinds.some((d) => "error" in d)) return 1;
657
728
  if (args.require) {
@@ -717,7 +788,20 @@ export function belowRequired(records: RecordView[], required: ProvenanceLevel):
717
788
  return records.filter((r) => r.provenance.level !== required);
718
789
  }
719
790
 
720
- function formatRecords(records: RecordView[], summary: { total: number; valid: number; invalid: number; superseded: number }, at: string | null): string {
791
+ /** Why `--uncommitted` can't be given with the other flags given, or undefined when it can (#3160). */
792
+ function uncommittedPairing(args: CommandContext["args"]): string | undefined {
793
+ if (!args.uncommitted) return undefined;
794
+ if (args.at !== undefined) return "--uncommitted reads the working tree against HEAD, and takes no --at";
795
+ if (args.since !== undefined) return "--uncommitted takes no --since: --since compares two revisions";
796
+ return undefined;
797
+ }
798
+
799
+ function formatRecords(
800
+ records: RecordView[],
801
+ summary: { total: number; valid: number; invalid: number; superseded: number },
802
+ at: string | null,
803
+ checkout?: CheckoutView,
804
+ ): string {
721
805
  const lines: string[] = [];
722
806
  const idWidth = Math.max(2, ...records.map((r) => (r.id ?? "-").length));
723
807
  const stateWidth = Math.max(5, ...records.map((r) => (r.state ?? "-").length));
@@ -728,7 +812,8 @@ function formatRecords(records: RecordView[], summary: { total: number; valid: n
728
812
  const remediated = r.remediatedBy.length > 0 ? ` remediated by ${r.remediatedBy.join(", ")}` : "";
729
813
  const attested = r.provenance.level === "attested" ? ` attested by ${r.provenance.principal}` : "";
730
814
  const sealed = r.attested === true ? ` sealed by ${(r.data?.[RECORD_SEAL_FIELD] as { signer: string }).signer}` : "";
731
- lines.push(`${(r.id ?? "-").padEnd(idWidth)} ${(r.state ?? "-").padEnd(stateWidth)} ${title}${superseded}${remediated}${attested}${sealed}${flag}`);
815
+ const uncommitted = r.worktree === "new" || r.worktree === "modified" ? ` (${r.worktree}, uncommitted)` : "";
816
+ lines.push(`${(r.id ?? "-").padEnd(idWidth)} ${(r.state ?? "-").padEnd(stateWidth)} ${title}${superseded}${remediated}${attested}${sealed}${uncommitted}${flag}`);
732
817
  if (r.ready !== undefined) {
733
818
  const blocked = (r.blockedBy ?? []).map((b) => `${b.id} (${b.state ?? "unknown"})`).join(", ");
734
819
  const implemented = (r.implements ?? []).map((d) => `${d.id} (${d.state ?? "unknown"})`).join(", ");
@@ -745,9 +830,16 @@ function formatRecords(records: RecordView[], summary: { total: number; valid: n
745
830
  lines.push(`${" ".repeat(idWidth + 2)}quorum ${q.agreed} of ${q.need} agreed, ${verdict}; ${q.notCounted.length} not counted${concerns}`);
746
831
  }
747
832
  }
833
+ for (const path of checkout?.deleted ?? []) lines.push(`${"-".padEnd(idWidth)} ${"-".padEnd(stateWidth)} ${path} (deleted, uncommitted)`);
748
834
  lines.push(
749
835
  `${summary.total} records${at ? ` at ${at.slice(0, 8)}` : ""}: ${summary.valid} valid, ${summary.invalid} invalid, ${summary.superseded} superseded`,
750
836
  );
837
+ if (checkout) {
838
+ const branch = checkout.branch ?? "a detached HEAD";
839
+ const head = checkout.head ? ` at ${checkout.head.slice(0, 8)}` : " with no commits";
840
+ const base = checkout.base ? `, forked from ${checkout.base.slice(0, 8)} of ${checkout.baseFrom}` : "";
841
+ lines.push(`working tree on ${branch}${head}${base}`);
842
+ }
751
843
  return lines.join("\n");
752
844
  }
753
845
 
@@ -173,6 +173,7 @@
173
173
  "record-closed",
174
174
  "write-scope-member",
175
175
  "write-scope-kind",
176
+ "write-scope-class-unknown",
176
177
  "agent-unknown",
177
178
  "record-unparseable",
178
179
  "record-schema-invalid",
@@ -167,7 +167,9 @@
167
167
  "ratify-quorum-not-met",
168
168
  "write-scope-member",
169
169
  "write-scope-kind",
170
+ "write-scope-class-unknown",
170
171
  "agent-unknown",
172
+ "principal-unidentified",
171
173
  "record-unparseable",
172
174
  "record-schema-invalid",
173
175
  "record-id-duplicate",
@@ -261,7 +261,9 @@
261
261
  "session-not-open",
262
262
  "write-scope-member",
263
263
  "write-scope-kind",
264
+ "write-scope-class-unknown",
264
265
  "agent-unknown",
266
+ "principal-unidentified",
265
267
  "record-unparseable",
266
268
  "record-schema-invalid",
267
269
  "record-id-duplicate",
@@ -0,0 +1,171 @@
1
+ /**
2
+ * Uncommitted records in the read contract (#3160): each record of a
3
+ * working-tree read says whether HEAD holds it as it is, the document names
4
+ * the branch, head and base, and `--uncommitted` lists only what HEAD
5
+ * doesn't hold. The documents validate against records.schema.json.
6
+ */
7
+
8
+ import { execFileSync } from "node:child_process";
9
+ import { cpSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+ import Ajv2020 from "ajv/dist/2020";
13
+ import { afterAll, describe, expect, test } from "vitest";
14
+ import { CONFORMANCE_FIXTURE_DIR, UNCOMMITTED_DECISION } from "./conformance";
15
+ import { queryRecords, type RecordsDocument } from "./records-cli";
16
+ import schema from "./records.schema.json";
17
+
18
+ const validate = new Ajv2020({ strict: true, allErrors: true }).compile(schema);
19
+ const KIND = "decisions/decision.kind.mjs";
20
+
21
+ type Result = Extract<RecordsDocument, { records: unknown }>;
22
+
23
+ function result(doc: RecordsDocument): Result {
24
+ expect(validate(doc), JSON.stringify(validate.errors, null, 2)).toBe(true);
25
+ if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
26
+ return doc;
27
+ }
28
+
29
+ const scratch: string[] = [];
30
+ afterAll(() => {
31
+ for (const d of scratch) rmSync(d, { recursive: true, force: true });
32
+ });
33
+
34
+ const GIT_ENV = { ...process.env, GIT_CONFIG_GLOBAL: "/dev/null", GIT_CONFIG_NOSYSTEM: "1" };
35
+ function git(cwd: string, ...args: string[]): string {
36
+ return execFileSync("git", ["-c", "user.name=t", "-c", "user.email=t@localhost", "-c", "commit.gpgsign=false", ...args], { cwd, env: GIT_ENV, encoding: "utf-8" }).trim();
37
+ }
38
+
39
+ const DECIDED = readFileSync(join(CONFORMANCE_FIXTURE_DIR, "decisions", "fix-001-how-the-app-is-deployed.md"), "utf-8");
40
+ /** A decided record with id `fix-<n>`. */
41
+ const decided = (n: string) => DECIDED.replace('id: "fix-001"', `id: "fix-${n}"`);
42
+ /** A proposed record with id `fix-<n>`. */
43
+ const proposed = (n: string) => UNCOMMITTED_DECISION.text.replace('id: "fix-002"', `id: "fix-${n}"`);
44
+ const file = (n: string) => `decisions/fix-${n}-a-record.md`;
45
+
46
+ /** A directory with the decision kind and its schema, and no records. */
47
+ function kindDir(): string {
48
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-records-uncommitted-")));
49
+ scratch.push(root);
50
+ cpSync(join(CONFORMANCE_FIXTURE_DIR, "decisions"), join(root, "decisions"), { recursive: true, filter: (src) => !src.endsWith(".md") });
51
+ return root;
52
+ }
53
+
54
+ /** A repository whose main holds fix-001, fix-003 and fix-005, on a work branch one commit past main. */
55
+ function repository(): { root: string; main: string } {
56
+ const root = kindDir();
57
+ for (const n of ["001", "003", "005"]) writeFileSync(join(root, file(n)), decided(n));
58
+ git(root, "init", "--quiet", "--initial-branch=main");
59
+ git(root, "add", "-A");
60
+ git(root, "commit", "--quiet", "-m", "records");
61
+ const main = git(root, "rev-parse", "HEAD");
62
+ git(root, "checkout", "--quiet", "-b", "chant/work/w-1");
63
+ writeFileSync(join(root, "notes.txt"), "work\n");
64
+ git(root, "add", "notes.txt");
65
+ git(root, "commit", "--quiet", "-m", "work");
66
+ return { root, main };
67
+ }
68
+
69
+ describe("records in the working tree (#3160)", () => {
70
+ test("each record is committed, modified or new against HEAD, and the checkout names its branch, head, base and deleted records", async () => {
71
+ const { root, main } = repository();
72
+ writeFileSync(join(root, file("001")), `${decided("001")}\nA line kept in the working tree.\n`);
73
+ writeFileSync(join(root, file("002")), proposed("002"));
74
+ writeFileSync(join(root, file("004")), proposed("004"));
75
+ git(root, "add", file("004"));
76
+ unlinkSync(join(root, file("003")));
77
+ const head = git(root, "rev-parse", "HEAD");
78
+
79
+ const doc = result(await queryRecords({ kind: KIND, cwd: root }));
80
+ expect(doc.uncommitted).toBe(false);
81
+ expect(doc.records.map((r) => [r.id, r.worktree])).toEqual([
82
+ ["fix-001", "modified"],
83
+ ["fix-002", "new"],
84
+ ["fix-004", "new"],
85
+ ["fix-005", "committed"],
86
+ ]);
87
+ expect(doc.checkout).toEqual({ branch: "chant/work/w-1", head, base: main, baseFrom: "main", deleted: [file("003")] });
88
+
89
+ const only = result(await queryRecords({ kind: KIND, cwd: root, uncommitted: true }));
90
+ expect(only.uncommitted).toBe(true);
91
+ expect(only.records.map((r) => [r.id, r.worktree])).toEqual([
92
+ ["fix-001", "modified"],
93
+ ["fix-002", "new"],
94
+ ["fix-004", "new"],
95
+ ]);
96
+ expect(only.checkout).toEqual(doc.checkout);
97
+ expect(only.summary).toEqual({ total: 3, valid: 3, invalid: 0, superseded: 0 });
98
+ });
99
+
100
+ test("a record changed back to what HEAD holds, or touched without a change, is committed, and the read leaves the index alone", async () => {
101
+ const { root } = repository();
102
+ const path = join(root, file("001"));
103
+ writeFileSync(path, decided("001"));
104
+ const index = join(root, ".git", "index");
105
+ const before = { bytes: readFileSync(index), mtime: statSync(index).mtimeMs };
106
+ const doc = result(await queryRecords({ kind: KIND, cwd: root, uncommitted: true }));
107
+ expect(doc.records).toEqual([]);
108
+ expect(doc.checkout?.deleted).toEqual([]);
109
+ expect(readFileSync(index).equals(before.bytes)).toBe(true);
110
+ expect(statSync(index).mtimeMs).toBe(before.mtime);
111
+ });
112
+
113
+ test("--base names the target the base is forked from", async () => {
114
+ const { root } = repository();
115
+ const head = git(root, "rev-parse", "HEAD");
116
+ const doc = result(await queryRecords({ kind: KIND, cwd: root, base: "HEAD" }));
117
+ expect(doc.checkout).toMatchObject({ head, base: head, baseFrom: "flag" });
118
+ });
119
+
120
+ test("a detached HEAD has no branch, and a repository with no commits has no head and only new records", async () => {
121
+ const { root } = repository();
122
+ git(root, "checkout", "--quiet", "--detach");
123
+ expect(result(await queryRecords({ kind: KIND, cwd: root })).checkout?.branch).toBeNull();
124
+
125
+ const empty = kindDir();
126
+ writeFileSync(join(empty, file("001")), decided("001"));
127
+ git(empty, "init", "--quiet", "--initial-branch=main");
128
+ const doc = result(await queryRecords({ kind: KIND, cwd: empty, uncommitted: true }));
129
+ expect(doc.checkout).toEqual({ branch: "main", head: null, base: null, baseFrom: null, deleted: [] });
130
+ expect(doc.records.map((r) => r.worktree)).toEqual(["new"]);
131
+ });
132
+
133
+ test("a read at a revision has no checkout and no worktree", async () => {
134
+ const { root } = repository();
135
+ writeFileSync(join(root, file("002")), proposed("002"));
136
+ const doc = result(await queryRecords({ kind: KIND, cwd: root, at: "HEAD" }));
137
+ expect(doc.checkout).toBeUndefined();
138
+ expect(doc.records.every((r) => r.worktree === undefined)).toBe(true);
139
+ await expect(queryRecords({ kind: KIND, cwd: root, at: "HEAD", uncommitted: true })).rejects.toThrow(/takes no --at/);
140
+ });
141
+
142
+ test("outside git a read has no checkout, and --uncommitted fails with not-a-git-repository", async () => {
143
+ const root = kindDir();
144
+ writeFileSync(join(root, file("001")), decided("001"));
145
+ const doc = result(await queryRecords({ kind: KIND, cwd: root }));
146
+ expect(doc.checkout).toBeUndefined();
147
+ expect(doc.records[0].worktree).toBeUndefined();
148
+ const failed = await queryRecords({ kind: KIND, cwd: root, uncommitted: true });
149
+ expect(validate(failed)).toBe(true);
150
+ expect(failed).toMatchObject({ error: { code: "not-a-git-repository" } });
151
+ });
152
+
153
+ test("the schema holds an --uncommitted document to its promise", () => {
154
+ const head = "a".repeat(40);
155
+ const doc = {
156
+ $schema: schema.$id,
157
+ contract: 1,
158
+ kind: { name: "decision", schema: "urn:x", file: KIND },
159
+ at: null,
160
+ current: false,
161
+ uncommitted: true,
162
+ checkout: { branch: "main", head, base: head, baseFrom: "main", deleted: [] },
163
+ records: [{ id: "fix-001", path: file("001"), state: "decided", valid: true, reasons: [], supersededBy: null, remediatedBy: [], data: {}, worktree: "new" }],
164
+ summary: { total: 1, valid: 1, invalid: 0, superseded: 0 },
165
+ };
166
+ expect(validate(doc)).toBe(true);
167
+ expect(validate({ ...doc, records: [{ ...doc.records[0], worktree: "committed" }] })).toBe(false);
168
+ const { checkout: _checkout, ...noCheckout } = doc;
169
+ expect(validate(noCheckout)).toBe(false);
170
+ });
171
+ });