@intentius/chant 0.85.0 → 0.87.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 (129) hide show
  1. package/dist/cli/main.d.ts.map +1 -1
  2. package/dist/cli/registry.d.ts +13 -1
  3. package/dist/cli/registry.d.ts.map +1 -1
  4. package/dist/lifecycle/gate-ledger.d.ts +13 -0
  5. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  6. package/dist/workspace/__fixtures__/sessions.d.ts +23 -0
  7. package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -0
  8. package/dist/workspace/checks/records.d.ts +1 -0
  9. package/dist/workspace/checks/records.d.ts.map +1 -1
  10. package/dist/workspace/checks.d.ts +4 -0
  11. package/dist/workspace/checks.d.ts.map +1 -1
  12. package/dist/workspace/composites.d.ts +14 -1
  13. package/dist/workspace/composites.d.ts.map +1 -1
  14. package/dist/workspace/conformance/index.d.ts +211 -0
  15. package/dist/workspace/conformance/index.d.ts.map +1 -0
  16. package/dist/workspace/conformance/vitest.d.ts +11 -0
  17. package/dist/workspace/conformance/vitest.d.ts.map +1 -0
  18. package/dist/workspace/declaration.d.ts +28 -0
  19. package/dist/workspace/declaration.d.ts.map +1 -1
  20. package/dist/workspace/declaration.schema.json +40 -0
  21. package/dist/workspace/declared-kinds.d.ts +43 -0
  22. package/dist/workspace/declared-kinds.d.ts.map +1 -0
  23. package/dist/workspace/graph-cli.d.ts +11 -0
  24. package/dist/workspace/graph-cli.d.ts.map +1 -1
  25. package/dist/workspace/intent-cli.d.ts +2 -1
  26. package/dist/workspace/intent-cli.d.ts.map +1 -1
  27. package/dist/workspace/intent-joins.d.ts +45 -8
  28. package/dist/workspace/intent-joins.d.ts.map +1 -1
  29. package/dist/workspace/intent.d.ts +71 -7
  30. package/dist/workspace/intent.d.ts.map +1 -1
  31. package/dist/workspace/ls.d.ts +31 -1
  32. package/dist/workspace/ls.d.ts.map +1 -1
  33. package/dist/workspace/reason-codes.d.ts +47 -4
  34. package/dist/workspace/reason-codes.d.ts.map +1 -1
  35. package/dist/workspace/record-sessions.d.ts +51 -0
  36. package/dist/workspace/record-sessions.d.ts.map +1 -0
  37. package/dist/workspace/record-source.d.ts +2 -0
  38. package/dist/workspace/record-source.d.ts.map +1 -1
  39. package/dist/workspace/records-cli.d.ts +71 -4
  40. package/dist/workspace/records-cli.d.ts.map +1 -1
  41. package/dist/workspace/records-since.d.ts +90 -0
  42. package/dist/workspace/records-since.d.ts.map +1 -0
  43. package/dist/workspace/records-write.d.ts +171 -0
  44. package/dist/workspace/records-write.d.ts.map +1 -0
  45. package/dist/workspace/records.d.ts +244 -15
  46. package/dist/workspace/records.d.ts.map +1 -1
  47. package/dist/workspace/runtimes.d.ts +60 -0
  48. package/dist/workspace/runtimes.d.ts.map +1 -0
  49. package/dist/workspace/status-gates.d.ts +90 -0
  50. package/dist/workspace/status-gates.d.ts.map +1 -0
  51. package/dist/workspace/status.d.ts +17 -0
  52. package/dist/workspace/status.d.ts.map +1 -1
  53. package/dist/workspace/trust/seal.d.ts +85 -0
  54. package/dist/workspace/trust/seal.d.ts.map +1 -0
  55. package/dist/workspace/trust/ssh-commit.d.ts +7 -0
  56. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  57. package/dist/workspace/work.d.ts +56 -0
  58. package/dist/workspace/work.d.ts.map +1 -0
  59. package/package.json +19 -1
  60. package/src/cli/main.ts +55 -3
  61. package/src/cli/registry.ts +13 -1
  62. package/src/lifecycle/gate-ledger.ts +14 -0
  63. package/src/workspace/__fixtures__/sessions.ts +66 -0
  64. package/src/workspace/checks/records.ts +19 -0
  65. package/src/workspace/checks.test.ts +2 -0
  66. package/src/workspace/checks.ts +7 -1
  67. package/src/workspace/composites.schema.json +65 -3
  68. package/src/workspace/composites.test.ts +95 -5
  69. package/src/workspace/composites.ts +28 -7
  70. package/src/workspace/conformance/__fixture__/app/package.json +7 -0
  71. package/src/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
  72. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +32 -0
  73. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +376 -0
  74. package/src/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
  75. package/src/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
  76. package/src/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
  77. package/src/workspace/conformance/__fixture__/delivery/package.json +7 -0
  78. package/src/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
  79. package/src/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
  80. package/src/workspace/conformance/conformance.test.ts +149 -0
  81. package/src/workspace/conformance/index.mjs +31 -0
  82. package/src/workspace/conformance/index.ts +453 -0
  83. package/src/workspace/conformance/vitest.ts +62 -0
  84. package/src/workspace/declaration.schema.json +40 -0
  85. package/src/workspace/declaration.ts +62 -0
  86. package/src/workspace/declared-kinds.test.ts +321 -0
  87. package/src/workspace/declared-kinds.ts +76 -0
  88. package/src/workspace/graph-cli.ts +8 -0
  89. package/src/workspace/intent-cli.ts +29 -6
  90. package/src/workspace/intent-gaps.test.ts +217 -0
  91. package/src/workspace/intent-joins.test.ts +60 -0
  92. package/src/workspace/intent-joins.ts +71 -19
  93. package/src/workspace/intent.schema.json +304 -7
  94. package/src/workspace/intent.test.ts +99 -0
  95. package/src/workspace/intent.ts +365 -46
  96. package/src/workspace/ls.schema.json +34 -0
  97. package/src/workspace/ls.ts +69 -4
  98. package/src/workspace/read-contract.test.ts +30 -9
  99. package/src/workspace/reason-codes.test.ts +16 -4
  100. package/src/workspace/reason-codes.ts +55 -4
  101. package/src/workspace/record-assets.test.ts +3 -1
  102. package/src/workspace/record-sessions.ts +105 -0
  103. package/src/workspace/record-source.ts +14 -5
  104. package/src/workspace/records-amend.schema.json +167 -0
  105. package/src/workspace/records-cli.ts +308 -19
  106. package/src/workspace/records-contract.test.ts +57 -2
  107. package/src/workspace/records-formats.test.ts +640 -0
  108. package/src/workspace/records-new.schema.json +158 -0
  109. package/src/workspace/records-quorum.test.ts +196 -0
  110. package/src/workspace/records-review.schema.json +227 -0
  111. package/src/workspace/records-sessions.test.ts +108 -0
  112. package/src/workspace/records-since.schema.json +193 -0
  113. package/src/workspace/records-since.test.ts +174 -0
  114. package/src/workspace/records-since.ts +259 -0
  115. package/src/workspace/records-write-contract.test.ts +125 -0
  116. package/src/workspace/records-write.test.ts +373 -0
  117. package/src/workspace/records-write.ts +765 -0
  118. package/src/workspace/records.schema.json +202 -9
  119. package/src/workspace/records.ts +700 -41
  120. package/src/workspace/runtimes.ts +107 -0
  121. package/src/workspace/status-contract.test.ts +163 -0
  122. package/src/workspace/status-gates.ts +215 -0
  123. package/src/workspace/status.schema.json +69 -3
  124. package/src/workspace/status.ts +35 -2
  125. package/src/workspace/trust/seal.test.ts +232 -0
  126. package/src/workspace/trust/seal.ts +195 -0
  127. package/src/workspace/trust/ssh-commit.ts +2 -2
  128. package/src/workspace/work.test.ts +390 -0
  129. package/src/workspace/work.ts +163 -0
@@ -0,0 +1,376 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "urn:intentius:chant:decision:1",
4
+ "title": "Decision file front matter",
5
+ "description": "The front matter of one decision file in docs/design/decisions/. The format is described in README.md beside this file. Values stay within what YAML and JSON share, so a file can be sealed later (#2524 D4).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "patternProperties": {
9
+ "^x-": {}
10
+ },
11
+ "required": [
12
+ "schema",
13
+ "id",
14
+ "title",
15
+ "state",
16
+ "area",
17
+ "source",
18
+ "question",
19
+ "options",
20
+ "choice",
21
+ "rejected",
22
+ "supersedes",
23
+ "evidence",
24
+ "decided_by",
25
+ "decided_on",
26
+ "reviews",
27
+ "constrains"
28
+ ],
29
+ "properties": {
30
+ "schema": {
31
+ "description": "Format version of this file. Bumped only by a change to this schema.",
32
+ "const": 1
33
+ },
34
+ "id": {
35
+ "description": "Stable id: a lowercase prefix per epic, a dash and three or more digits. Never reused, never renumbered.",
36
+ "type": "string",
37
+ "pattern": "^[a-z][a-z0-9]{0,15}-[0-9]{3,}$"
38
+ },
39
+ "title": {
40
+ "description": "Short topic, as the source table names it, without revision markers.",
41
+ "type": "string",
42
+ "minLength": 1
43
+ },
44
+ "state": {
45
+ "description": "Lifecycle state, from #2555.",
46
+ "enum": ["proposed", "decided", "ratified", "superseded", "withdrawn"]
47
+ },
48
+ "area": {
49
+ "description": "The design section the decision belongs to, such as \"D5\". Groups the review queue. Null only in an importer draft.",
50
+ "type": ["string", "null"]
51
+ },
52
+ "source": {
53
+ "description": "Where the decision was first recorded: a row of an issue's decisions table, or the workspace itself when no issue holds the decision (#2654).",
54
+ "oneOf": [{ "$ref": "#/definitions/sourceIssue" }, { "$ref": "#/definitions/sourceWorkspace" }]
55
+ },
56
+ "question": {
57
+ "description": "One sentence. Null only in an importer draft.",
58
+ "type": ["string", "null"]
59
+ },
60
+ "options": {
61
+ "description": "Every option considered, the chosen one included.",
62
+ "type": "array",
63
+ "minItems": 1,
64
+ "items": { "$ref": "#/definitions/option" }
65
+ },
66
+ "choice": {
67
+ "description": "The chosen option and why. Null in state proposed or withdrawn.",
68
+ "oneOf": [
69
+ { "type": "null" },
70
+ {
71
+ "type": "object",
72
+ "additionalProperties": false,
73
+ "required": ["option", "reason"],
74
+ "properties": {
75
+ "option": { "$ref": "#/definitions/optionId" },
76
+ "reason": { "type": ["string", "null"] }
77
+ }
78
+ }
79
+ ]
80
+ },
81
+ "rejected": {
82
+ "description": "The options not chosen, kept on the record.",
83
+ "type": "array",
84
+ "items": {
85
+ "type": "object",
86
+ "additionalProperties": false,
87
+ "required": ["option", "why"],
88
+ "properties": {
89
+ "option": { "$ref": "#/definitions/optionId" },
90
+ "why": { "type": ["string", "null"] }
91
+ }
92
+ }
93
+ },
94
+ "supersedes": {
95
+ "description": "Earlier choices this decision replaced: another decision file, or an option chosen in an earlier revision of the same design before decision files existed.",
96
+ "type": "array",
97
+ "items": {
98
+ "oneOf": [
99
+ {
100
+ "type": "object",
101
+ "additionalProperties": false,
102
+ "required": ["decision"],
103
+ "properties": {
104
+ "decision": { "$ref": "#/definitions/decisionId" }
105
+ }
106
+ },
107
+ {
108
+ "type": "object",
109
+ "additionalProperties": false,
110
+ "required": ["revision", "option"],
111
+ "properties": {
112
+ "revision": { "type": "string", "pattern": "^v[0-9]+$" },
113
+ "option": { "$ref": "#/definitions/optionId" }
114
+ }
115
+ }
116
+ ]
117
+ }
118
+ },
119
+ "evidence": {
120
+ "description": "The research, audits, design sections and workspace files behind the decision. A public link carries a url; a file in the workspace carries a path and the hash of its bytes (#2549). May be empty when the decision cites nothing; chant workspace records then warns decision-no-evidence (#2654).",
121
+ "type": "array",
122
+ "items": {
123
+ "oneOf": [{ "$ref": "#/definitions/evidenceLink" }, { "$ref": "#/definitions/evidencePin" }]
124
+ }
125
+ },
126
+ "decided_by": {
127
+ "description": "Who chose. A forge login. Null in state proposed or withdrawn.",
128
+ "type": ["string", "null"]
129
+ },
130
+ "decided_on": {
131
+ "description": "When the choice was made, as YYYY-MM-DD. Quoted, so YAML reads it as a string.",
132
+ "type": ["string", "null"],
133
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
134
+ },
135
+ "reviews": {
136
+ "description": "Each reviewer's verdict. Empty until a review happens.",
137
+ "type": "array",
138
+ "items": {
139
+ "type": "object",
140
+ "additionalProperties": false,
141
+ "required": ["reviewer", "verdict", "on"],
142
+ "properties": {
143
+ "reviewer": { "type": "string", "minLength": 1 },
144
+ "verdict": { "enum": ["agree", "dissent", "abstain"] },
145
+ "note": { "type": ["string", "null"] },
146
+ "on": { "type": "string", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$" },
147
+ "digest": {
148
+ "description": "The digest of the record text the verdict judged: the lowercase hex SHA-256 of the file with LF line endings and without its reviews block (#2672). A verdict whose digest is not the record's digest now stops counting toward the quorum. A verdict without one still counts, with the warning review-undigested.",
149
+ "type": "string",
150
+ "pattern": "^[0-9a-f]{64}$"
151
+ },
152
+ "session": {
153
+ "description": "The id of the review session the verdict was given in, as the session names it (#2650 C10). chant workspace records review --session writes it (#2670).",
154
+ "type": "string",
155
+ "minLength": 1
156
+ },
157
+ "seal": {
158
+ "description": "The reviewer's ssh signature over the verdict (#2687): the bytes <id>\\n<digest>\\n<verdict>\\n<reviewer>\\n<on>, in the ssh-keygen namespace chant-review. chant workspace records review --sign writes it. Once a signers file is active at base, a verdict counts toward the quorum only when its seal verifies for its reviewer against that file. A seal needs a digest.",
159
+ "type": "object",
160
+ "additionalProperties": false,
161
+ "required": ["signer", "key", "signature"],
162
+ "properties": {
163
+ "signer": { "description": "Who signed: the reviewer, as the signers file names them.", "type": "string", "minLength": 1 },
164
+ "key": { "description": "The signing key's fingerprint. Reported, never trusted: the signature is the proof.", "type": "string", "pattern": "^SHA256:[A-Za-z0-9+/]+=*$" },
165
+ "signature": { "description": "The armored ssh signature, as ssh-keygen -Y sign prints it.", "type": "string", "pattern": "^-----BEGIN SSH SIGNATURE-----\\n" }
166
+ }
167
+ },
168
+ "proposes": {
169
+ "description": "With a dissent, the id of a new proposed decision the reviewer opened.",
170
+ "$ref": "#/definitions/decisionId"
171
+ },
172
+ "addressed_by": {
173
+ "description": "With a dissent, where the decider or the group answered the concern: a decision id, an issue or pull request (owner/repo#n) or an https link. An answer addresses a concern without necessarily accommodating it (RFC 7282, section 3).",
174
+ "anyOf": [
175
+ { "$ref": "#/definitions/decisionId" },
176
+ { "$ref": "#/definitions/issueRef" },
177
+ { "type": "string", "pattern": "^https://" }
178
+ ]
179
+ },
180
+ "withdrawn_on": {
181
+ "description": "With a dissent, the date its author withdrew the concern. Only the reviewer who raised a concern withdraws it.",
182
+ "type": "string",
183
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
184
+ }
185
+ },
186
+ "dependencies": { "seal": ["digest"] },
187
+ "if": {
188
+ "required": ["verdict"],
189
+ "properties": { "verdict": { "const": "dissent" } }
190
+ },
191
+ "then": {
192
+ "description": "A dissent needs a reason: the dissent by {reviewer} has no note.",
193
+ "required": ["note"],
194
+ "properties": { "note": { "type": "string", "pattern": "\\S" } }
195
+ },
196
+ "else": {
197
+ "description": "Only a dissent is a concern: the {verdict} by {reviewer} cannot carry addressed_by or withdrawn_on.",
198
+ "properties": { "addressed_by": false, "withdrawn_on": false }
199
+ }
200
+ }
201
+ },
202
+ "constrains": {
203
+ "description": "What this decision governs: issues (owner/repo#n), other decisions (their ids), workspace members (member:<name>) or files and directories in the workspace (path:<path>, the grammar of an evidence path, #2549). At least one entry: a decision that governs nothing is refused.",
204
+ "type": "array",
205
+ "minItems": 1,
206
+ "items": {
207
+ "type": "string",
208
+ "pattern": "^([A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+|[a-z][a-z0-9]{0,15}-[0-9]{3,}|member:[a-z0-9][a-z0-9-]{0,39}|path:(?!/)(?!(?:[^/]*/)*\\.{1,2}(?:/|$))(?!.*//)[^\\\\\\u0000-\\u001f]*[^/\\\\\\u0000-\\u001f])$"
209
+ }
210
+ }
211
+ },
212
+ "allOf": [
213
+ {
214
+ "if": {
215
+ "properties": { "state": { "enum": ["decided", "ratified", "superseded"] } }
216
+ },
217
+ "then": {
218
+ "properties": {
219
+ "area": { "type": "string" },
220
+ "question": { "type": "string", "minLength": 1 },
221
+ "choice": {
222
+ "type": "object",
223
+ "properties": { "reason": { "type": "string", "minLength": 1 } }
224
+ },
225
+ "rejected": {
226
+ "items": { "properties": { "why": { "type": "string", "minLength": 1 } } }
227
+ },
228
+ "options": {
229
+ "items": {
230
+ "properties": {
231
+ "how": { "type": "string", "minLength": 1 },
232
+ "tradeoff": { "type": "string", "minLength": 1 }
233
+ }
234
+ }
235
+ },
236
+ "decided_by": { "type": "string" },
237
+ "decided_on": { "type": "string" }
238
+ }
239
+ }
240
+ },
241
+ {
242
+ "if": {
243
+ "properties": { "state": { "enum": ["proposed", "withdrawn"] } }
244
+ },
245
+ "then": {
246
+ "properties": { "choice": { "type": "null" } }
247
+ }
248
+ }
249
+ ],
250
+ "definitions": {
251
+ "workspacePath": {
252
+ "description": "A path from the workspace root, with / separators: no leading /, no . or .. segment, no empty segment, no backslash and no trailing /.",
253
+ "type": "string",
254
+ "pattern": "^(?!/)(?!(?:[^/]*/)*\\.{1,2}(?:/|$))(?!.*//)[^\\\\\\u0000-\\u001f]*[^/\\\\\\u0000-\\u001f]$"
255
+ },
256
+ "evidenceLink": {
257
+ "description": "A public link.",
258
+ "type": "object",
259
+ "additionalProperties": false,
260
+ "required": ["title", "url"],
261
+ "properties": {
262
+ "title": { "type": "string", "minLength": 1 },
263
+ "url": { "type": "string", "pattern": "^https://" },
264
+ "as_of": {
265
+ "description": "When the linked text was read, as an ISO 8601 timestamp. Lets a reader flag evidence that changed afterwards.",
266
+ "type": ["string", "null"]
267
+ },
268
+ "sha256": {
269
+ "description": "Hex SHA-256 of the linked content, once evidence is pinned by hash (#2555). Null until then.",
270
+ "type": ["string", "null"],
271
+ "pattern": "^[0-9a-f]{64}$"
272
+ }
273
+ }
274
+ },
275
+ "evidencePin": {
276
+ "description": "A file in the workspace, pinned by the hash of its bytes (#2524 D18, #2549). chant workspace records reports a file that changed or went missing since.",
277
+ "type": "object",
278
+ "additionalProperties": false,
279
+ "required": ["title", "path", "sha256"],
280
+ "properties": {
281
+ "title": { "type": "string", "minLength": 1 },
282
+ "path": {
283
+ "description": "The file, from the workspace root. It sits inside a member, such as design/screens/home.json.",
284
+ "$ref": "#/definitions/workspacePath"
285
+ },
286
+ "sha256": {
287
+ "description": "Hex SHA-256 of the file's bytes when the decision was written. chant workspace records pin <path> prints it.",
288
+ "type": "string",
289
+ "pattern": "^[0-9a-f]{64}$"
290
+ },
291
+ "as_of": {
292
+ "description": "When the file was read, as an ISO 8601 timestamp.",
293
+ "type": ["string", "null"]
294
+ }
295
+ }
296
+ },
297
+ "sourceIssue": {
298
+ "description": "A row of the decisions table in an issue.",
299
+ "type": "object",
300
+ "additionalProperties": false,
301
+ "required": ["issue", "row"],
302
+ "properties": {
303
+ "issue": { "$ref": "#/definitions/issueRef" },
304
+ "row": {
305
+ "description": "The row's topic cell, verbatim, including any revision marker.",
306
+ "type": "string"
307
+ },
308
+ "revision": {
309
+ "description": "The design revision that last changed this row, such as \"v8\". Null when the row carries no marker.",
310
+ "type": ["string", "null"]
311
+ }
312
+ }
313
+ },
314
+ "sourceWorkspace": {
315
+ "description": "A decision made in the workspace, such as in a product's own design flow, with no issue behind it (#2654).",
316
+ "type": "object",
317
+ "additionalProperties": false,
318
+ "required": ["kind", "member"],
319
+ "properties": {
320
+ "kind": { "const": "workspace" },
321
+ "member": {
322
+ "description": "The workspace member the decision was made in, by its name in the declaration.",
323
+ "type": "string",
324
+ "pattern": "^[a-z0-9][a-z0-9-]{0,39}$"
325
+ },
326
+ "session": {
327
+ "description": "The id of the session the decision was made in, as the member names it. Null or absent when there was none.",
328
+ "type": ["string", "null"],
329
+ "minLength": 1
330
+ },
331
+ "issue": {
332
+ "description": "An issue the decision relates to, when there is one.",
333
+ "$ref": "#/definitions/issueRef"
334
+ }
335
+ }
336
+ },
337
+ "issueRef": {
338
+ "type": "string",
339
+ "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+$"
340
+ },
341
+ "decisionId": {
342
+ "type": "string",
343
+ "pattern": "^[a-z][a-z0-9]{0,15}-[0-9]{3,}$"
344
+ },
345
+ "optionId": {
346
+ "type": "string",
347
+ "pattern": "^[a-z]$"
348
+ },
349
+ "option": {
350
+ "type": "object",
351
+ "additionalProperties": false,
352
+ "required": ["id", "label", "how", "tradeoff"],
353
+ "properties": {
354
+ "id": { "$ref": "#/definitions/optionId" },
355
+ "label": {
356
+ "description": "The option as the source table words it, without revision markers.",
357
+ "type": "string",
358
+ "minLength": 1
359
+ },
360
+ "how": {
361
+ "description": "How the option works.",
362
+ "type": ["string", "null"]
363
+ },
364
+ "tradeoff": {
365
+ "description": "What choosing it costs or gains.",
366
+ "type": ["string", "null"]
367
+ },
368
+ "chosen_in": {
369
+ "description": "The earlier design revision that had chosen this option, when a later revision replaced it.",
370
+ "type": "string",
371
+ "pattern": "^v[0-9]+$"
372
+ }
373
+ }
374
+ }
375
+ }
376
+ }
@@ -0,0 +1,40 @@
1
+ ---
2
+ schema: 1
3
+ id: "fix-001"
4
+ title: "How the app is deployed"
5
+ state: "decided"
6
+ area: "delivery"
7
+ source:
8
+ kind: "workspace"
9
+ member: "delivery"
10
+ question: "What declares the app's deployment?"
11
+ options:
12
+ - id: "a"
13
+ label: "a chant member with a composite and a component"
14
+ how: "The delivery member declares the app as a WebService composite instance, and a component whose contract names WebService deploys it."
15
+ tradeoff: "chant workspace graph --composites has a row to join, and the member needs no package beyond @intentius/chant."
16
+ - id: "b"
17
+ label: "no deployment member"
18
+ how: "The workspace holds the app and its decisions only."
19
+ tradeoff: "One member fewer, and the composites read has nothing to join."
20
+ choice:
21
+ option: "a"
22
+ reason: "A reader of graph --composites is only tested when the join has a row."
23
+ rejected:
24
+ - option: "b"
25
+ why: "It leaves graph --composites with an empty join."
26
+ supersedes: []
27
+ evidence:
28
+ - title: "INTENTIUS/chant#2679, the reader conformance suite outside the chant repo"
29
+ url: "https://github.com/INTENTIUS/chant/issues/2679"
30
+ decided_by: "lex00"
31
+ decided_on: "2026-09-24"
32
+ reviews: []
33
+ constrains:
34
+ - "member:app"
35
+ - "member:delivery"
36
+ ---
37
+
38
+ # How the app is deployed
39
+
40
+ This workspace is generated by `@intentius/chant/workspace/conformance` for a reader's conformance run. The delivery member declares the app as a composite, and a component deploys it.
@@ -0,0 +1,7 @@
1
+ import type { ChantConfig } from "@intentius/chant";
2
+
3
+ // The member's one lexicon is declared by path (#2520), so the workspace
4
+ // needs no package beyond @intentius/chant.
5
+ export default {
6
+ lexicons: [{ name: "fixture", module: "./lexicon/index.ts" }],
7
+ } satisfies ChantConfig;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * A lexicon of one resource and one composite, declared by path in
3
+ * chant.config.ts, so the reader conformance workspace (#2679) has a
4
+ * composite instance without installing a lexicon package.
5
+ */
6
+ import { Composite, createResource } from "@intentius/chant";
7
+
8
+ export const Service = createResource("Fixture::Service", "fixture", {});
9
+
10
+ export const WebService = Composite<{ port: number }>((props) => {
11
+ const service = new Service({ port: props.port });
12
+ return { service };
13
+ }, "WebService");
14
+
15
+ export const fixturePlugin = {
16
+ name: "fixture",
17
+ serializer: {
18
+ name: "fixture",
19
+ rulePrefix: "FIX",
20
+ serialize: (entities: Map<string, unknown>) => JSON.stringify(Array.from(entities.keys()).sort()),
21
+ },
22
+ generate: async () => {},
23
+ validate: async () => {},
24
+ coverage: async () => {},
25
+ package: async () => {},
26
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "delivery",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Declares the reader conformance workspace's app as a composite, with the component that deploys it."
7
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The component that deploys the app. Its contract names the composite kind
3
+ * it deploys, so `chant workspace graph --composites` joins it to the `app`
4
+ * instance in app.ts.
5
+ */
6
+ import type { Component } from "@intentius/chant/components/component";
7
+
8
+ export const appComponent: Component = {
9
+ name: "app",
10
+ archetype: "service",
11
+ composites: ["WebService"],
12
+ dependsOn: [],
13
+ deploy: [],
14
+ };
@@ -0,0 +1,4 @@
1
+ import { WebService } from "../lexicon/index";
2
+
3
+ /** The app, as one WebService composite instance. */
4
+ export const app = WebService({ port: 8080 });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * The reader conformance suite as a reader outside the chant repository runs
3
+ * it (#2679): on the workspace generated from `__fixture__/`, with no
4
+ * test runner, for a subset of the commands, and from the files the package
5
+ * ships.
6
+ */
7
+
8
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { join, resolve } from "node:path";
10
+ import { afterAll, beforeAll, describe, expect, test } from "vitest";
11
+ import {
12
+ checkReaderRead,
13
+ CONFORMANCE_FIXTURE_DIR,
14
+ createConformanceWorkspace,
15
+ READ_CONTRACT_COMMANDS,
16
+ REFERENCE_READS,
17
+ runWorkspaceReaderConformance,
18
+ selectCommands,
19
+ type ChantRun,
20
+ type ChantTransport,
21
+ type ConformanceWorkspace,
22
+ type ReadContractCommand,
23
+ } from "./index";
24
+ import * as suite from "./index";
25
+
26
+ const coreRoot = resolve(import.meta.dirname, "..", "..", "..");
27
+ const repoRoot = resolve(coreRoot, "..", "..");
28
+
29
+ const JSON_FLAG: Record<ReadContractCommand, string[]> = {
30
+ ls: ["--json"],
31
+ graph: [],
32
+ check: ["--format", "json"],
33
+ status: ["--json"],
34
+ records: ["--json"],
35
+ "graph --intent": ["--json"],
36
+ "graph --composites": ["--json"],
37
+ };
38
+
39
+ /** The smallest reader that conforms, with a hook to misbehave. */
40
+ function reader(twist: (doc: Record<string, unknown>, chant: ChantTransport) => unknown = (doc) => doc) {
41
+ return (chant: ChantTransport) => ({
42
+ async read(command: ReadContractCommand, args: string[]) {
43
+ const run = await chant.run(["workspace", ...command.split(" "), ...args, ...JSON_FLAG[command]]);
44
+ return twist(JSON.parse(run.stdout) as Record<string, unknown>, chant);
45
+ },
46
+ });
47
+ }
48
+
49
+ let ws: ConformanceWorkspace;
50
+ beforeAll(() => {
51
+ ws = createConformanceWorkspace();
52
+ }, 300_000);
53
+ afterAll(() => ws?.dispose());
54
+
55
+ describe("the generated conformance workspace (#2679)", () => {
56
+ test("a conforming reader passes every command, graph --composites included, with nothing skipped", async () => {
57
+ const report = await runWorkspaceReaderConformance(reader(), { workspaceDir: ws.dir });
58
+ expect(report.problems).toEqual([]);
59
+ expect(report.checked).toEqual([...READ_CONTRACT_COMMANDS]);
60
+ expect(report.skipped).toEqual([]);
61
+ }, 300_000);
62
+
63
+ test("graph --composites joins the fixture's composite to its component", async () => {
64
+ const docs: Record<string, unknown>[] = [];
65
+ await runWorkspaceReaderConformance(reader((doc) => (docs.push(doc), doc)), { workspaceDir: ws.dir, commands: ["graph --composites"] });
66
+ expect(docs[0]).toMatchObject({
67
+ composites: [{ id: "delivery/app", kinds: ["WebService"], components: [{ component: "delivery/app", by: "composites", via: "member" }] }],
68
+ summary: { composites: 1, withComponent: 1 },
69
+ });
70
+ }, 300_000);
71
+
72
+ test("the decision record and the region the intent read names are in the workspace", async () => {
73
+ const docs: Record<string, unknown>[] = [];
74
+ await runWorkspaceReaderConformance(reader((doc) => (docs.push(doc), doc)), { workspaceDir: ws.dir, commands: ["records", "graph --intent"] });
75
+ expect((docs[0].records as { id: string }[]).map((r) => r.id)).toEqual(["fix-001"]);
76
+ expect(docs[1].region).toBe(`region:${REFERENCE_READS["graph --intent"][0]}`);
77
+ }, 300_000);
78
+ });
79
+
80
+ describe("commands (#2679)", () => {
81
+ test("only the listed commands are read, and the report names the rest as skipped", async () => {
82
+ const read: string[] = [];
83
+ const report = await runWorkspaceReaderConformance(
84
+ (chant) => ({ read: (command, args) => (read.push(command), reader()(chant).read(command, args)) }),
85
+ { workspaceDir: ws.dir, commands: ["status", "ls", "graph --composites"] },
86
+ );
87
+ expect(report.problems).toEqual([]);
88
+ expect(read).toEqual(["ls", "status", "graph --composites"]);
89
+ expect(report.checked).toEqual(["ls", "status", "graph --composites"]);
90
+ expect(report.skipped).toEqual(["graph", "check", "records", "graph --intent"]);
91
+ }, 300_000);
92
+
93
+ test("a name that is not a contract command, or an empty list, is refused", () => {
94
+ expect(() => selectCommands(["graph --kind" as ReadContractCommand])).toThrow(/not read-contract commands: graph --kind/);
95
+ expect(() => selectCommands([])).toThrow(/commands is empty/);
96
+ });
97
+ });
98
+
99
+ describe("what the runner-neutral suite catches (#2679)", () => {
100
+ test("a changed document, a reader that throws and a write to the workspace", async () => {
101
+ const changed = await runWorkspaceReaderConformance(reader((doc) => ({ ...doc, extra: 1 })), { workspaceDir: ws.dir, commands: ["ls"] });
102
+ expect(changed.problems.join("\n")).toMatch(/ls: the reader must return the document chant printed, unchanged/);
103
+
104
+ const thrown = await runWorkspaceReaderConformance(() => ({ read: () => { throw new Error("no chant"); } }), { workspaceDir: ws.dir, commands: ["status"] });
105
+ expect(thrown.problems).toEqual(["status: the reader threw: no chant"]);
106
+
107
+ const wrote = await runWorkspaceReaderConformance(
108
+ reader((doc) => (writeFileSync(join(ws.dir, "cache.json"), "{}"), doc)),
109
+ { workspaceDir: ws.dir, commands: ["ls"] },
110
+ );
111
+ expect(wrote.problems).toEqual(["workspace: the reads changed files: cache.json (added)"]);
112
+ }, 300_000);
113
+
114
+ test("checkReaderRead: the schema, the $schema id and the contract version", () => {
115
+ const argv = ["workspace", "ls", "--json"];
116
+ const doc = { $schema: "https://intentius.io/chant/schemas/workspace/ls/v1/ls.schema.json", contract: 2 };
117
+ const printed: ChantRun[] = [{ argv, status: 0, stdout: JSON.stringify(doc), stderr: "" }];
118
+ const problems = checkReaderRead("ls", [], [argv], printed, doc);
119
+ expect(problems.join("\n")).toMatch(/ls: the document does not validate against ls.schema.json/);
120
+ expect(problems.join("\n")).toMatch(/ls: contract is 2, expected 1/);
121
+ expect(checkReaderRead("ls", [], [argv], [{ ...printed[0], stdout: "" }], doc)).toEqual(["ls: chant workspace ls --json printed nothing"]);
122
+ });
123
+ });
124
+
125
+ describe("what the package ships (#2679)", () => {
126
+ const pkg = JSON.parse(readFileSync(join(coreRoot, "package.json"), "utf-8")) as { files: string[]; exports: Record<string, Record<string, string>> };
127
+
128
+ test("the fixture and both entries are in the package's files and exports", () => {
129
+ expect(pkg.files).toContain("src/");
130
+ for (const key of ["./workspace/conformance", "./workspace/conformance/vitest"]) {
131
+ const entry = pkg.exports[key];
132
+ for (const cond of ["development", "default"]) expect(existsSync(join(coreRoot, entry[cond])), `${key} ${cond}`).toBe(true);
133
+ expect(entry.types).toMatch(/^\.\/dist\/workspace\/conformance\/.+\.d\.ts$/);
134
+ }
135
+ expect(CONFORMANCE_FIXTURE_DIR).toBe(join(coreRoot, "src", "workspace", "conformance", "__fixture__"));
136
+ });
137
+
138
+ test("the plain-Node entry re-exports every runtime export of the suite", () => {
139
+ const text = readFileSync(join(import.meta.dirname, "index.mjs"), "utf-8");
140
+ const listed = [.../export const \{([^}]+)\}/.exec(text)![1].matchAll(/[A-Za-z_]+/g)].map((m) => m[0]).sort();
141
+ expect(listed).toEqual(Object.keys(suite).sort());
142
+ });
143
+
144
+ test("the fixture's decision kind and schema are the reference workspace's", () => {
145
+ for (const f of ["decision.kind.mjs", "decision.schema.json"]) {
146
+ expect(readFileSync(join(CONFORMANCE_FIXTURE_DIR, "decisions", f), "utf-8"), f).toBe(readFileSync(join(repoRoot, "reference-workspace", "decisions", f), "utf-8"));
147
+ }
148
+ });
149
+ });
@@ -0,0 +1,31 @@
1
+ // The entry of `@intentius/chant/workspace/conformance` for plain Node
2
+ // (#2679). The suite is written in TypeScript like the rest of the package,
3
+ // and Node does not strip types under node_modules, so this file loads it
4
+ // through tsx, a dependency of this package. `node --test` runs a reader's
5
+ // test file that imports it with no loader flag. Types come from
6
+ // dist/workspace/conformance/index.d.ts.
7
+
8
+ import { tsImport } from "tsx/esm/api";
9
+
10
+ const suite = await tsImport("./index.ts", import.meta.url);
11
+
12
+ export const {
13
+ READ_CONTRACT_COMMANDS,
14
+ READ_CONTRACT_SCHEMAS,
15
+ READ_CONTRACT_JSON_FLAGS,
16
+ REFERENCE_READS,
17
+ CONFORMANCE_FIXTURE_DIR,
18
+ defaultChantCommand,
19
+ referenceWorkspaceDir,
20
+ readContractSchema,
21
+ treeDigest,
22
+ treeChanges,
23
+ readerCallProblems,
24
+ checkReaderRead,
25
+ selectCommands,
26
+ createConformanceWorkspace,
27
+ conformanceTarget,
28
+ recordingTransport,
29
+ readAndCheck,
30
+ runWorkspaceReaderConformance,
31
+ } = suite;