@intentius/chant 0.100.0 → 0.101.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 (276) hide show
  1. package/dist/build.d.ts +6 -0
  2. package/dist/build.d.ts.map +1 -1
  3. package/dist/cli/build-options.d.ts +2 -0
  4. package/dist/cli/build-options.d.ts.map +1 -1
  5. package/dist/cli/commands/build.d.ts.map +1 -1
  6. package/dist/cli/commands/import.d.ts.map +1 -1
  7. package/dist/cli/handlers/fan-out.d.ts.map +1 -1
  8. package/dist/cli/main.d.ts.map +1 -1
  9. package/dist/cli/mcp/workspace-tools.d.ts +8 -0
  10. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  11. package/dist/cli/registry.d.ts +22 -0
  12. package/dist/cli/registry.d.ts.map +1 -1
  13. package/dist/config.d.ts +11 -0
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/lexicon.d.ts +24 -1
  16. package/dist/lexicon.d.ts.map +1 -1
  17. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  18. package/dist/lifecycle/plan-digest.d.ts +26 -5
  19. package/dist/lifecycle/plan-digest.d.ts.map +1 -1
  20. package/dist/lint/config.d.ts +4 -4
  21. package/dist/op/activities/activity-contracts.d.ts +1 -0
  22. package/dist/op/activities/activity-contracts.d.ts.map +1 -1
  23. package/dist/op/activities/propose-upgrade.d.ts +2 -0
  24. package/dist/op/activities/propose-upgrade.d.ts.map +1 -1
  25. package/dist/op/index.d.ts +1 -1
  26. package/dist/op/index.d.ts.map +1 -1
  27. package/dist/serializer.d.ts +8 -0
  28. package/dist/serializer.d.ts.map +1 -1
  29. package/dist/telemetry-attribution.d.ts +77 -0
  30. package/dist/telemetry-attribution.d.ts.map +1 -0
  31. package/dist/workspace/agent-cli.d.ts +83 -0
  32. package/dist/workspace/agent-cli.d.ts.map +1 -0
  33. package/dist/workspace/changes-cli.d.ts.map +1 -1
  34. package/dist/workspace/changes.d.ts +8 -1
  35. package/dist/workspace/changes.d.ts.map +1 -1
  36. package/dist/workspace/checks/links.d.ts +1 -0
  37. package/dist/workspace/checks/links.d.ts.map +1 -1
  38. package/dist/workspace/checks/live.d.ts +40 -0
  39. package/dist/workspace/checks/live.d.ts.map +1 -0
  40. package/dist/workspace/checks.d.ts +21 -2
  41. package/dist/workspace/checks.d.ts.map +1 -1
  42. package/dist/workspace/compose-graph.d.ts +63 -0
  43. package/dist/workspace/compose-graph.d.ts.map +1 -1
  44. package/dist/workspace/decide.d.ts +1 -1
  45. package/dist/workspace/decide.d.ts.map +1 -1
  46. package/dist/workspace/declaration.d.ts +32 -0
  47. package/dist/workspace/declaration.d.ts.map +1 -1
  48. package/dist/workspace/declaration.schema.json +138 -3
  49. package/dist/workspace/export-cli.d.ts +12 -0
  50. package/dist/workspace/export-cli.d.ts.map +1 -0
  51. package/dist/workspace/export.d.ts +145 -0
  52. package/dist/workspace/export.d.ts.map +1 -0
  53. package/dist/workspace/graph-cli.d.ts.map +1 -1
  54. package/dist/workspace/import.d.ts +73 -0
  55. package/dist/workspace/import.d.ts.map +1 -0
  56. package/dist/workspace/kinds.d.ts +6 -2
  57. package/dist/workspace/kinds.d.ts.map +1 -1
  58. package/dist/workspace/lineage-adopt-cli.d.ts +15 -0
  59. package/dist/workspace/lineage-adopt-cli.d.ts.map +1 -0
  60. package/dist/workspace/lineage-adopt.d.ts +106 -0
  61. package/dist/workspace/lineage-adopt.d.ts.map +1 -0
  62. package/dist/workspace/lineage-check.d.ts +9 -2
  63. package/dist/workspace/lineage-check.d.ts.map +1 -1
  64. package/dist/workspace/lineage-cli.d.ts +6 -1
  65. package/dist/workspace/lineage-cli.d.ts.map +1 -1
  66. package/dist/workspace/lineage-hash-index.d.ts +110 -0
  67. package/dist/workspace/lineage-hash-index.d.ts.map +1 -0
  68. package/dist/workspace/lineage-init.d.ts +10 -0
  69. package/dist/workspace/lineage-init.d.ts.map +1 -1
  70. package/dist/workspace/lineage-lock.d.ts +147 -0
  71. package/dist/workspace/lineage-lock.d.ts.map +1 -1
  72. package/dist/workspace/lineage-migrations.d.ts +15 -3
  73. package/dist/workspace/lineage-migrations.d.ts.map +1 -1
  74. package/dist/workspace/lineage-provenance.d.ts +18 -0
  75. package/dist/workspace/lineage-provenance.d.ts.map +1 -0
  76. package/dist/workspace/lineage-upgrade-cli.d.ts +2 -0
  77. package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
  78. package/dist/workspace/lineage-upgrade.d.ts +30 -1
  79. package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
  80. package/dist/workspace/lineage-versions.d.ts +116 -0
  81. package/dist/workspace/lineage-versions.d.ts.map +1 -0
  82. package/dist/workspace/links.d.ts +31 -5
  83. package/dist/workspace/links.d.ts.map +1 -1
  84. package/dist/workspace/ls-generated.d.ts +37 -0
  85. package/dist/workspace/ls-generated.d.ts.map +1 -0
  86. package/dist/workspace/ls.d.ts +4 -0
  87. package/dist/workspace/ls.d.ts.map +1 -1
  88. package/dist/workspace/member-commands.d.ts.map +1 -1
  89. package/dist/workspace/nested-graph.d.ts +56 -0
  90. package/dist/workspace/nested-graph.d.ts.map +1 -0
  91. package/dist/workspace/nesting.d.ts +21 -0
  92. package/dist/workspace/nesting.d.ts.map +1 -0
  93. package/dist/workspace/pin-cli.d.ts +10 -0
  94. package/dist/workspace/pin-cli.d.ts.map +1 -0
  95. package/dist/workspace/pin-integrity.d.ts +51 -0
  96. package/dist/workspace/pin-integrity.d.ts.map +1 -0
  97. package/dist/workspace/reason-codes.d.ts +26 -2
  98. package/dist/workspace/reason-codes.d.ts.map +1 -1
  99. package/dist/workspace/record-sessions.d.ts +7 -11
  100. package/dist/workspace/record-sessions.d.ts.map +1 -1
  101. package/dist/workspace/records-cli.d.ts +30 -1
  102. package/dist/workspace/records-cli.d.ts.map +1 -1
  103. package/dist/workspace/records-close.d.ts +5 -2
  104. package/dist/workspace/records-close.d.ts.map +1 -1
  105. package/dist/workspace/records-write.d.ts +22 -4
  106. package/dist/workspace/records-write.d.ts.map +1 -1
  107. package/dist/workspace/records.d.ts +43 -5
  108. package/dist/workspace/records.d.ts.map +1 -1
  109. package/dist/workspace/returns.d.ts +129 -0
  110. package/dist/workspace/returns.d.ts.map +1 -0
  111. package/dist/workspace/status-gates.d.ts.map +1 -1
  112. package/dist/workspace/template-manifest.d.ts +11 -3
  113. package/dist/workspace/template-manifest.d.ts.map +1 -1
  114. package/dist/workspace/trust/attestor.d.ts +8 -0
  115. package/dist/workspace/trust/attestor.d.ts.map +1 -1
  116. package/dist/workspace/trust/dsse.d.ts +58 -0
  117. package/dist/workspace/trust/dsse.d.ts.map +1 -0
  118. package/dist/workspace/trust/evidence-cli.d.ts +66 -0
  119. package/dist/workspace/trust/evidence-cli.d.ts.map +1 -0
  120. package/dist/workspace/trust/evidence.d.ts +93 -0
  121. package/dist/workspace/trust/evidence.d.ts.map +1 -0
  122. package/dist/workspace/trust/policy.d.ts +54 -1
  123. package/dist/workspace/trust/policy.d.ts.map +1 -1
  124. package/dist/workspace/trust/provenance.d.ts +21 -1
  125. package/dist/workspace/trust/provenance.d.ts.map +1 -1
  126. package/dist/workspace/trust/rotation.d.ts +132 -0
  127. package/dist/workspace/trust/rotation.d.ts.map +1 -0
  128. package/dist/workspace/trust/seal.d.ts.map +1 -1
  129. package/dist/workspace/trust/signers-cli.d.ts +49 -0
  130. package/dist/workspace/trust/signers-cli.d.ts.map +1 -0
  131. package/dist/workspace/trust/ssh-commit.d.ts +12 -0
  132. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  133. package/dist/workspace/trust/test-repo.d.ts +13 -0
  134. package/dist/workspace/trust/test-repo.d.ts.map +1 -1
  135. package/dist/workspace/trust/verify.d.ts +15 -0
  136. package/dist/workspace/trust/verify.d.ts.map +1 -1
  137. package/dist/workspace/work-evidence.d.ts +1 -1
  138. package/dist/workspace/work-evidence.d.ts.map +1 -1
  139. package/dist/workspace/write-scope.d.ts +199 -0
  140. package/dist/workspace/write-scope.d.ts.map +1 -0
  141. package/package.json +1 -1
  142. package/src/build.ts +8 -0
  143. package/src/cli/build-options.ts +4 -1
  144. package/src/cli/commands/build.ts +2 -0
  145. package/src/cli/commands/import-live.test.ts +69 -1
  146. package/src/cli/commands/import.ts +48 -22
  147. package/src/cli/handlers/fan-out.test.ts +6 -6
  148. package/src/cli/handlers/fan-out.ts +2 -1
  149. package/src/cli/handlers/graph.test.ts +42 -0
  150. package/src/cli/handlers/graph.ts +22 -0
  151. package/src/cli/handlers/operator.ts +1 -1
  152. package/src/cli/main.test.ts +32 -0
  153. package/src/cli/main.ts +103 -6
  154. package/src/cli/mcp/workspace-tools.test.ts +1 -1
  155. package/src/cli/mcp/workspace-tools.ts +33 -2
  156. package/src/cli/registry.ts +22 -0
  157. package/src/cli/serve-mcp-workspace.test.ts +1 -1
  158. package/src/codegen/release-wiring.test.ts +5 -1
  159. package/src/components/fan-out-output.test.ts +1 -1
  160. package/src/components/fan-out.test.ts +1 -1
  161. package/src/components/promote.test.ts +1 -1
  162. package/src/config.ts +12 -0
  163. package/src/content-digest.test.ts +2 -2
  164. package/src/lexicon.ts +25 -1
  165. package/src/lifecycle/gate-ledger.test.ts +14 -0
  166. package/src/lifecycle/gate-ledger.ts +2 -1
  167. package/src/lifecycle/plan-digest.test.ts +54 -3
  168. package/src/lifecycle/plan-digest.ts +38 -8
  169. package/src/op/activities/activity-contracts.ts +1 -0
  170. package/src/op/activities/propose-upgrade.ts +8 -5
  171. package/src/op/gate-approval.test.ts +17 -0
  172. package/src/op/gate.ts +3 -3
  173. package/src/op/index.ts +1 -1
  174. package/src/serializer.ts +9 -0
  175. package/src/telemetry-attribution.test.ts +91 -0
  176. package/src/telemetry-attribution.ts +145 -0
  177. package/src/workspace/agent-cli.ts +134 -0
  178. package/src/workspace/agent.schema.json +356 -0
  179. package/src/workspace/behold-kinds.test.ts +1 -1
  180. package/src/workspace/changes-cli.ts +5 -0
  181. package/src/workspace/changes.schema.json +179 -1
  182. package/src/workspace/changes.ts +58 -4
  183. package/src/workspace/check-live.test.ts +192 -0
  184. package/src/workspace/check.schema.json +64 -0
  185. package/src/workspace/checks/links.ts +23 -2
  186. package/src/workspace/checks/live.ts +113 -0
  187. package/src/workspace/checks.test.ts +2 -2
  188. package/src/workspace/checks.ts +20 -3
  189. package/src/workspace/compose-graph.test.ts +47 -0
  190. package/src/workspace/compose-graph.ts +120 -3
  191. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +7 -1
  192. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +5 -0
  193. package/src/workspace/declaration.schema.json +138 -3
  194. package/src/workspace/declaration.ts +106 -1
  195. package/src/workspace/declared-kinds.test.ts +33 -0
  196. package/src/workspace/evidence.schema.json +279 -0
  197. package/src/workspace/export-cli.ts +180 -0
  198. package/src/workspace/export-import.test.ts +282 -0
  199. package/src/workspace/export.ts +486 -0
  200. package/src/workspace/graph-cli.ts +47 -1
  201. package/src/workspace/graph-contract.test.ts +89 -4
  202. package/src/workspace/graph.schema.json +206 -1
  203. package/src/workspace/import.ts +325 -0
  204. package/src/workspace/kinds.test.ts +5 -5
  205. package/src/workspace/kinds.ts +25 -3
  206. package/src/workspace/lineage-adopt-cli.ts +103 -0
  207. package/src/workspace/lineage-adopt.test.ts +552 -0
  208. package/src/workspace/lineage-adopt.ts +452 -0
  209. package/src/workspace/lineage-check.ts +26 -5
  210. package/src/workspace/lineage-cli.ts +12 -2
  211. package/src/workspace/lineage-hash-index.ts +305 -0
  212. package/src/workspace/lineage-init.test.ts +9 -0
  213. package/src/workspace/lineage-init.ts +29 -9
  214. package/src/workspace/lineage-lock.ts +54 -0
  215. package/src/workspace/lineage-migrations.test.ts +27 -0
  216. package/src/workspace/lineage-migrations.ts +44 -14
  217. package/src/workspace/lineage-provenance.ts +40 -0
  218. package/src/workspace/lineage-upgrade-cli.ts +9 -5
  219. package/src/workspace/lineage-upgrade.test.ts +92 -1
  220. package/src/workspace/lineage-upgrade.ts +111 -16
  221. package/src/workspace/lineage-versions.ts +348 -0
  222. package/src/workspace/links.test.ts +121 -2
  223. package/src/workspace/links.ts +97 -6
  224. package/src/workspace/ls-contract.test.ts +84 -1
  225. package/src/workspace/ls-generated.ts +111 -0
  226. package/src/workspace/ls.schema.json +19 -0
  227. package/src/workspace/ls.ts +8 -1
  228. package/src/workspace/member-commands.ts +3 -1
  229. package/src/workspace/nested-graph.test.ts +176 -0
  230. package/src/workspace/nested-graph.ts +169 -0
  231. package/src/workspace/nesting.ts +37 -0
  232. package/src/workspace/pin-cli.test.ts +71 -0
  233. package/src/workspace/pin-cli.ts +57 -0
  234. package/src/workspace/pin-integrity.test.ts +121 -0
  235. package/src/workspace/pin-integrity.ts +104 -0
  236. package/src/workspace/points-write.schema.json +1 -0
  237. package/src/workspace/read-contract.test.ts +24 -0
  238. package/src/workspace/reason-codes.test.ts +10 -0
  239. package/src/workspace/reason-codes.ts +29 -2
  240. package/src/workspace/record-sessions.ts +12 -14
  241. package/src/workspace/records-amend.schema.json +4 -0
  242. package/src/workspace/records-cli.ts +93 -12
  243. package/src/workspace/records-close.schema.json +6 -2
  244. package/src/workspace/records-close.ts +10 -2
  245. package/src/workspace/records-formats.test.ts +13 -5
  246. package/src/workspace/records-new.schema.json +4 -0
  247. package/src/workspace/records-review.schema.json +4 -0
  248. package/src/workspace/records-sessions.test.ts +23 -6
  249. package/src/workspace/records-write.test.ts +65 -8
  250. package/src/workspace/records-write.ts +70 -6
  251. package/src/workspace/records.schema.json +55 -3
  252. package/src/workspace/records.ts +96 -5
  253. package/src/workspace/returns.ts +328 -0
  254. package/src/workspace/signers.schema.json +206 -0
  255. package/src/workspace/status-gates.ts +2 -1
  256. package/src/workspace/template-manifest.ts +22 -4
  257. package/src/workspace/trust/attestor.ts +15 -0
  258. package/src/workspace/trust/dsse.ts +134 -0
  259. package/src/workspace/trust/evidence-cli.ts +195 -0
  260. package/src/workspace/trust/evidence.test.ts +241 -0
  261. package/src/workspace/trust/evidence.ts +207 -0
  262. package/src/workspace/trust/policy.ts +110 -3
  263. package/src/workspace/trust/provenance.ts +41 -4
  264. package/src/workspace/trust/record-seal.test.ts +1 -1
  265. package/src/workspace/trust/rotation.test.ts +258 -0
  266. package/src/workspace/trust/rotation.ts +336 -0
  267. package/src/workspace/trust/seal.ts +11 -0
  268. package/src/workspace/trust/signers-cli.ts +178 -0
  269. package/src/workspace/trust/ssh-commit.ts +53 -4
  270. package/src/workspace/trust/test-repo.ts +18 -0
  271. package/src/workspace/trust/trust.test.ts +8 -2
  272. package/src/workspace/trust/verify-cli.ts +1 -0
  273. package/src/workspace/trust/verify.ts +22 -0
  274. package/src/workspace/work-evidence.schema.json +4 -0
  275. package/src/workspace/write-scope.test.ts +340 -0
  276. package/src/workspace/write-scope.ts +448 -0
@@ -28,6 +28,7 @@
28
28
  "paths",
29
29
  "findings",
30
30
  "summary",
31
+ "scope",
31
32
  "ok"
32
33
  ],
33
34
  "properties": {
@@ -198,9 +199,20 @@
198
199
  }
199
200
  }
200
201
  },
202
+ "scope": {
203
+ "description": "The write scope check (#2548, ws-067): each commit in the range, merges left out, judged against the writeScope and agents of the declaration at the range's base, with the trust policy read there. Null when the base has no declaration, or one with no writeScope block and no agents. Every finding fails the check, whatever severity says.",
204
+ "oneOf": [
205
+ {
206
+ "type": "null"
207
+ },
208
+ {
209
+ "$ref": "#/$defs/scope"
210
+ }
211
+ ]
212
+ },
201
213
  "ok": {
202
214
  "type": "boolean",
203
- "description": "False only when severity is fail and there is a finding."
215
+ "description": "False when severity is fail and there is a finding, or when scope has a finding."
204
216
  }
205
217
  }
206
218
  },
@@ -467,6 +479,172 @@
467
479
  },
468
480
  "paths": false
469
481
  }
482
+ },
483
+ "scope": {
484
+ "type": "object",
485
+ "required": [
486
+ "base",
487
+ "restricted",
488
+ "agents",
489
+ "commits",
490
+ "findings"
491
+ ],
492
+ "properties": {
493
+ "base": {
494
+ "type": "string",
495
+ "pattern": "^[0-9a-f]{40,64}$",
496
+ "description": "The commit the declaration and the trust policy were read at: the range's base."
497
+ },
498
+ "restricted": {
499
+ "type": "array",
500
+ "items": {
501
+ "enum": [
502
+ "human",
503
+ "agent",
504
+ "runner",
505
+ "service"
506
+ ]
507
+ },
508
+ "description": "The principal classes with a writeScope entry, and agent when any agent session is declared."
509
+ },
510
+ "agents": {
511
+ "type": "array",
512
+ "items": {
513
+ "type": "string"
514
+ },
515
+ "description": "The agent sessions the declaration at base names."
516
+ },
517
+ "commits": {
518
+ "type": "array",
519
+ "items": {
520
+ "$ref": "#/$defs/scopeCommit"
521
+ }
522
+ },
523
+ "findings": {
524
+ "type": "array",
525
+ "items": {
526
+ "$ref": "#/$defs/scopeFinding"
527
+ }
528
+ }
529
+ }
530
+ },
531
+ "scopeCommit": {
532
+ "type": "object",
533
+ "required": [
534
+ "commit",
535
+ "subject",
536
+ "principal",
537
+ "attested",
538
+ "class",
539
+ "agent",
540
+ "paths"
541
+ ],
542
+ "properties": {
543
+ "commit": {
544
+ "type": "string",
545
+ "pattern": "^[0-9a-f]{40,64}$"
546
+ },
547
+ "subject": {
548
+ "type": "string"
549
+ },
550
+ "principal": {
551
+ "type": "string",
552
+ "description": "The signer that attests the commit under the policy at base, or else the author's email, which the commit only claims."
553
+ },
554
+ "attested": {
555
+ "type": "boolean",
556
+ "description": "Whether principal is an attested signer."
557
+ },
558
+ "class": {
559
+ "enum": [
560
+ "human",
561
+ "agent",
562
+ "runner",
563
+ "service"
564
+ ],
565
+ "description": "agent for a commit naming a session with a Chant-Agent trailer or by a principal a session lists; otherwise the class the principal's role grants at base give."
566
+ },
567
+ "agent": {
568
+ "type": [
569
+ "string",
570
+ "null"
571
+ ],
572
+ "description": "The agent session, or null."
573
+ },
574
+ "paths": {
575
+ "type": "integer",
576
+ "minimum": 0,
577
+ "description": "How many paths in the workspace the commit writes."
578
+ }
579
+ }
580
+ },
581
+ "scopeFinding": {
582
+ "type": "object",
583
+ "required": [
584
+ "id",
585
+ "code",
586
+ "commit",
587
+ "path",
588
+ "principal",
589
+ "class",
590
+ "agent",
591
+ "verb",
592
+ "message"
593
+ ],
594
+ "properties": {
595
+ "id": {
596
+ "type": "string"
597
+ },
598
+ "code": {
599
+ "enum": [
600
+ "write-scope-member",
601
+ "write-scope-kind",
602
+ "agent-unknown"
603
+ ]
604
+ },
605
+ "commit": {
606
+ "type": "string",
607
+ "pattern": "^[0-9a-f]{40,64}$"
608
+ },
609
+ "path": {
610
+ "type": [
611
+ "string",
612
+ "null"
613
+ ],
614
+ "description": "From the workspace root; null for agent-unknown, a finding on the whole commit."
615
+ },
616
+ "principal": {
617
+ "type": "string"
618
+ },
619
+ "class": {
620
+ "enum": [
621
+ "human",
622
+ "agent",
623
+ "runner",
624
+ "service"
625
+ ]
626
+ },
627
+ "agent": {
628
+ "type": [
629
+ "string",
630
+ "null"
631
+ ]
632
+ },
633
+ "verb": {
634
+ "enum": [
635
+ "new",
636
+ "amend",
637
+ "review",
638
+ "close",
639
+ "delete",
640
+ null
641
+ ],
642
+ "description": "For a record file of a kind the declaration at base names: how the commit wrote it. A modified record is review when only its reviews (or a session's verdicts) changed, close when a session entered a closed state, and amend otherwise."
643
+ },
644
+ "message": {
645
+ "type": "string"
646
+ }
647
+ }
470
648
  }
471
649
  }
472
650
  }
@@ -40,17 +40,18 @@ import { execFileSync } from "node:child_process";
40
40
  import { relative, resolve, sep } from "node:path";
41
41
  // @ts-ignore — picomatch has no types declaration
42
42
  import picomatch from "picomatch";
43
- import { declaredRecordKinds, readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError, type ChangeSeverity } from "./declaration";
43
+ import { declaredRecordKinds, readDeclaration, readerVersion, WORKSPACE_ERROR_CODES, WorkspaceReadError, type ChangeSeverity, type Declaration } from "./declaration";
44
44
  import { declaredKindFile } from "./declared-kinds";
45
45
  import { isGeneratedPath } from "./generated-files";
46
46
  import { parseRegion } from "./intent";
47
47
  import { constraintCovers, isWorkspacePath, memberHolding } from "./record-assets";
48
- import { RecordReadError, type LoadedRecordKind, type RecordEntry } from "./records";
48
+ import { loadRecordKind, RecordReadError, type LoadedRecordKind, type RecordEntry } from "./records";
49
49
  import { readRecordsFor } from "./records-cli";
50
50
  import type { ReasonCode } from "./reason-codes";
51
51
  import { joinPath } from "./tree";
52
52
  import { locateWorkspace } from "./which-chant";
53
53
  import { idList, isDecided } from "./work";
54
+ import { checkWriteScope, type CheckKind, type ScopeReport } from "./write-scope";
54
55
 
55
56
  /** The version of the `changes` document this chant writes. */
56
57
  export const CHANGES_CONTRACT_VERSION = 1;
@@ -172,7 +173,13 @@ export type ChangesDocument =
172
173
  paths: ChangedPath[];
173
174
  findings: ChangeFinding[];
174
175
  summary: { paths: number; covered: number; uncovered: number; outOfScope: number; ignored: number; records: number };
175
- /** False only when severity is fail and there are findings. */
176
+ /**
177
+ * The write scope check (#2548): each commit in the range judged
178
+ * against the writeScope and agents the declaration at base gives,
179
+ * or null when that declaration restricts no one.
180
+ */
181
+ scope: ScopeReport | null;
182
+ /** False when severity is fail and there are findings, or when a commit writes outside its writer's scope. */
176
183
  ok: boolean;
177
184
  })
178
185
  | (Head & { error: { code: ChangesErrorCode; message: string } });
@@ -463,6 +470,7 @@ async function run(query: ChangesQuery, head: Head): Promise<Exclude<ChangesDocu
463
470
  }
464
471
  }
465
472
  const count = (s: ChangeStatus) => paths.filter((p) => p.status === s).length;
473
+ const scope = await writeScopeAtBase(query, top, range, workspacePrefix);
466
474
  return {
467
475
  ...head,
468
476
  workspace: { name: declaration.name, root: located.root },
@@ -474,10 +482,56 @@ async function run(query: ChangesQuery, head: Head): Promise<Exclude<ChangesDocu
474
482
  paths,
475
483
  findings,
476
484
  summary: { paths: paths.length, covered: count("covered"), uncovered: count("uncovered"), outOfScope: count("out-of-scope"), ignored: count("ignored"), records: count("record") },
477
- ok: !(severity === "fail" && findings.length > 0),
485
+ scope,
486
+ ok: !(severity === "fail" && findings.length > 0) && (scope === null || scope.findings.length === 0),
478
487
  };
479
488
  }
480
489
 
490
+ /**
491
+ * The write scope check over the range (#2548), with the declaration, its
492
+ * record kinds and the trust policy read at the range's base, so a change
493
+ * can't widen its own scope. Null when the base has no declaration, or one
494
+ * that restricts no one. The base is read without the root-chant rule: a
495
+ * change may move the chant pin, and the scope is data either chant reads.
496
+ */
497
+ async function writeScopeAtBase(query: ChangesQuery, top: string, range: { base: string; head: string }, prefix: string): Promise<ScopeReport | null> {
498
+ let located: ReturnType<typeof locateWorkspace>;
499
+ let declaration: Declaration;
500
+ try {
501
+ located = locateWorkspace(query.cwd, range.base);
502
+ declaration = readDeclaration(located.tree);
503
+ } catch (err) {
504
+ if (err instanceof WorkspaceReadError && err.code === "declaration-missing") return null;
505
+ throw err;
506
+ }
507
+ if (declaration.writeScope === null && declaration.agents.length === 0) return null;
508
+ const kinds: CheckKind[] = [];
509
+ for (const d of declaredRecordKinds(declaration)) {
510
+ let loaded: LoadedRecordKind;
511
+ try {
512
+ loaded = await loadRecordKind(declaredKindFile(d, located.rootOnDisk), query.cwd);
513
+ } catch (err) {
514
+ // A kind that doesn't load here: its files are judged as plain paths.
515
+ if (err instanceof RecordReadError) continue;
516
+ throw err;
517
+ }
518
+ const { kind } = loaded;
519
+ const session = kind.session;
520
+ kinds.push({
521
+ scoped: { name: kind.name, declaredName: d.name, member: d.member, declared: true },
522
+ dir: toPosix(relative(top, loaded.dir)) || ".",
523
+ match: new RegExp(kind.location.match),
524
+ reviewFields: [kind.reviews?.field, session?.verdicts].filter((f): f is string => typeof f === "string"),
525
+ session:
526
+ session && kind.stateField !== undefined
527
+ ? { stateField: kind.stateField, closed: kind.closedStates ?? [], fills: [session.openedRev].filter((f): f is string => typeof f === "string") }
528
+ : null,
529
+ format: kind.format,
530
+ });
531
+ }
532
+ return checkWriteScope({ top, base: range.base, head: range.head, prefix, declaration, kinds });
533
+ }
534
+
481
535
  function finding(code: ChangesFindingCode, p: ChangedPath, severity: "warn" | "fail", message: string, records: string[]): ChangeFinding {
482
536
  return { id: `finding:${code}:${p.path}`, code, path: p.path, message, severity, records, triage: changeFindingSource({ code, path: p.path }) };
483
537
  }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * `chant workspace check --live --env <env>` (#2549, ws-062): declared member
3
+ * links resolved against the live graph of each producer. A fake chant stands
4
+ * in for the member's own toolchain: it prints ir.json for a source read and
5
+ * live.json for `--live`, and records its command line in argv.txt.
6
+ */
7
+
8
+ import { existsSync, readFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { afterAll, afterEach, describe, expect, test, vi } from "vitest";
11
+ import { cleanScratch, contract, declaration, repo } from "./__fixtures__/contract-repo";
12
+ import { parseArgs } from "../cli/main";
13
+ import schema from "./check.schema.json";
14
+ import { workspaceGraph } from "./graph-cli";
15
+ import { runChecks, runWorkspaceCheck } from "./lineage-check";
16
+ import type { CheckDocument } from "./lineage-check";
17
+
18
+ const { expectValid } = contract(schema);
19
+
20
+ afterAll(() => cleanScratch());
21
+ afterEach(() => vi.restoreAllMocks());
22
+
23
+ const FAKE_CHANT = `#!/bin/sh
24
+ [ "$1" = graph ] || { echo "Error: Unknown command: $1" >&2; exit 1; }
25
+ printf '%s\\n' "$*" > ../argv.txt
26
+ case " $* " in
27
+ *" --live "*) cat live.json ;;
28
+ *) cat ir.json ;;
29
+ esac
30
+ `;
31
+
32
+ const graph = (exports: { name: string; value?: unknown }[]) => JSON.stringify({ version: 1, nodes: [], edges: [], groups: {}, exports });
33
+
34
+ function workspace(live: { name: string; value?: unknown }[], links: unknown[], producerKind: "chant" | "other" = "chant", extra: Record<string, unknown> = {}) {
35
+ return repo({
36
+ "chant.workspace.json": declaration([
37
+ producerKind === "chant" ? { name: "api", dir: "api", kind: "chant" } : { name: "api", dir: "api", kind: "other", because: "a queue made by hand", outputs: ["QueueUrl"] },
38
+ { name: "web", dir: "web", kind: "other", because: "a static site", links },
39
+ ], extra),
40
+ "api/chant.config.ts": "export default {};\n",
41
+ "api/ir.json": graph([{ name: "QueueUrl" }, { name: "TopicArn" }]),
42
+ "api/live.json": graph(live),
43
+ "web/index.html": "",
44
+ ".gitignore": "node_modules\n",
45
+ "node_modules/.bin/chant": { text: FAKE_CHANT, mode: 0o755 },
46
+ });
47
+ }
48
+
49
+ async function live(root: string, env = "prod") {
50
+ const doc = (await runChecks(root, undefined, { live: { env } })) as Extract<CheckDocument, { workspace: unknown }>;
51
+ const diagnostics = doc.declaration!.diagnostics.filter((d) => d.ruleId === "WSP141" || d.ruleId === "WSP142");
52
+ return { doc, diagnostics, rows: doc.declaration!.live?.links ?? [] };
53
+ }
54
+
55
+ describe("check --live (#2549)", () => {
56
+ test("a link to an output the live graph publishes resolves, and the row says it resolved live", async () => {
57
+ const root = workspace([{ name: "QueueUrl", value: "https://q" }], [{ member: "api", output: "QueueUrl" }]);
58
+ const { doc, diagnostics, rows } = await live(root);
59
+ expect(diagnostics).toEqual([]);
60
+ expectValid(doc);
61
+ expect(doc.declaration!.live!.env).toBe("prod");
62
+ expect(rows).toMatchObject([{ consumer: "web", producer: "api", output: "QueueUrl", resolves: "live", status: "resolved", reason: null }]);
63
+ // The member ran as `chant graph --live --env prod`.
64
+ expect(readFileSync(join(root, "argv.txt"), "utf-8")).toContain("--env prod --live");
65
+ });
66
+
67
+ test("an output the producer declares but its estate does not publish is WSP141, a warning", async () => {
68
+ const root = workspace([{ name: "TopicArn", value: "arn" }], [{ member: "api", output: "QueueUrl" }]);
69
+ const { diagnostics, rows } = await live(root);
70
+ expect(rows[0]).toMatchObject({ status: "missing", resolves: "live" });
71
+ expect(diagnostics).toHaveLength(1);
72
+ expect(diagnostics[0]).toMatchObject({ ruleId: "WSP141", severity: "warning", entity: "web" });
73
+ expect(diagnostics[0].message).toContain("does not resolve live in prod");
74
+ expect(diagnostics[0].message).toContain("TopicArn");
75
+ });
76
+
77
+ test("a live read that returned no outputs at all leaves the link unresolved, not missing", async () => {
78
+ const root = workspace([], [{ member: "api", output: "QueueUrl" }]);
79
+ const { diagnostics, rows } = await live(root);
80
+ expect(rows[0].status).toBe("unresolved");
81
+ expect(diagnostics).toMatchObject([{ ruleId: "WSP142", severity: "info" }]);
82
+ expect(diagnostics[0].message).toContain("returned no outputs");
83
+ });
84
+
85
+ test("a producer of a kind chant does not read live is kept unresolved, with the reason", async () => {
86
+ const root = workspace([], [{ member: "api", output: "QueueUrl" }], "other");
87
+ const { diagnostics, rows } = await live(root);
88
+ expect(rows[0]).toMatchObject({ status: "unresolved", resolves: "live" });
89
+ expect(diagnostics).toMatchObject([{ ruleId: "WSP142" }]);
90
+ expect(diagnostics[0].message).toContain("kind other");
91
+ });
92
+
93
+ test("a telemetry link is not resolved live", async () => {
94
+ const root = workspace([{ name: "QueueUrl" }], [{ member: "api", output: "traces", kind: "telemetry" }]);
95
+ const { rows } = await live(root);
96
+ expect(rows).toEqual([]);
97
+ });
98
+
99
+ test("severity settings apply to the live checks like any other", async () => {
100
+ const root = repo({
101
+ "chant.workspace.json": declaration(
102
+ [
103
+ { name: "api", dir: "api", kind: "chant" },
104
+ { name: "web", dir: "web", kind: "other", because: "a static site", links: [{ member: "api", output: "QueueUrl" }], suppress: [{ check: "WSP141", because: "deployed next week" }] },
105
+ ],
106
+ { checks: { WSP141: "error" } },
107
+ ),
108
+ "api/chant.config.ts": "export default {};\n",
109
+ "api/ir.json": graph([{ name: "QueueUrl" }]),
110
+ "api/live.json": graph([{ name: "Other" }]),
111
+ "web/index.html": "",
112
+ ".gitignore": "node_modules\n",
113
+ "node_modules/.bin/chant": { text: FAKE_CHANT, mode: 0o755 },
114
+ });
115
+ const { doc, diagnostics } = await live(root);
116
+ expect(diagnostics).toEqual([]);
117
+ expect(doc.declaration!.suppressed.map((s) => s.ruleId)).toContain("WSP141");
118
+ });
119
+
120
+ test("without --live no member runs and the report has no live section", async () => {
121
+ const root = workspace([{ name: "QueueUrl" }], [{ member: "api", output: "QueueUrl" }]);
122
+ const doc = (await runChecks(root)) as Extract<CheckDocument, { workspace: unknown }>;
123
+ expect(doc.declaration!.live).toBeUndefined();
124
+ expect(existsSync(join(root, "argv.txt"))).toBe(false);
125
+ });
126
+ });
127
+
128
+ describe("the command (#2549)", () => {
129
+ async function run(root: string, argv: string[]) {
130
+ const out: string[] = [];
131
+ const err: string[] = [];
132
+ vi.spyOn(console, "log").mockImplementation((...a: unknown[]) => void out.push(a.join(" ")));
133
+ vi.spyOn(console, "error").mockImplementation((...a: unknown[]) => void err.push(a.join(" ")));
134
+ vi.spyOn(process, "cwd").mockReturnValue(root);
135
+ const code = await runWorkspaceCheck({ args: parseArgs(["workspace", "check", ...argv]), plugins: [] } as never);
136
+ return { code, out: out.join("\n"), err: err.join("\n") };
137
+ }
138
+
139
+ test("--live needs --env, and takes no --at, and runs no member when it refuses", async () => {
140
+ const root = workspace([{ name: "QueueUrl" }], [{ member: "api", output: "QueueUrl" }]);
141
+ const noEnv = await run(root, ["--live"]);
142
+ expect(noEnv.code).toBe(1);
143
+ expect(noEnv.err).toContain("--live needs an environment");
144
+ const atRev = await run(root, ["--live", "--env", "prod", "--at", "HEAD"]);
145
+ expect(atRev.code).toBe(1);
146
+ expect(atRev.err).toContain("--at");
147
+ expect(existsSync(join(root, "argv.txt"))).toBe(false);
148
+ });
149
+
150
+ test("--live --json prints the live section, and a WSP141 warning leaves the exit code 0", async () => {
151
+ // The fake producer's source lists no outputs, so the source check WSP093 is turned off; the live warning is the one under test.
152
+ const root = workspace([{ name: "TopicArn" }], [{ member: "api", output: "QueueUrl" }], "chant", { checks: { WSP093: "off" } });
153
+ const { code, out } = await run(root, ["--live", "--env", "prod", "--json"]);
154
+ const report = JSON.parse(out) as { declaration: { live: { env: string; links: { status: string }[] }; diagnostics: { ruleId: string }[] } };
155
+ expect(report.declaration.live).toMatchObject({ env: "prod", links: [{ status: "missing" }] });
156
+ expect(report.declaration.diagnostics.map((d) => d.ruleId)).toContain("WSP141");
157
+ expect(code).toBe(0);
158
+ });
159
+ });
160
+
161
+ describe("the design member kind (#2549, ws-062)", () => {
162
+ const designWorkspace = (extra: Record<string, unknown> = {}) =>
163
+ repo({
164
+ "chant.workspace.json": declaration([
165
+ { name: "design", dir: "design", kind: "design", outputs: ["screens/home.json"], ...extra },
166
+ { name: "web", dir: "web", kind: "other", because: "a static site", links: [{ member: "design", output: "screens/home.json" }] },
167
+ ]),
168
+ "design/screens/home.json": "{}\n",
169
+ "web/index.html": "",
170
+ });
171
+
172
+ test("is a declared kind: no WSP003, no WSP009, no `because` needed, and its listed outputs are link targets", async () => {
173
+ const doc = (await runChecks(designWorkspace())) as Extract<CheckDocument, { workspace: unknown }>;
174
+ const ids = doc.declaration!.diagnostics.map((d) => d.ruleId);
175
+ expect(ids).not.toContain("WSP003");
176
+ expect(doc.declaration!.diagnostics.filter((d) => d.entity === "design")).toEqual([]);
177
+ expect(doc.declaration!.links).toMatchObject([{ consumer: "web", producer: "design", output: "screens/home.json", status: "resolved" }]);
178
+ });
179
+
180
+ test("a data member: workspace graph reads it and builds nothing, saying so", async () => {
181
+ const { doc } = await workspaceGraph({ cwd: designWorkspace() });
182
+ if ("error" in doc) throw new Error(doc.error.message);
183
+ expect(doc.members.find((m) => m.name === "design")).toMatchObject({ kind: "design", status: "skipped", reason: { code: "kind-not-run" } });
184
+ expect(doc.members.find((m) => m.name === "design")!.reason!.message).toContain("data");
185
+ });
186
+
187
+ test("a design member's link is not resolved live: its files are not an estate", async () => {
188
+ const { rows, diagnostics } = await live(designWorkspace());
189
+ expect(rows[0]).toMatchObject({ status: "unresolved", resolves: "live" });
190
+ expect(diagnostics).toMatchObject([{ ruleId: "WSP142" }]);
191
+ });
192
+ });
@@ -146,6 +146,70 @@
146
146
  "ok": {
147
147
  "type": "boolean"
148
148
  },
149
+ "live": {
150
+ "description": "Present with --live --env (#2549, ws-062): the declared output links resolved against the live graph of the environment. Each member of kind chant ran chant graph --live. Added by chant 0.101.0.",
151
+ "type": "object",
152
+ "required": [
153
+ "env",
154
+ "links"
155
+ ],
156
+ "properties": {
157
+ "env": {
158
+ "type": "string"
159
+ },
160
+ "links": {
161
+ "type": "array",
162
+ "items": {
163
+ "type": "object",
164
+ "required": [
165
+ "consumer",
166
+ "producer",
167
+ "output",
168
+ "resolves",
169
+ "status",
170
+ "reason"
171
+ ],
172
+ "properties": {
173
+ "consumer": {
174
+ "type": "string"
175
+ },
176
+ "producer": {
177
+ "type": "string"
178
+ },
179
+ "output": {
180
+ "type": "string"
181
+ },
182
+ "kind": {
183
+ "type": "string"
184
+ },
185
+ "origin": {
186
+ "const": "declared"
187
+ },
188
+ "resolves": {
189
+ "const": "live"
190
+ },
191
+ "status": {
192
+ "enum": [
193
+ "resolved",
194
+ "missing",
195
+ "unresolved",
196
+ "invalid"
197
+ ]
198
+ },
199
+ "reason": {
200
+ "type": [
201
+ "string",
202
+ "null"
203
+ ]
204
+ },
205
+ "pointer": {
206
+ "type": "string"
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+ },
149
213
  "diagnostics": {
150
214
  "description": "Active findings, in the shape `chant lint --format json` prints, sorted by position.",
151
215
  "type": "array",
@@ -16,12 +16,13 @@
16
16
  * | WSP095 | an inferred join is ambiguous: a parameter matches two or more outputs |
17
17
  * | WSP096 | a consumer states the same link twice |
18
18
  * | WSP097 | an entry lists `outputs` for a kind that doesn't take them from the entry |
19
+ * | WSP098 | a link states a `protocol` and its kind is not `telemetry` (#2558) |
19
20
  */
20
21
 
21
22
  import type { WorkspaceCheck, WorkspaceCheckContext, WorkspaceDiagnostic } from "../checks";
22
23
  import type { Member } from "../declaration";
23
24
  import { sourceMemberHandles } from "../member-handles";
24
- import { DEFAULT_LINK_KIND, LINK_KINDS, linkTargetProblem, resolveLinks, type AmbiguousRow, type LinkRow, type LinkTableRow } from "../links";
25
+ import { DEFAULT_LINK_KIND, LINK_KINDS, TELEMETRY_LINK_KIND, linkTargetProblem, resolveLinks, type AmbiguousRow, type LinkRow, type LinkTableRow } from "../links";
25
26
 
26
27
  const tables = new WeakMap<WorkspaceCheckContext, LinkTableRow[]>();
27
28
 
@@ -62,7 +63,7 @@ export const LINK_CHECKS: readonly WorkspaceCheck[] = [
62
63
  {
63
64
  id: "WSP092",
64
65
  name: "link-kind-unknown",
65
- description: `Every member link's kind is one chant knows. Unknown link kinds fail closed; ${DEFAULT_LINK_KIND} is the default and the only one so far.`,
66
+ description: `Every member link's kind is one chant knows. Unknown link kinds fail closed; ${DEFAULT_LINK_KIND} is the default, and ${LINK_KINDS.filter((k) => k !== DEFAULT_LINK_KIND).join(", ")} is the other.`,
66
67
  severity: "error",
67
68
  configurable: false,
68
69
  check(ctx) {
@@ -180,4 +181,24 @@ export const LINK_CHECKS: readonly WorkspaceCheck[] = [
180
181
  });
181
182
  },
182
183
  },
184
+ {
185
+ id: "WSP098",
186
+ name: "link-protocol-misplaced",
187
+ description: "A link states a protocol only when it is a telemetry link: protocol is the OTLP protocol the consumer sends to the producer's collector, and no other link kind has one.",
188
+ severity: "error",
189
+ configurable: true,
190
+ check(ctx) {
191
+ return ctx.declaration.members.flatMap((m) =>
192
+ m.links
193
+ .filter((l) => l.protocol !== null && (l.kind ?? DEFAULT_LINK_KIND) !== TELEMETRY_LINK_KIND)
194
+ .map((l) => ({
195
+ checkId: this.id,
196
+ severity: this.severity,
197
+ message: `member ${m.name}'s link to ${l.member} output ${l.output} states protocol ${l.protocol}, and its kind is ${l.kind ?? DEFAULT_LINK_KIND}; protocol belongs to a ${TELEMETRY_LINK_KIND} link`,
198
+ entity: m.name,
199
+ pointer: `${l.pointer}/protocol`,
200
+ })),
201
+ );
202
+ },
203
+ },
183
204
  ];