@intentius/chant 0.89.0 → 0.91.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 (194) hide show
  1. package/dist/cli/handlers/components.d.ts +8 -0
  2. package/dist/cli/handlers/components.d.ts.map +1 -1
  3. package/dist/cli/handlers/operator.d.ts +14 -0
  4. package/dist/cli/handlers/operator.d.ts.map +1 -1
  5. package/dist/cli/handlers/run.d.ts.map +1 -1
  6. package/dist/cli/handlers/serve.d.ts.map +1 -1
  7. package/dist/cli/main.d.ts.map +1 -1
  8. package/dist/cli/mcp/server.d.ts +10 -5
  9. package/dist/cli/mcp/server.d.ts.map +1 -1
  10. package/dist/cli/mcp/types.d.ts +13 -5
  11. package/dist/cli/mcp/types.d.ts.map +1 -1
  12. package/dist/cli/mcp/workspace-tools.d.ts +55 -0
  13. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -0
  14. package/dist/cli/registry.d.ts +31 -0
  15. package/dist/cli/registry.d.ts.map +1 -1
  16. package/dist/components/verbs/vuln-scan.d.ts +72 -0
  17. package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
  18. package/dist/lifecycle/git.d.ts +40 -5
  19. package/dist/lifecycle/git.d.ts.map +1 -1
  20. package/dist/lifecycle/lease.d.ts +72 -16
  21. package/dist/lifecycle/lease.d.ts.map +1 -1
  22. package/dist/lifecycle/member-ledger.d.ts +3 -2
  23. package/dist/lifecycle/member-ledger.d.ts.map +1 -1
  24. package/dist/lifecycle/plan-ledger.d.ts +114 -0
  25. package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
  26. package/dist/lifecycle/work-lease.d.ts +140 -0
  27. package/dist/lifecycle/work-lease.d.ts.map +1 -0
  28. package/dist/op/activities/activity-contracts.d.ts +2 -2
  29. package/dist/op/builders.d.ts.map +1 -1
  30. package/dist/op/discover.d.ts +25 -0
  31. package/dist/op/discover.d.ts.map +1 -1
  32. package/dist/op/index.d.ts +9 -3
  33. package/dist/op/index.d.ts.map +1 -1
  34. package/dist/op/lifecycle-receipt-store.d.ts +34 -0
  35. package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
  36. package/dist/op/local-executor.d.ts +19 -0
  37. package/dist/op/local-executor.d.ts.map +1 -1
  38. package/dist/op/local-output.d.ts.map +1 -1
  39. package/dist/op/op-ir.d.ts +10 -1
  40. package/dist/op/op-ir.d.ts.map +1 -1
  41. package/dist/op/op-verb-class.d.ts.map +1 -1
  42. package/dist/op/operator.d.ts +29 -0
  43. package/dist/op/operator.d.ts.map +1 -1
  44. package/dist/op/runtime.d.ts +9 -0
  45. package/dist/op/runtime.d.ts.map +1 -1
  46. package/dist/op/runtimes/local.d.ts.map +1 -1
  47. package/dist/op/step-output-ref.d.ts +2 -2
  48. package/dist/op/step-output-ref.d.ts.map +1 -1
  49. package/dist/op/steward.d.ts +140 -0
  50. package/dist/op/steward.d.ts.map +1 -0
  51. package/dist/op/types.d.ts +51 -0
  52. package/dist/op/types.d.ts.map +1 -1
  53. package/dist/op/work-lease-decl.d.ts +18 -0
  54. package/dist/op/work-lease-decl.d.ts.map +1 -0
  55. package/dist/op/work-lease-run.d.ts +173 -0
  56. package/dist/op/work-lease-run.d.ts.map +1 -0
  57. package/dist/workspace/box-isolation.d.ts +99 -0
  58. package/dist/workspace/box-isolation.d.ts.map +1 -0
  59. package/dist/workspace/checks/box-isolation.d.ts +18 -0
  60. package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
  61. package/dist/workspace/checks/boxes.d.ts +71 -0
  62. package/dist/workspace/checks/boxes.d.ts.map +1 -0
  63. package/dist/workspace/checks/records.d.ts +1 -0
  64. package/dist/workspace/checks/records.d.ts.map +1 -1
  65. package/dist/workspace/checks.d.ts +10 -1
  66. package/dist/workspace/checks.d.ts.map +1 -1
  67. package/dist/workspace/conformance/index.d.ts +42 -2
  68. package/dist/workspace/conformance/index.d.ts.map +1 -1
  69. package/dist/workspace/conformance/vitest.d.ts.map +1 -1
  70. package/dist/workspace/decide.d.ts +184 -0
  71. package/dist/workspace/decide.d.ts.map +1 -0
  72. package/dist/workspace/decision-points.schema.json +137 -0
  73. package/dist/workspace/declaration.d.ts +51 -0
  74. package/dist/workspace/declaration.d.ts.map +1 -1
  75. package/dist/workspace/declaration.schema.json +172 -0
  76. package/dist/workspace/declared-kinds.d.ts +12 -0
  77. package/dist/workspace/declared-kinds.d.ts.map +1 -1
  78. package/dist/workspace/points-cli.d.ts +113 -0
  79. package/dist/workspace/points-cli.d.ts.map +1 -0
  80. package/dist/workspace/points.d.ts +320 -0
  81. package/dist/workspace/points.d.ts.map +1 -0
  82. package/dist/workspace/reason-codes.d.ts +29 -0
  83. package/dist/workspace/reason-codes.d.ts.map +1 -1
  84. package/dist/workspace/record-assets.d.ts.map +1 -1
  85. package/dist/workspace/records-cli.d.ts +12 -0
  86. package/dist/workspace/records-cli.d.ts.map +1 -1
  87. package/dist/workspace/records-write.d.ts +35 -3
  88. package/dist/workspace/records-write.d.ts.map +1 -1
  89. package/dist/workspace/records.d.ts +12 -3
  90. package/dist/workspace/records.d.ts.map +1 -1
  91. package/dist/workspace/source-block.d.ts +85 -0
  92. package/dist/workspace/source-block.d.ts.map +1 -0
  93. package/dist/workspace/status-stewards.d.ts +121 -0
  94. package/dist/workspace/status-stewards.d.ts.map +1 -0
  95. package/dist/workspace/status.d.ts +52 -1
  96. package/dist/workspace/status.d.ts.map +1 -1
  97. package/dist/workspace/work-cli.d.ts +78 -0
  98. package/dist/workspace/work-cli.d.ts.map +1 -0
  99. package/package.json +1 -1
  100. package/src/cli/handlers/components.test.ts +93 -0
  101. package/src/cli/handlers/components.ts +44 -3
  102. package/src/cli/handlers/operator.ts +107 -2
  103. package/src/cli/handlers/run.test.ts +19 -0
  104. package/src/cli/handlers/run.ts +53 -1
  105. package/src/cli/handlers/serve.ts +2 -1
  106. package/src/cli/main.test.ts +40 -0
  107. package/src/cli/main.ts +78 -2
  108. package/src/cli/mcp/docs-parity.test.ts +20 -2
  109. package/src/cli/mcp/server.ts +23 -6
  110. package/src/cli/mcp/types.ts +15 -2
  111. package/src/cli/mcp/workspace-tools.test.ts +211 -0
  112. package/src/cli/mcp/workspace-tools.ts +449 -0
  113. package/src/cli/registry.ts +31 -0
  114. package/src/components/verbs/vuln-scan.test.ts +124 -1
  115. package/src/components/verbs/vuln-scan.ts +142 -1
  116. package/src/lifecycle/git.ts +65 -15
  117. package/src/lifecycle/lease.test.ts +22 -0
  118. package/src/lifecycle/lease.ts +133 -29
  119. package/src/lifecycle/member-ledger.ts +3 -2
  120. package/src/lifecycle/plan-ledger.test.ts +148 -0
  121. package/src/lifecycle/plan-ledger.ts +158 -0
  122. package/src/lifecycle/work-lease.test.ts +236 -0
  123. package/src/lifecycle/work-lease.ts +426 -0
  124. package/src/op/builders.ts +5 -0
  125. package/src/op/discover.ts +71 -0
  126. package/src/op/index.ts +16 -3
  127. package/src/op/lifecycle-receipt-store.test.ts +60 -0
  128. package/src/op/lifecycle-receipt-store.ts +61 -0
  129. package/src/op/local-executor.ts +216 -18
  130. package/src/op/local-output.ts +13 -0
  131. package/src/op/op-ir.ts +14 -0
  132. package/src/op/op-verb-class.ts +6 -0
  133. package/src/op/operator.ts +75 -4
  134. package/src/op/runtime.ts +6 -0
  135. package/src/op/runtimes/local.ts +3 -0
  136. package/src/op/step-output-ref.ts +6 -2
  137. package/src/op/steward.test.ts +212 -0
  138. package/src/op/steward.ts +253 -0
  139. package/src/op/types.ts +53 -0
  140. package/src/op/work-lease-decl.ts +80 -0
  141. package/src/op/work-lease-run.test.ts +326 -0
  142. package/src/op/work-lease-run.ts +395 -0
  143. package/src/workspace/box-isolation.test.ts +261 -0
  144. package/src/workspace/box-isolation.ts +205 -0
  145. package/src/workspace/check-contract.test.ts +3 -1
  146. package/src/workspace/check.schema.json +15 -7
  147. package/src/workspace/checks/box-isolation.ts +68 -0
  148. package/src/workspace/checks/boxes.test.ts +197 -0
  149. package/src/workspace/checks/boxes.ts +307 -0
  150. package/src/workspace/checks/records.ts +25 -0
  151. package/src/workspace/checks.test.ts +7 -0
  152. package/src/workspace/checks.ts +19 -2
  153. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +4 -0
  154. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +100 -6
  155. package/src/workspace/conformance/index.mjs +3 -0
  156. package/src/workspace/conformance/index.ts +185 -7
  157. package/src/workspace/conformance/vitest.ts +17 -8
  158. package/src/workspace/decide.test.ts +224 -0
  159. package/src/workspace/decide.ts +576 -0
  160. package/src/workspace/decision-points.schema.json +137 -0
  161. package/src/workspace/declaration.schema.json +172 -0
  162. package/src/workspace/declaration.ts +137 -0
  163. package/src/workspace/declared-kinds.ts +25 -2
  164. package/src/workspace/intent.schema.json +4 -1
  165. package/src/workspace/point-answer.schema.json +95 -0
  166. package/src/workspace/points-cli.ts +273 -0
  167. package/src/workspace/points-write.schema.json +489 -0
  168. package/src/workspace/points.schema.json +710 -0
  169. package/src/workspace/points.test.ts +264 -0
  170. package/src/workspace/points.ts +564 -0
  171. package/src/workspace/read-contract.test.ts +18 -1
  172. package/src/workspace/reason-codes.test.ts +14 -1
  173. package/src/workspace/reason-codes.ts +36 -0
  174. package/src/workspace/record-assets.test.ts +3 -2
  175. package/src/workspace/record-assets.ts +4 -1
  176. package/src/workspace/records-amend.schema.json +2 -1
  177. package/src/workspace/records-cli.ts +15 -2
  178. package/src/workspace/records-close.schema.json +2 -1
  179. package/src/workspace/records-contract.test.ts +3 -2
  180. package/src/workspace/records-new.schema.json +4 -1
  181. package/src/workspace/records-review.schema.json +2 -1
  182. package/src/workspace/records-write.ts +85 -7
  183. package/src/workspace/records.schema.json +18 -0
  184. package/src/workspace/records.ts +61 -3
  185. package/src/workspace/source-block.test.ts +167 -0
  186. package/src/workspace/source-block.ts +129 -0
  187. package/src/workspace/status-contract.test.ts +178 -0
  188. package/src/workspace/status-stewards.ts +225 -0
  189. package/src/workspace/status.schema.json +230 -4
  190. package/src/workspace/status.ts +106 -5
  191. package/src/workspace/work-cli.test.ts +180 -0
  192. package/src/workspace/work-cli.ts +246 -0
  193. package/src/workspace/work-lease.schema.json +233 -0
  194. package/src/workspace/work-readiness-chud.test.ts +145 -0
@@ -0,0 +1,137 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://intentius.io/chant/schemas/workspace/decision-points/v1/decision-points.schema.json",
4
+ "title": "Decision points",
5
+ "description": "A workspace's decision points (ws-058, #2738): the recurring questions it asks of its own graph, declared as data in a JSON file that an answer record kind names in `answers.points`. Each point is a typed question (`noul`, `choice` with 2 to 255 options, or `score` with 2 to 10 ordered levels, the question types of the POST /v1/systemone wire format, #2491), the inputs it reads, each named as a read-contract output, and an ordered chain of deciders: `table` rows, `model` deciders with a backend, a pinned model id and a threshold, and a `quorum` of people, always last. Taken from chud's decision-points.schema.json; `boolean` is read as `noul`. What JSON Schema cannot say is checked in code (`points.ts`): an input's output is one the read contract has, a table row tests declared inputs and answers a candidate, a field belongs to its decider's kind, a model id is pinned rather than an alias, and the chain ends in its one quorum. Unknown fields are refused, except fields whose names start with x-.",
6
+ "type": "object",
7
+ "required": ["points"],
8
+ "properties": {
9
+ "$schema": { "type": "string" },
10
+ "points": {
11
+ "type": "object",
12
+ "minProperties": 1,
13
+ "propertyNames": { "pattern": "^[a-z][a-z0-9-]*$" },
14
+ "additionalProperties": { "$ref": "#/$defs/point" }
15
+ }
16
+ },
17
+ "patternProperties": { "^x-": true },
18
+ "additionalProperties": false,
19
+ "$defs": {
20
+ "text": { "type": "string", "minLength": 1 },
21
+ "point": {
22
+ "type": "object",
23
+ "required": ["title", "question", "inputs", "deciders"],
24
+ "properties": {
25
+ "title": { "$ref": "#/$defs/text" },
26
+ "question": { "$ref": "#/$defs/question" },
27
+ "inputs": {
28
+ "type": "object",
29
+ "description": "The state the deciders see, input name to description. A name is a read-contract output (record, decision, work-item, finding, region, commit, member, gate, release, environment, component), optionally followed by dotted field names, such as work-item.fits_small: a field of that output, or a value the caller derives from it. The caller supplies the values; a table row can only test these names.",
30
+ "minProperties": 1,
31
+ "propertyNames": { "pattern": "^[A-Za-z_][A-Za-z0-9_-]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$" },
32
+ "additionalProperties": { "type": "string" }
33
+ },
34
+ "deciders": {
35
+ "type": "array",
36
+ "minItems": 1,
37
+ "items": { "$ref": "#/$defs/decider" }
38
+ }
39
+ },
40
+ "patternProperties": { "^x-": true },
41
+ "additionalProperties": false
42
+ },
43
+ "question": {
44
+ "type": "object",
45
+ "required": ["type", "instructions", "criteria"],
46
+ "properties": {
47
+ "type": { "enum": ["noul", "boolean", "choice", "score"], "description": "boolean is chud's name for noul, read as noul." },
48
+ "instructions": { "$ref": "#/$defs/text" },
49
+ "criteria": {
50
+ "description": "The candidates. noul: {true, false} descriptions. choice: option to description, 2 to 255 options. score: 2 to 10 ordered levels.",
51
+ "type": ["object", "array"]
52
+ }
53
+ },
54
+ "additionalProperties": false,
55
+ "allOf": [
56
+ {
57
+ "if": { "properties": { "type": { "enum": ["noul", "boolean"] } } },
58
+ "then": {
59
+ "properties": {
60
+ "criteria": {
61
+ "type": "object",
62
+ "required": ["true", "false"],
63
+ "properties": { "true": { "type": "string" }, "false": { "type": "string" } },
64
+ "additionalProperties": false
65
+ }
66
+ }
67
+ }
68
+ },
69
+ {
70
+ "if": { "properties": { "type": { "const": "choice" } } },
71
+ "then": { "properties": { "criteria": { "type": "object", "minProperties": 2, "maxProperties": 255, "additionalProperties": { "type": "string" } } } }
72
+ },
73
+ {
74
+ "if": { "properties": { "type": { "const": "score" } } },
75
+ "then": { "properties": { "criteria": { "type": "array", "minItems": 2, "maxItems": 10, "uniqueItems": true, "items": { "$ref": "#/$defs/text" } } } }
76
+ }
77
+ ]
78
+ },
79
+ "scalar": { "type": ["string", "number", "boolean", "null"] },
80
+ "condition": {
81
+ "description": "A value the input must equal, or one comparison.",
82
+ "oneOf": [
83
+ { "$ref": "#/$defs/scalar" },
84
+ {
85
+ "type": "object",
86
+ "minProperties": 1,
87
+ "maxProperties": 1,
88
+ "properties": {
89
+ "eq": { "$ref": "#/$defs/scalar" },
90
+ "ne": { "$ref": "#/$defs/scalar" },
91
+ "lt": { "type": "number" },
92
+ "lte": { "type": "number" },
93
+ "gt": { "type": "number" },
94
+ "gte": { "type": "number" },
95
+ "in": { "type": "array", "items": { "$ref": "#/$defs/scalar" } }
96
+ },
97
+ "additionalProperties": false
98
+ }
99
+ ]
100
+ },
101
+ "row": {
102
+ "type": "object",
103
+ "required": ["when", "answer"],
104
+ "properties": {
105
+ "when": {
106
+ "type": "object",
107
+ "description": "Every named input must meet its condition. An empty object matches every state.",
108
+ "additionalProperties": { "$ref": "#/$defs/condition" }
109
+ },
110
+ "answer": { "type": ["string", "boolean"] },
111
+ "note": { "type": "string" }
112
+ },
113
+ "additionalProperties": false
114
+ },
115
+ "decider": {
116
+ "type": "object",
117
+ "required": ["kind"],
118
+ "properties": {
119
+ "kind": { "enum": ["table", "model", "quorum"] },
120
+ "rows": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/row" } },
121
+ "backend": { "type": "string", "pattern": "^[a-z][a-z0-9-]*$", "description": "model: which backend answers, such as systemone (any server with the POST /v1/systemone wire format). chant never calls it: the decide Op activity or a runtime's decider does." },
122
+ "model": { "type": "string", "minLength": 1, "description": "model: the pinned model id the backend is asked for, such as jev-1.13.0. An alias such as jev-latest is refused." },
123
+ "threshold": { "type": "number", "exclusiveMinimum": 0, "maximum": 1, "description": "model: the least confidence that makes the answer a proposal. Below it the answer is not observed and the next decider is asked." },
124
+ "unreachable": { "enum": ["escalate", "fail"], "description": "model: what happens when the backend can't be reached or gives no usable answer: escalate to the next decider (the default), or fail the ask and write nothing." },
125
+ "count": { "type": "integer", "minimum": 1, "description": "quorum: how many distinct people must answer." },
126
+ "roles": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/text" }, "description": "quorum: the trust policy roles whose holders count." },
127
+ "note": { "type": "string" }
128
+ },
129
+ "additionalProperties": false,
130
+ "allOf": [
131
+ { "if": { "properties": { "kind": { "const": "table" } } }, "then": { "required": ["rows"] } },
132
+ { "if": { "properties": { "kind": { "const": "model" } } }, "then": { "required": ["backend", "model", "threshold"] } },
133
+ { "if": { "properties": { "kind": { "const": "quorum" } } }, "then": { "required": ["count"] } }
134
+ ]
135
+ }
136
+ }
137
+ }
@@ -56,6 +56,13 @@
56
56
  "$ref": "#/$defs/recordKind"
57
57
  }
58
58
  },
59
+ "hosts": {
60
+ "type": "array",
61
+ "description": "The hosts boxes run on (#2727). A host is one machine reached under one hostname: boxes on it share its ports, its state root and, in a browser, its cookie jar, so chant workspace check compares the boxes on one host and never boxes on different hosts. A member's box block names its host. Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
62
+ "items": {
63
+ "$ref": "#/$defs/host"
64
+ }
65
+ },
59
66
  "checks": {
60
67
  "type": "object",
61
68
  "description": "Severity for the declaration checks that allow it, keyed by WSP id (#2535, ws-028): error, warning, info or off. chant workspace check refuses an id it does not know, and one whose severity is fixed.",
@@ -269,6 +276,9 @@
269
276
  "$ref": "#/$defs/recordKind"
270
277
  }
271
278
  },
279
+ "box": {
280
+ "$ref": "#/$defs/box"
281
+ },
272
282
  "upstream": {
273
283
  "type": "string",
274
284
  "minLength": 1,
@@ -354,6 +364,114 @@
354
364
  "description": "A file or directory path with / separators: no leading /, no . or .. segments and no trailing /.",
355
365
  "pattern": "^(?!\\.\\.?(/|$))[^/\\\\]+(/(?!\\.\\.?(/|$))[^/\\\\]+)*$"
356
366
  },
367
+ "box": {
368
+ "type": "object",
369
+ "description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
370
+ "properties": {
371
+ "capabilities": {
372
+ "type": "array",
373
+ "description": "Each capability the box reaches outside itself, named once. None when omitted.",
374
+ "items": {
375
+ "$ref": "#/$defs/capability"
376
+ }
377
+ },
378
+ "host": {
379
+ "$ref": "#/$defs/name",
380
+ "description": "The host the box runs on, one of the top-level hosts (#2727). The box's name is the member's name."
381
+ },
382
+ "slot": {
383
+ "type": "integer",
384
+ "minimum": 0,
385
+ "description": "The box's number on its host (#2727). Its ports come from the slot's block of the host's range."
386
+ },
387
+ "ports": {
388
+ "type": "object",
389
+ "description": "The ports the box listens on, each a name and its offset in the box's block (#2727). The port is the block's first port plus the offset.",
390
+ "propertyNames": {
391
+ "$ref": "#/$defs/kindName"
392
+ },
393
+ "additionalProperties": {
394
+ "type": "integer",
395
+ "minimum": 0,
396
+ "maximum": 65534
397
+ }
398
+ },
399
+ "state": {
400
+ "type": "object",
401
+ "description": "State the box keeps outside its checkout, such as hud's identity and database, each a name and a path relative to the box's state directory (#2727). The name is usually the environment variable the runtime sets, such as HUD_IDENTITY_PATH. A path that is absolute, starts with ~ or an environment reference, or has a .. segment is a WSP124 finding.",
402
+ "propertyNames": {
403
+ "type": "string",
404
+ "pattern": "^[A-Za-z_][A-Za-z0-9_-]{0,63}$"
405
+ },
406
+ "additionalProperties": {
407
+ "type": "string",
408
+ "minLength": 1
409
+ }
410
+ },
411
+ "cookies": {
412
+ "type": "array",
413
+ "description": "Session cookie names the box's runtimes set, such as hud_session (#2727). A browser scopes a cookie to a host, not a port, so each resolves to <cookie>_<member name>.",
414
+ "uniqueItems": true,
415
+ "items": {
416
+ "type": "string",
417
+ "pattern": "^[A-Za-z][A-Za-z0-9_-]{0,63}$"
418
+ }
419
+ }
420
+ },
421
+ "dependentRequired": {
422
+ "host": [
423
+ "slot"
424
+ ],
425
+ "slot": [
426
+ "host"
427
+ ],
428
+ "ports": [
429
+ "host"
430
+ ],
431
+ "state": [
432
+ "host"
433
+ ],
434
+ "cookies": [
435
+ "host"
436
+ ]
437
+ },
438
+ "patternProperties": {
439
+ "^x-": true
440
+ },
441
+ "additionalProperties": false
442
+ },
443
+ "capability": {
444
+ "type": "object",
445
+ "description": "A capability a box needs and does not hold the credential for, such as inference, fountain or a third-party API (#2726).",
446
+ "required": [
447
+ "name"
448
+ ],
449
+ "properties": {
450
+ "name": {
451
+ "$ref": "#/$defs/kindName",
452
+ "description": "The capability, such as inference or fountain. Unique within the box."
453
+ },
454
+ "broker": {
455
+ "type": "string",
456
+ "minLength": 1,
457
+ "description": "What brokers the capability: the runtime that holds the credential and swaps the box's own token for it, such as lobby or door. It may name a member of this workspace, or a runtime outside it. Without it the capability is unbrokered, and chant workspace check fails with WSP122."
458
+ },
459
+ "scope": {
460
+ "type": "array",
461
+ "uniqueItems": true,
462
+ "description": "What of the box's own the broker lets it reach, which the broker enforces. For Fountain: agent (start conversations with its own agent), vault (attach its own vault), conversations and sandboxes (its own). The words are the broker's; chant records them.",
463
+ "items": {
464
+ "type": "string",
465
+ "minLength": 1,
466
+ "pattern": "^[^\\u0000-\\u001f]+$"
467
+ }
468
+ }
469
+ },
470
+ "patternProperties": {
471
+ "^x-": true
472
+ },
473
+ "additionalProperties": false
474
+ },
357
475
  "recordKind": {
358
476
  "type": "object",
359
477
  "description": "A record kind the workspace declares (#2680): a kind file that exports recordKind. chant workspace check fails (WSP115) when the file is missing or does not load as a record kind.",
@@ -469,6 +587,60 @@
469
587
  "additionalProperties": false
470
588
  }
471
589
  ]
590
+ },
591
+ "host": {
592
+ "type": "object",
593
+ "description": "A host boxes run on (#2727).",
594
+ "required": [
595
+ "name",
596
+ "ports"
597
+ ],
598
+ "properties": {
599
+ "name": {
600
+ "$ref": "#/$defs/name",
601
+ "description": "The host's name, unique among hosts. A box block names it in host."
602
+ },
603
+ "ports": {
604
+ "type": "object",
605
+ "description": "The TCP ports boxes on this host take theirs from. Slot n owns the block from + n * perBox to from + (n + 1) * perBox - 1, which must end at or before to.",
606
+ "required": [
607
+ "from",
608
+ "to",
609
+ "perBox"
610
+ ],
611
+ "properties": {
612
+ "from": {
613
+ "$ref": "#/$defs/port",
614
+ "description": "The first port of the range."
615
+ },
616
+ "to": {
617
+ "$ref": "#/$defs/port",
618
+ "description": "The last port of the range, inclusive."
619
+ },
620
+ "perBox": {
621
+ "type": "integer",
622
+ "minimum": 1,
623
+ "maximum": 65535,
624
+ "description": "How many ports each box's block holds. A box's port offsets are below it."
625
+ }
626
+ },
627
+ "additionalProperties": false
628
+ },
629
+ "stateRoot": {
630
+ "type": "string",
631
+ "minLength": 1,
632
+ "description": "Where boxes on this host keep state, as an environment reference such as ${XDG_STATE_HOME}/chant/boxes, which the runtime expands on the machine. A box's state directory is <stateRoot>/<member name>. A literal machine path, such as /Users/... or anything under $HOME, is a WSP124 finding. Default ${XDG_STATE_HOME}/chant/boxes."
633
+ }
634
+ },
635
+ "patternProperties": {
636
+ "^x-": true
637
+ },
638
+ "additionalProperties": false
639
+ },
640
+ "port": {
641
+ "type": "integer",
642
+ "minimum": 1,
643
+ "maximum": 65535
472
644
  }
473
645
  }
474
646
  }
@@ -145,6 +145,54 @@ export interface RecordKindDeclaration {
145
145
  pointer: string;
146
146
  }
147
147
 
148
+ /**
149
+ * A capability a box reaches through a broker (#2726): inference, Fountain,
150
+ * a third-party API. The broker is runtime (a lobby, a door, a studio) and
151
+ * holds the credential; the declaration names it and the scope it enforces.
152
+ */
153
+ export interface BoxCapability {
154
+ name: string;
155
+ /** What brokers it, such as `lobby`, or null when the entry names no broker (WSP122). */
156
+ broker: string | null;
157
+ /** What of the box's own the broker lets it reach, such as `agent`, `vault`, `conversations`, `sandboxes`. */
158
+ scope: string[];
159
+ /** The entry's JSON Pointer in the file, for messages. */
160
+ pointer: string;
161
+ }
162
+
163
+ /** A member that is a box, or a box's declarations (#2726). */
164
+ export interface BoxDeclaration {
165
+ capabilities: BoxCapability[];
166
+ /** The host and slot of the box's isolation, or null when the block declares none (#2727). The values are derived in `boxes.ts`. */
167
+ isolation: BoxIsolationDeclaration | null;
168
+ /** The block's JSON Pointer in the file, for messages. */
169
+ pointer: string;
170
+ }
171
+
172
+ /** What a box declares about its isolation (#2727): its identity on a host, and the names of what it needs kept apart. */
173
+ export interface BoxIsolationDeclaration {
174
+ host: string;
175
+ slot: number;
176
+ /** Port name to offset in the box's block, in file order. */
177
+ ports: Record<string, number>;
178
+ /** State name to a path relative to the box's state directory, in file order. */
179
+ state: Record<string, string>;
180
+ cookies: string[];
181
+ }
182
+
183
+ /** A host boxes run on (#2727): one machine under one hostname. */
184
+ export interface Host {
185
+ name: string;
186
+ ports: { from: number; to: number; perBox: number };
187
+ /** As written, or null for the default ({@link DEFAULT_STATE_ROOT}). */
188
+ stateRoot: string | null;
189
+ /** The entry's JSON Pointer in the file, for messages. */
190
+ pointer: string;
191
+ }
192
+
193
+ /** Where boxes keep state when their host names no stateRoot (#2727). */
194
+ export const DEFAULT_STATE_ROOT = "${XDG_STATE_HOME}/chant/boxes";
195
+
148
196
  export interface Member {
149
197
  type: "member";
150
198
  name: string;
@@ -160,6 +208,8 @@ export interface Member {
160
208
  links: LinkDeclaration[];
161
209
  /** The record kinds this member declares, in file order (#2680). */
162
210
  records: RecordKindDeclaration[];
211
+ /** The member's box block, or null when it declares none (#2726). */
212
+ box: BoxDeclaration | null;
163
213
  upstream: string | null;
164
214
  because: string | null;
165
215
  suppress: Suppression[];
@@ -202,6 +252,8 @@ export interface Declaration {
202
252
  quorum: number | null;
203
253
  /** The workspace's own record kinds, from the top-level `records`, in file order (#2680). A member's are on the member. */
204
254
  records: RecordKindDeclaration[];
255
+ /** The hosts boxes run on, in file order (#2727). */
256
+ hosts: Host[];
205
257
  /** The file, relative to the workspace root's tree (`chant.workspace.json` or `.jsonc`). */
206
258
  file: string;
207
259
  }
@@ -414,6 +466,7 @@ export function parseDeclaration(text: string, file: string, reader: string = re
414
466
  pointer: `${pointer}/links/${j}`,
415
467
  }));
416
468
  const records = recordKindsOf(e.records, e.dir as string, e.name as string, `${pointer}/records`);
469
+ const box = boxOf(e.box, `${pointer}/box`);
417
470
  return {
418
471
  type: "member",
419
472
  name: e.name as string,
@@ -424,6 +477,7 @@ export function parseDeclaration(text: string, file: string, reader: string = re
424
477
  outputs: e.outputs === undefined ? null : [...(e.outputs as string[])],
425
478
  links,
426
479
  records,
480
+ box,
427
481
  upstream: (e.upstream as string | undefined) ?? null,
428
482
  because: (e.because as string | undefined) ?? null,
429
483
  suppress,
@@ -508,6 +562,18 @@ export function parseDeclaration(text: string, file: string, reader: string = re
508
562
  byName.set(r.name, r);
509
563
  }
510
564
 
565
+ // A box names each capability once (#2726): a broker reads its scope by name.
566
+ for (const m of members) {
567
+ const seen = new Map<string, BoxCapability>();
568
+ for (const c of m.box?.capabilities ?? []) {
569
+ const first = seen.get(c.name);
570
+ if (first) throw new WorkspaceReadError("declaration-invalid", `member ${m.name}'s box lists the capability ${c.name} twice; the first is at ${first.pointer}`, at(`${c.pointer}/name`));
571
+ seen.set(c.name, c);
572
+ }
573
+ }
574
+
575
+ const hosts = hostsOf(obj, members, at);
576
+
511
577
  const pins = ((obj.pins as Record<string, string>[] | undefined) ?? []).map((p) => ({
512
578
  package: p.package ?? null,
513
579
  version: p.version ?? null,
@@ -526,6 +592,7 @@ export function parseDeclaration(text: string, file: string, reader: string = re
526
592
  checks: { ...((obj.checks as Record<string, CheckSeverity> | undefined) ?? {}) },
527
593
  quorum: typeof obj.quorum === "number" ? obj.quorum : null,
528
594
  records: ownRecords,
595
+ hosts,
529
596
  file,
530
597
  };
531
598
  }
@@ -541,6 +608,76 @@ function recordKindsOf(raw: unknown, dir: string, member: string | null, pointer
541
608
  }));
542
609
  }
543
610
 
611
+ /** The `box` block at `pointer`, already validated, or null when there is none (#2726, #2727). */
612
+ function boxOf(raw: unknown, pointer: string): BoxDeclaration | null {
613
+ if (raw === undefined) return null;
614
+ const b = raw as {
615
+ capabilities?: { name: string; broker?: string; scope?: string[] }[];
616
+ host?: string;
617
+ slot?: number;
618
+ ports?: Record<string, number>;
619
+ state?: Record<string, string>;
620
+ cookies?: string[];
621
+ };
622
+ return {
623
+ capabilities: (b.capabilities ?? []).map((c, i) => ({ name: c.name, broker: c.broker ?? null, scope: [...(c.scope ?? [])], pointer: `${pointer}/capabilities/${i}` })),
624
+ // The schema requires host and slot together, and host for ports, state and cookies.
625
+ isolation:
626
+ b.host === undefined
627
+ ? null
628
+ : { host: b.host, slot: b.slot!, ports: { ...(b.ports ?? {}) }, state: { ...(b.state ?? {}) }, cookies: [...(b.cookies ?? [])] },
629
+ pointer,
630
+ };
631
+ }
632
+
633
+ /**
634
+ * The hosts, already validated, with the rules the schema can't say (#2727):
635
+ * host names are unique, a box's host is declared, its slot's block fits the
636
+ * host's range and its offsets fit the block. What two boxes resolve to is
637
+ * `chant workspace check`'s question (WSP123), not a read error.
638
+ */
639
+ function hostsOf(obj: Record<string, unknown>, members: Member[], at: (pointer: string, key?: boolean) => ErrorLocation): Host[] {
640
+ const hosts = ((obj.hosts as Record<string, unknown>[] | undefined) ?? []).map((h, i): Host => ({
641
+ name: h.name as string,
642
+ ports: { ...(h.ports as Host["ports"]) },
643
+ stateRoot: (h.stateRoot as string | undefined) ?? null,
644
+ pointer: `/hosts/${i}`,
645
+ }));
646
+ const invalid = (message: string, pointer: string) => new WorkspaceReadError("declaration-invalid", message, at(pointer));
647
+ const byName = new Map<string, Host>();
648
+ for (const h of hosts) {
649
+ const first = byName.get(h.name);
650
+ if (first) throw invalid(`the host name ${JSON.stringify(h.name)} is already used by the host at ${first.pointer}`, `${h.pointer}/name`);
651
+ byName.set(h.name, h);
652
+ if (h.ports.from > h.ports.to) throw invalid(`host ${h.name}'s port range starts at ${h.ports.from}, after its end ${h.ports.to}`, `${h.pointer}/ports`);
653
+ }
654
+ for (const m of members) {
655
+ const iso = m.box?.isolation;
656
+ if (!iso) continue;
657
+ const box = m.box!.pointer;
658
+ const host = byName.get(iso.host);
659
+ if (!host) {
660
+ const known = hosts.map((h) => h.name).join(", ") || "none are declared";
661
+ throw invalid(`member ${m.name}'s box names the host ${JSON.stringify(iso.host)}, which hosts does not declare; declared hosts: ${known}`, `${box}/host`);
662
+ }
663
+ const { from, to, perBox } = host.ports;
664
+ const last = from + (iso.slot + 1) * perBox - 1;
665
+ if (last > to) {
666
+ const slots = Math.floor((to - from + 1) / perBox);
667
+ throw invalid(
668
+ `member ${m.name}'s box has slot ${iso.slot} on host ${host.name}, which needs ports ${from + iso.slot * perBox} to ${last}, past the end of the host's range (${to}); the range holds ${slots} slot${slots === 1 ? "" : "s"}`,
669
+ `${box}/slot`,
670
+ );
671
+ }
672
+ for (const [port, offset] of Object.entries(iso.ports)) {
673
+ if (offset >= perBox) {
674
+ throw invalid(`member ${m.name}'s box gives port ${port} offset ${offset}, and host ${host.name} gives each box ${perBox} port${perBox === 1 ? "" : "s"} (offsets 0 to ${perBox - 1})`, `${box}/ports/${pointerToken(port)}`);
675
+ }
676
+ }
677
+ }
678
+ return hosts;
679
+ }
680
+
544
681
  /**
545
682
  * Every record kind the declaration names (#2680), in the order readers use
546
683
  * them: the workspace's own first, then each member's, members in file order
@@ -13,7 +13,7 @@
13
13
  * kind file.
14
14
  */
15
15
 
16
- import { join } from "node:path";
16
+ import { dirname, join, relative, resolve, sep } from "node:path";
17
17
  import { declaredRecordKinds, type Declaration, type RecordKindDeclaration } from "./declaration";
18
18
  import type { ReasonCode } from "./reason-codes";
19
19
  import type { WorkspaceTree } from "./tree";
@@ -30,6 +30,12 @@ export interface DeclaredKind {
30
30
  kind: string | null;
31
31
  /** Why it can't be loaded, or null. */
32
32
  reason: { code: DeclaredKindReasonCode; message: string } | null;
33
+ /**
34
+ * For an answer kind (ws-058, #2738): its points file from the workspace
35
+ * root, and what is wrong with it, empty when nothing is. Absent for any
36
+ * other kind, and when the kind wasn't loaded.
37
+ */
38
+ points?: { file: string; problems: { field: string | null; message: string }[] };
33
39
  }
34
40
 
35
41
  /** The kind file of `declared` on disk, under the workspace root `rootOnDisk`. */
@@ -65,7 +71,9 @@ export async function loadDeclaredKinds(
65
71
  }
66
72
  try {
67
73
  const loaded = await records.loadRecordKind(file);
68
- out.push({ declared: d, file, kind: loaded.kind.name, reason: null });
74
+ const entry: DeclaredKind = { declared: d, file, kind: loaded.kind.name, reason: null };
75
+ if (loaded.kind.answers) entry.points = await checkPoints(resolve(dirname(file), loaded.kind.answers.points), rootOnDisk, tree);
76
+ out.push(entry);
69
77
  } catch (err) {
70
78
  if (!(err instanceof records.RecordReadError)) throw err;
71
79
  // loadRecordKind names the file as it was given, here an absolute path: name it from the workspace root instead.
@@ -74,3 +82,18 @@ export async function loadDeclaredKinds(
74
82
  }
75
83
  return out;
76
84
  }
85
+
86
+ /** An answer kind's points file (ws-058), read from `tree` and validated as `points ask` reads it. */
87
+ async function checkPoints(abs: string, rootOnDisk: string, tree: WorkspaceTree): Promise<NonNullable<DeclaredKind["points"]>> {
88
+ const file = relative(rootOnDisk, abs).split(sep).join("/");
89
+ if (file.startsWith("../")) return { file, problems: [{ field: null, message: "is outside the workspace" }] };
90
+ if (tree.stat(file) !== "file") return { file, problems: [{ field: null, message: `does not exist${tree.label}` }] };
91
+ const { parsePoints, PointsError } = await import("./points");
92
+ try {
93
+ parsePoints(tree.read(file), file);
94
+ return { file, problems: [] };
95
+ } catch (err) {
96
+ if (err instanceof PointsError) return { file, problems: err.problems };
97
+ throw err;
98
+ }
99
+ }
@@ -940,7 +940,10 @@
940
940
  "work-implements-undecided",
941
941
  "work-done-unpinned",
942
942
  "work-closed-without-date",
943
- "work-done-gap-open"
943
+ "work-done-gap-open",
944
+ "answer-points-unreadable",
945
+ "answer-point-unknown",
946
+ "answer-point-changed"
944
947
  ]
945
948
  },
946
949
  "message": {