@intentius/chant 0.85.0 → 0.86.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 (121) hide show
  1. package/dist/cli/main.d.ts.map +1 -1
  2. package/dist/cli/registry.d.ts +11 -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 +66 -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 +40 -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 +65 -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 +164 -0
  44. package/dist/workspace/records-write.d.ts.map +1 -0
  45. package/dist/workspace/records.d.ts +202 -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/work.d.ts +56 -0
  54. package/dist/workspace/work.d.ts.map +1 -0
  55. package/package.json +19 -1
  56. package/src/cli/main.ts +48 -3
  57. package/src/cli/registry.ts +11 -1
  58. package/src/lifecycle/gate-ledger.ts +14 -0
  59. package/src/workspace/__fixtures__/sessions.ts +66 -0
  60. package/src/workspace/checks/records.ts +19 -0
  61. package/src/workspace/checks.test.ts +2 -0
  62. package/src/workspace/checks.ts +7 -1
  63. package/src/workspace/composites.schema.json +65 -3
  64. package/src/workspace/composites.test.ts +95 -5
  65. package/src/workspace/composites.ts +28 -7
  66. package/src/workspace/conformance/__fixture__/app/package.json +7 -0
  67. package/src/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
  68. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +32 -0
  69. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +364 -0
  70. package/src/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
  71. package/src/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
  72. package/src/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
  73. package/src/workspace/conformance/__fixture__/delivery/package.json +7 -0
  74. package/src/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
  75. package/src/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
  76. package/src/workspace/conformance/conformance.test.ts +149 -0
  77. package/src/workspace/conformance/index.mjs +31 -0
  78. package/src/workspace/conformance/index.ts +453 -0
  79. package/src/workspace/conformance/vitest.ts +62 -0
  80. package/src/workspace/declaration.schema.json +40 -0
  81. package/src/workspace/declaration.ts +62 -0
  82. package/src/workspace/declared-kinds.test.ts +321 -0
  83. package/src/workspace/declared-kinds.ts +76 -0
  84. package/src/workspace/graph-cli.ts +8 -0
  85. package/src/workspace/intent-cli.ts +29 -6
  86. package/src/workspace/intent-joins.test.ts +60 -0
  87. package/src/workspace/intent-joins.ts +71 -19
  88. package/src/workspace/intent.schema.json +282 -7
  89. package/src/workspace/intent.test.ts +97 -0
  90. package/src/workspace/intent.ts +332 -45
  91. package/src/workspace/ls.schema.json +34 -0
  92. package/src/workspace/ls.ts +69 -4
  93. package/src/workspace/read-contract.test.ts +30 -9
  94. package/src/workspace/reason-codes.test.ts +15 -4
  95. package/src/workspace/reason-codes.ts +47 -4
  96. package/src/workspace/record-assets.test.ts +3 -1
  97. package/src/workspace/record-sessions.ts +105 -0
  98. package/src/workspace/record-source.ts +14 -5
  99. package/src/workspace/records-amend.schema.json +167 -0
  100. package/src/workspace/records-cli.ts +246 -19
  101. package/src/workspace/records-contract.test.ts +57 -2
  102. package/src/workspace/records-formats.test.ts +640 -0
  103. package/src/workspace/records-new.schema.json +158 -0
  104. package/src/workspace/records-quorum.test.ts +196 -0
  105. package/src/workspace/records-review.schema.json +202 -0
  106. package/src/workspace/records-sessions.test.ts +108 -0
  107. package/src/workspace/records-since.schema.json +193 -0
  108. package/src/workspace/records-since.test.ts +174 -0
  109. package/src/workspace/records-since.ts +259 -0
  110. package/src/workspace/records-write-contract.test.ts +125 -0
  111. package/src/workspace/records-write.test.ts +373 -0
  112. package/src/workspace/records-write.ts +736 -0
  113. package/src/workspace/records.schema.json +187 -9
  114. package/src/workspace/records.ts +631 -41
  115. package/src/workspace/runtimes.ts +107 -0
  116. package/src/workspace/status-contract.test.ts +163 -0
  117. package/src/workspace/status-gates.ts +215 -0
  118. package/src/workspace/status.schema.json +69 -3
  119. package/src/workspace/status.ts +35 -2
  120. package/src/workspace/work.test.ts +388 -0
  121. package/src/workspace/work.ts +163 -0
@@ -28,6 +28,10 @@
28
28
  *
29
29
  * Each member runs under its own toolchain, as for `workspace graph`, and at
30
30
  * the same revision with `--at`. Nothing is written and no listener starts.
31
+ *
32
+ * Each component also lists the runtimes it can deploy on (#2674,
33
+ * `runtimes.ts`): `local`, and each lexicon its member configures that hosts
34
+ * `chant run --components`, with the command line for each.
31
35
  */
32
36
 
33
37
  import { formatError } from "../cli/format";
@@ -40,6 +44,7 @@ import type { LinkRow } from "./links";
40
44
  import { emitDocument, type UnitResult } from "./member-commands";
41
45
  import { readerVersion, type ErrorLocation, type WorkspaceErrorCode } from "./declaration";
42
46
  import type { ReasonCode } from "./reason-codes";
47
+ import { componentRuntimes, readRuntimesIn, RUNTIME_REASON_CODES, type ComponentRuntime, type MemberRuntimes, type PluginLoader, type RuntimeReason } from "./runtimes";
43
48
 
44
49
  /** `$id` of the JSON Schema for the document, shipped beside this file. */
45
50
  export const COMPOSITES_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/composites/v1/composites.schema.json";
@@ -61,6 +66,9 @@ export const COMPOSITES_REASON_CODES = [
61
66
  ] as const satisfies readonly ReasonCode[];
62
67
  export type CompositesReasonCode = (typeof COMPOSITES_REASON_CODES)[number];
63
68
 
69
+ /** Why a member's runtimes are only `local`, or leave out a lexicon its config lists (#2674). Closed. */
70
+ export const COMPOSITES_RUNTIME_REASON_CODES = RUNTIME_REASON_CODES;
71
+
64
72
  export interface CompositesMember {
65
73
  name: string;
66
74
  dir: string;
@@ -69,6 +77,8 @@ export interface CompositesMember {
69
77
  status: "read" | "skipped" | "failed";
70
78
  reason: MemberReason | null;
71
79
  chant: string | null;
80
+ /** Why its components' runtimes leave something out: its config, or a lexicon, couldn't be read. Empty for a member not of kind chant. */
81
+ runtimeReasons: RuntimeReason[];
72
82
  }
73
83
 
74
84
  export interface ComponentEntry {
@@ -82,6 +92,8 @@ export interface ComponentEntry {
82
92
  composites: string[] | null;
83
93
  /** The component's file from the workspace root, when the member's chant names it. */
84
94
  file: string | null;
95
+ /** The runtimes it can deploy on: `local` first, then each hosting lexicon its member configures. */
96
+ runtimes: ComponentRuntime[];
85
97
  }
86
98
 
87
99
  export interface ComponentMatch {
@@ -133,7 +145,7 @@ export interface CompositesResult {
133
145
  }
134
146
 
135
147
  /** Read one member's component graph run into components, or the reason it can't be read. */
136
- function readComponents(member: string, dir: string, run: UnitResult): { components: ComponentEntry[] } | { reason: MemberReason } {
148
+ function readComponents(member: string, dir: string, run: UnitResult, runtimes: MemberRuntimes | undefined): { components: ComponentEntry[] } | { reason: MemberReason } {
137
149
  if (run.exitCode !== 0) {
138
150
  const tail = run.stderr.trim().split("\n").slice(-5).join("\n");
139
151
  return { reason: { code: "command-failed", message: `chant graph --components exited ${run.exitCode}${tail ? `: ${tail}` : ""}` } };
@@ -153,6 +165,7 @@ function readComponents(member: string, dir: string, run: UnitResult): { compone
153
165
  archetype: typeof attrs.archetype === "string" ? attrs.archetype : null,
154
166
  composites: composites && composites.length > 0 ? composites : null,
155
167
  file: file ? (dir === "." ? file : `${dir}/${file}`) : null,
168
+ runtimes: componentRuntimes(n.id, runtimes),
156
169
  });
157
170
  }
158
171
  return { components };
@@ -220,16 +233,24 @@ export function joinComponents(instances: CompositeInstanceRow[], components: Co
220
233
  });
221
234
  }
222
235
 
223
- function memberEntry(m: ComposedMember, componentReason: MemberReason | null): CompositesMember {
236
+ function memberEntry(m: ComposedMember, componentReason: MemberReason | null, runtimes: MemberRuntimes | undefined): CompositesMember {
224
237
  const reason = m.reason ?? componentReason;
225
238
  const status = m.status === "composed" ? (componentReason ? "failed" : "read") : m.status;
226
- return { name: m.name, dir: m.dir, kind: m.kind, status, reason, chant: m.chant };
239
+ return { name: m.name, dir: m.dir, kind: m.kind, status, reason, chant: m.chant, runtimeReasons: runtimes?.reasons ?? [] };
227
240
  }
228
241
 
229
242
  /** Read the workspace's composites and components, and build the document. Never throws a `WorkspaceReadError`. */
230
- export async function workspaceComposites(query: Omit<GraphQuery, "kind" | "components">): Promise<CompositesResult> {
243
+ export async function workspaceComposites(query: Omit<GraphQuery, "kind" | "components" | "inTree"> & { loadPlugin?: PluginLoader }): Promise<CompositesResult> {
231
244
  const head: Head = { $schema: COMPOSITES_OUTPUT_SCHEMA_ID, contract: COMPOSITES_CONTRACT_VERSION, chant: readerVersion() };
232
- const { doc: graph, failed, components: runs } = await workspaceGraph({ ...query, components: true });
245
+ let runtimes = new Map<string, MemberRuntimes>();
246
+ const { loadPlugin, ...graphQuery } = query;
247
+ const { doc: graph, failed, components: runs } = await workspaceGraph({
248
+ ...graphQuery,
249
+ components: true,
250
+ inTree: async (root, members) => {
251
+ runtimes = await readRuntimesIn(root, members, loadPlugin);
252
+ },
253
+ });
233
254
  if ("error" in graph) return { doc: { ...head, error: graph.error }, failed: true };
234
255
 
235
256
  const byMember = new Map((runs ?? []).map((r) => [r.unit.member, r]));
@@ -240,13 +261,13 @@ export async function workspaceComposites(query: Omit<GraphQuery, "kind" | "comp
240
261
  const run = m.status === "composed" ? byMember.get(m.name) : undefined;
241
262
  let reason: MemberReason | null = null;
242
263
  if (run) {
243
- const read = readComponents(m.name, m.dir, run);
264
+ const read = readComponents(m.name, m.dir, run, runtimes.get(m.name));
244
265
  if ("reason" in read) {
245
266
  reason = read.reason;
246
267
  componentsFailed = true;
247
268
  } else components.push(...read.components);
248
269
  }
249
- members.push(memberEntry(m, reason));
270
+ members.push(memberEntry(m, reason, runtimes.get(m.name)));
250
271
  }
251
272
  components.sort((a, b) => a.id.localeCompare(b.id));
252
273
 
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "app",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "The reader conformance workspace's product: the handler of a small web app."
7
+ }
@@ -0,0 +1,29 @@
1
+ // The reader conformance workspace's product (#2679): the request handler of
2
+ // a small web app, with no dependencies. The suite reads
3
+ // `graph --intent app/src/server.mjs:19`, so line 19 is the home page's
4
+ // status line, as it is in chant's reference-workspace/app/src/server.mjs.
5
+ // Nothing here opens a port: chant's own packages never listen (#2657), and
6
+ // this file ships in one.
7
+
8
+ /** The app's display name. */
9
+ export const APP_NAME = "Conformance app";
10
+
11
+ /** The home page. */
12
+ export const HOME = `<!doctype html>
13
+ <html lang="en">
14
+ <head><meta charset="utf-8"><title>${APP_NAME}</title></head>
15
+ <body>
16
+ <header><h1>${APP_NAME}</h1></header>
17
+ <main>
18
+ <!-- the status line -->
19
+ <p id="status">Running.</p>
20
+ </main>
21
+ </body>
22
+ </html>
23
+ `;
24
+
25
+ /** Answer one request with a status and a body. */
26
+ export function handle(url) {
27
+ if (url === "/healthz") return { status: 200, type: "application/json", body: JSON.stringify({ ok: true }) };
28
+ return { status: 200, type: "text/html; charset=utf-8", body: HOME };
29
+ }
@@ -0,0 +1,32 @@
1
+ // The decision record kind (#2555), data only: no imports, no code.
2
+ //
3
+ // chant workspace records --kind decisions/decision.kind.mjs --current --json
4
+ //
5
+ // A copy of docs/design/decisions/decision.kind.mjs in the chant repo, with
6
+ // decision.schema.json beside it, so a workspace made from this one reads its
7
+ // decisions without the chant repo. chant's test/reference-workspace.test.ts fails
8
+ // when this kind's data or the schema drifts from chant's.
9
+ export const recordKind = {
10
+ name: "decision",
11
+ location: { dir: ".", match: "^[a-z][a-z0-9]{0,15}-[0-9]{3,}-.+\\.md$" },
12
+ format: "markdown-front-matter",
13
+ schema: { id: "urn:intentius:chant:decision:1", path: "decision.schema.json" },
14
+ idField: "id",
15
+ stateField: "state",
16
+ states: ["proposed", "decided", "ratified", "superseded", "withdrawn"],
17
+ // Sealed once reached (#2555).
18
+ closedStates: ["ratified", "superseded"],
19
+ supersedes: { field: "supersedes", key: "decision" },
20
+ // A supersedes link takes effect under an equal or stricter approval rule
21
+ // (#2524 D4): from a record ranked above 0 and at least as high as the one it
22
+ // names. A decided record supersedes a decided or proposed one, a ratified
23
+ // record supersedes any, and a proposed or withdrawn record none.
24
+ approval: { proposed: 0, withdrawn: 0, decided: 1, ratified: 2, superseded: 2 },
25
+ // Evidence entries with a path pin a workspace file by the hash of its bytes (#2549).
26
+ pins: { field: "evidence" },
27
+ // member:<name> and path:<path> entries are the record's links in workspace graph (#2549).
28
+ constrains: { field: "constrains" },
29
+ // Verdicts, and the field naming the decider, for each record's digest and
30
+ // quorum (#2671, #2672).
31
+ reviews: { field: "reviews", decider: "decided_by" },
32
+ };
@@ -0,0 +1,364 @@
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
+ "proposes": {
158
+ "description": "With a dissent, the id of a new proposed decision the reviewer opened.",
159
+ "$ref": "#/definitions/decisionId"
160
+ },
161
+ "addressed_by": {
162
+ "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).",
163
+ "anyOf": [
164
+ { "$ref": "#/definitions/decisionId" },
165
+ { "$ref": "#/definitions/issueRef" },
166
+ { "type": "string", "pattern": "^https://" }
167
+ ]
168
+ },
169
+ "withdrawn_on": {
170
+ "description": "With a dissent, the date its author withdrew the concern. Only the reviewer who raised a concern withdraws it.",
171
+ "type": "string",
172
+ "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
173
+ }
174
+ },
175
+ "if": {
176
+ "required": ["verdict"],
177
+ "properties": { "verdict": { "const": "dissent" } }
178
+ },
179
+ "then": {
180
+ "description": "A dissent needs a reason: the dissent by {reviewer} has no note.",
181
+ "required": ["note"],
182
+ "properties": { "note": { "type": "string", "pattern": "\\S" } }
183
+ },
184
+ "else": {
185
+ "description": "Only a dissent is a concern: the {verdict} by {reviewer} cannot carry addressed_by or withdrawn_on.",
186
+ "properties": { "addressed_by": false, "withdrawn_on": false }
187
+ }
188
+ }
189
+ },
190
+ "constrains": {
191
+ "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.",
192
+ "type": "array",
193
+ "minItems": 1,
194
+ "items": {
195
+ "type": "string",
196
+ "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])$"
197
+ }
198
+ }
199
+ },
200
+ "allOf": [
201
+ {
202
+ "if": {
203
+ "properties": { "state": { "enum": ["decided", "ratified", "superseded"] } }
204
+ },
205
+ "then": {
206
+ "properties": {
207
+ "area": { "type": "string" },
208
+ "question": { "type": "string", "minLength": 1 },
209
+ "choice": {
210
+ "type": "object",
211
+ "properties": { "reason": { "type": "string", "minLength": 1 } }
212
+ },
213
+ "rejected": {
214
+ "items": { "properties": { "why": { "type": "string", "minLength": 1 } } }
215
+ },
216
+ "options": {
217
+ "items": {
218
+ "properties": {
219
+ "how": { "type": "string", "minLength": 1 },
220
+ "tradeoff": { "type": "string", "minLength": 1 }
221
+ }
222
+ }
223
+ },
224
+ "decided_by": { "type": "string" },
225
+ "decided_on": { "type": "string" }
226
+ }
227
+ }
228
+ },
229
+ {
230
+ "if": {
231
+ "properties": { "state": { "enum": ["proposed", "withdrawn"] } }
232
+ },
233
+ "then": {
234
+ "properties": { "choice": { "type": "null" } }
235
+ }
236
+ }
237
+ ],
238
+ "definitions": {
239
+ "workspacePath": {
240
+ "description": "A path from the workspace root, with / separators: no leading /, no . or .. segment, no empty segment, no backslash and no trailing /.",
241
+ "type": "string",
242
+ "pattern": "^(?!/)(?!(?:[^/]*/)*\\.{1,2}(?:/|$))(?!.*//)[^\\\\\\u0000-\\u001f]*[^/\\\\\\u0000-\\u001f]$"
243
+ },
244
+ "evidenceLink": {
245
+ "description": "A public link.",
246
+ "type": "object",
247
+ "additionalProperties": false,
248
+ "required": ["title", "url"],
249
+ "properties": {
250
+ "title": { "type": "string", "minLength": 1 },
251
+ "url": { "type": "string", "pattern": "^https://" },
252
+ "as_of": {
253
+ "description": "When the linked text was read, as an ISO 8601 timestamp. Lets a reader flag evidence that changed afterwards.",
254
+ "type": ["string", "null"]
255
+ },
256
+ "sha256": {
257
+ "description": "Hex SHA-256 of the linked content, once evidence is pinned by hash (#2555). Null until then.",
258
+ "type": ["string", "null"],
259
+ "pattern": "^[0-9a-f]{64}$"
260
+ }
261
+ }
262
+ },
263
+ "evidencePin": {
264
+ "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.",
265
+ "type": "object",
266
+ "additionalProperties": false,
267
+ "required": ["title", "path", "sha256"],
268
+ "properties": {
269
+ "title": { "type": "string", "minLength": 1 },
270
+ "path": {
271
+ "description": "The file, from the workspace root. It sits inside a member, such as design/screens/home.json.",
272
+ "$ref": "#/definitions/workspacePath"
273
+ },
274
+ "sha256": {
275
+ "description": "Hex SHA-256 of the file's bytes when the decision was written. chant workspace records pin <path> prints it.",
276
+ "type": "string",
277
+ "pattern": "^[0-9a-f]{64}$"
278
+ },
279
+ "as_of": {
280
+ "description": "When the file was read, as an ISO 8601 timestamp.",
281
+ "type": ["string", "null"]
282
+ }
283
+ }
284
+ },
285
+ "sourceIssue": {
286
+ "description": "A row of the decisions table in an issue.",
287
+ "type": "object",
288
+ "additionalProperties": false,
289
+ "required": ["issue", "row"],
290
+ "properties": {
291
+ "issue": { "$ref": "#/definitions/issueRef" },
292
+ "row": {
293
+ "description": "The row's topic cell, verbatim, including any revision marker.",
294
+ "type": "string"
295
+ },
296
+ "revision": {
297
+ "description": "The design revision that last changed this row, such as \"v8\". Null when the row carries no marker.",
298
+ "type": ["string", "null"]
299
+ }
300
+ }
301
+ },
302
+ "sourceWorkspace": {
303
+ "description": "A decision made in the workspace, such as in a product's own design flow, with no issue behind it (#2654).",
304
+ "type": "object",
305
+ "additionalProperties": false,
306
+ "required": ["kind", "member"],
307
+ "properties": {
308
+ "kind": { "const": "workspace" },
309
+ "member": {
310
+ "description": "The workspace member the decision was made in, by its name in the declaration.",
311
+ "type": "string",
312
+ "pattern": "^[a-z0-9][a-z0-9-]{0,39}$"
313
+ },
314
+ "session": {
315
+ "description": "The id of the session the decision was made in, as the member names it. Null or absent when there was none.",
316
+ "type": ["string", "null"],
317
+ "minLength": 1
318
+ },
319
+ "issue": {
320
+ "description": "An issue the decision relates to, when there is one.",
321
+ "$ref": "#/definitions/issueRef"
322
+ }
323
+ }
324
+ },
325
+ "issueRef": {
326
+ "type": "string",
327
+ "pattern": "^[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+#[0-9]+$"
328
+ },
329
+ "decisionId": {
330
+ "type": "string",
331
+ "pattern": "^[a-z][a-z0-9]{0,15}-[0-9]{3,}$"
332
+ },
333
+ "optionId": {
334
+ "type": "string",
335
+ "pattern": "^[a-z]$"
336
+ },
337
+ "option": {
338
+ "type": "object",
339
+ "additionalProperties": false,
340
+ "required": ["id", "label", "how", "tradeoff"],
341
+ "properties": {
342
+ "id": { "$ref": "#/definitions/optionId" },
343
+ "label": {
344
+ "description": "The option as the source table words it, without revision markers.",
345
+ "type": "string",
346
+ "minLength": 1
347
+ },
348
+ "how": {
349
+ "description": "How the option works.",
350
+ "type": ["string", "null"]
351
+ },
352
+ "tradeoff": {
353
+ "description": "What choosing it costs or gains.",
354
+ "type": ["string", "null"]
355
+ },
356
+ "chosen_in": {
357
+ "description": "The earlier design revision that had chosen this option, when a later revision replaced it.",
358
+ "type": "string",
359
+ "pattern": "^v[0-9]+$"
360
+ }
361
+ }
362
+ }
363
+ }
364
+ }
@@ -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 });