@intentius/chant 0.49.0 → 0.51.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 (294) hide show
  1. package/dist/audit/catalog.d.ts +13 -3
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +9 -0
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +6 -0
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/fetch.d.ts.map +1 -1
  8. package/dist/audit/report-html.d.ts.map +1 -1
  9. package/dist/audit/report-model.d.ts +6 -0
  10. package/dist/audit/report-model.d.ts.map +1 -1
  11. package/dist/audit/report.d.ts.map +1 -1
  12. package/dist/audit/rules-doc.d.ts.map +1 -1
  13. package/dist/audit/secrets.d.ts +95 -0
  14. package/dist/audit/secrets.d.ts.map +1 -0
  15. package/dist/audit/wrangler.d.ts +33 -0
  16. package/dist/audit/wrangler.d.ts.map +1 -0
  17. package/dist/build.d.ts.map +1 -1
  18. package/dist/cli/commands/audit.d.ts +7 -0
  19. package/dist/cli/commands/audit.d.ts.map +1 -1
  20. package/dist/cli/commands/build.d.ts +23 -0
  21. package/dist/cli/commands/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/build.d.ts.map +1 -1
  23. package/dist/cli/handlers/components.d.ts +31 -0
  24. package/dist/cli/handlers/components.d.ts.map +1 -1
  25. package/dist/cli/handlers/lifecycle.d.ts +11 -0
  26. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  27. package/dist/cli/handlers/op-progress.d.ts +57 -0
  28. package/dist/cli/handlers/op-progress.d.ts.map +1 -0
  29. package/dist/cli/handlers/operator.d.ts +32 -0
  30. package/dist/cli/handlers/operator.d.ts.map +1 -0
  31. package/dist/cli/handlers/run-client.d.ts +21 -1
  32. package/dist/cli/handlers/run-client.d.ts.map +1 -1
  33. package/dist/cli/handlers/run-report.d.ts.map +1 -1
  34. package/dist/cli/handlers/run.d.ts.map +1 -1
  35. package/dist/cli/handlers/scenario.d.ts +39 -0
  36. package/dist/cli/handlers/scenario.d.ts.map +1 -0
  37. package/dist/cli/handlers/search.d.ts +22 -0
  38. package/dist/cli/handlers/search.d.ts.map +1 -1
  39. package/dist/cli/main.d.ts.map +1 -1
  40. package/dist/cli/mcp/op-tools.d.ts.map +1 -1
  41. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  42. package/dist/cli/mcp/server.d.ts +35 -2
  43. package/dist/cli/mcp/server.d.ts.map +1 -1
  44. package/dist/cli/mcp/types.d.ts +29 -1
  45. package/dist/cli/mcp/types.d.ts.map +1 -1
  46. package/dist/cli/registry.d.ts +47 -3
  47. package/dist/cli/registry.d.ts.map +1 -1
  48. package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
  49. package/dist/components/capability.d.ts +17 -2
  50. package/dist/components/capability.d.ts.map +1 -1
  51. package/dist/components/cli-support.d.ts +7 -0
  52. package/dist/components/cli-support.d.ts.map +1 -1
  53. package/dist/components/component.d.ts +15 -0
  54. package/dist/components/component.d.ts.map +1 -1
  55. package/dist/components/driver.d.ts.map +1 -1
  56. package/dist/components/run-progress.d.ts +7 -5
  57. package/dist/components/run-progress.d.ts.map +1 -1
  58. package/dist/components/verbs/index.d.ts +6 -1
  59. package/dist/components/verbs/index.d.ts.map +1 -1
  60. package/dist/components/verbs/run-agent.d.ts +499 -0
  61. package/dist/components/verbs/run-agent.d.ts.map +1 -0
  62. package/dist/components/verbs/sign.d.ts +30 -0
  63. package/dist/components/verbs/sign.d.ts.map +1 -1
  64. package/dist/composite.d.ts +6 -1
  65. package/dist/composite.d.ts.map +1 -1
  66. package/dist/discovery/collect.d.ts.map +1 -1
  67. package/dist/discovery/fold-import.d.ts +15 -1
  68. package/dist/discovery/fold-import.d.ts.map +1 -1
  69. package/dist/discovery/fold-rank.d.ts +66 -0
  70. package/dist/discovery/fold-rank.d.ts.map +1 -0
  71. package/dist/discovery/index.d.ts +15 -0
  72. package/dist/discovery/index.d.ts.map +1 -1
  73. package/dist/discovery/param-deps.d.ts +17 -0
  74. package/dist/discovery/param-deps.d.ts.map +1 -0
  75. package/dist/fold/fold.d.ts +55 -2
  76. package/dist/fold/fold.d.ts.map +1 -1
  77. package/dist/fold/subset.d.ts +21 -14
  78. package/dist/fold/subset.d.ts.map +1 -1
  79. package/dist/lexicon-schema.d.ts +2 -0
  80. package/dist/lexicon-schema.d.ts.map +1 -1
  81. package/dist/lexicon.d.ts +134 -0
  82. package/dist/lexicon.d.ts.map +1 -1
  83. package/dist/lifecycle/assert-live.d.ts +77 -0
  84. package/dist/lifecycle/assert-live.d.ts.map +1 -0
  85. package/dist/lifecycle/change-set.d.ts +17 -0
  86. package/dist/lifecycle/change-set.d.ts.map +1 -1
  87. package/dist/lifecycle/converge-ledger.d.ts +90 -0
  88. package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
  89. package/dist/lifecycle/deep-diff.d.ts +18 -0
  90. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  91. package/dist/lifecycle/deep-observe.d.ts +9 -1
  92. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  93. package/dist/lifecycle/disruption.d.ts +96 -0
  94. package/dist/lifecycle/disruption.d.ts.map +1 -0
  95. package/dist/lifecycle/gate-ledger.d.ts +33 -0
  96. package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
  97. package/dist/lifecycle/git.d.ts +145 -21
  98. package/dist/lifecycle/git.d.ts.map +1 -1
  99. package/dist/lifecycle/index.d.ts +6 -0
  100. package/dist/lifecycle/index.d.ts.map +1 -1
  101. package/dist/lifecycle/lease.d.ts +113 -0
  102. package/dist/lifecycle/lease.d.ts.map +1 -0
  103. package/dist/lifecycle/replay.d.ts +2 -0
  104. package/dist/lifecycle/replay.d.ts.map +1 -1
  105. package/dist/lifecycle/scenario-eval.d.ts +42 -0
  106. package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
  107. package/dist/lifecycle/scenario.d.ts +163 -0
  108. package/dist/lifecycle/scenario.d.ts.map +1 -0
  109. package/dist/lifecycle/symptoms.d.ts +63 -0
  110. package/dist/lifecycle/symptoms.d.ts.map +1 -0
  111. package/dist/lint/output-docs.d.ts +94 -0
  112. package/dist/lint/output-docs.d.ts.map +1 -0
  113. package/dist/lint/post-synth.d.ts +29 -0
  114. package/dist/lint/post-synth.d.ts.map +1 -1
  115. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
  116. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
  117. package/dist/lsp/lexicon-providers.d.ts +7 -0
  118. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  119. package/dist/op/activity-contract.d.ts +139 -0
  120. package/dist/op/activity-contract.d.ts.map +1 -0
  121. package/dist/op/builders.d.ts +42 -2
  122. package/dist/op/builders.d.ts.map +1 -1
  123. package/dist/op/converge-rule.d.ts +161 -0
  124. package/dist/op/converge-rule.d.ts.map +1 -0
  125. package/dist/op/generate-pipeline.d.ts +39 -0
  126. package/dist/op/generate-pipeline.d.ts.map +1 -0
  127. package/dist/op/index.d.ts +14 -0
  128. package/dist/op/index.d.ts.map +1 -1
  129. package/dist/op/local-executor.d.ts +7 -1
  130. package/dist/op/local-executor.d.ts.map +1 -1
  131. package/dist/op/op-verb-class.d.ts +42 -0
  132. package/dist/op/op-verb-class.d.ts.map +1 -0
  133. package/dist/op/operator.d.ts +128 -0
  134. package/dist/op/operator.d.ts.map +1 -0
  135. package/dist/op/step-output-ref.d.ts +187 -0
  136. package/dist/op/step-output-ref.d.ts.map +1 -0
  137. package/dist/op/types.d.ts +18 -1
  138. package/dist/op/types.d.ts.map +1 -1
  139. package/dist/provenance.d.ts +73 -3
  140. package/dist/provenance.d.ts.map +1 -1
  141. package/dist/runtime-adapter.d.ts +7 -1
  142. package/dist/runtime-adapter.d.ts.map +1 -1
  143. package/dist/serializer.d.ts +18 -0
  144. package/dist/serializer.d.ts.map +1 -1
  145. package/dist/testing.d.ts +23 -2
  146. package/dist/testing.d.ts.map +1 -1
  147. package/dist/toml.d.ts +40 -5
  148. package/dist/toml.d.ts.map +1 -1
  149. package/package.json +1 -1
  150. package/src/audit/catalog.test.ts +1 -1
  151. package/src/audit/catalog.ts +75 -3
  152. package/src/audit/core.ts +9 -0
  153. package/src/audit/discover.ts +29 -2
  154. package/src/audit/fetch.test.ts +216 -3
  155. package/src/audit/fetch.ts +270 -59
  156. package/src/audit/report-html.ts +5 -2
  157. package/src/audit/report-model.ts +9 -0
  158. package/src/audit/report.test.ts +22 -0
  159. package/src/audit/report.ts +3 -2
  160. package/src/audit/rules-doc.ts +2 -0
  161. package/src/audit/secrets.test.ts +303 -0
  162. package/src/audit/secrets.ts +406 -0
  163. package/src/audit/wrangler.test.ts +230 -0
  164. package/src/audit/wrangler.ts +290 -0
  165. package/src/build.ts +8 -3
  166. package/src/cli/command-group.ts +1 -1
  167. package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
  168. package/src/cli/commands/audit.test.ts +215 -1
  169. package/src/cli/commands/audit.ts +86 -17
  170. package/src/cli/commands/build.test.ts +167 -2
  171. package/src/cli/commands/build.ts +114 -23
  172. package/src/cli/handlers/build.ts +2 -0
  173. package/src/cli/handlers/components.test.ts +199 -1
  174. package/src/cli/handlers/components.ts +160 -3
  175. package/src/cli/handlers/graph.test.ts +20 -0
  176. package/src/cli/handlers/graph.ts +10 -1
  177. package/src/cli/handlers/lifecycle.test.ts +90 -0
  178. package/src/cli/handlers/lifecycle.ts +30 -5
  179. package/src/cli/handlers/op-progress.test.ts +202 -0
  180. package/src/cli/handlers/op-progress.ts +192 -0
  181. package/src/cli/handlers/operator.test.ts +255 -0
  182. package/src/cli/handlers/operator.ts +240 -0
  183. package/src/cli/handlers/run-client.test.ts +82 -0
  184. package/src/cli/handlers/run-client.ts +85 -2
  185. package/src/cli/handlers/run-report.test.ts +62 -0
  186. package/src/cli/handlers/run-report.ts +20 -58
  187. package/src/cli/handlers/run.test.ts +144 -0
  188. package/src/cli/handlers/run.ts +40 -18
  189. package/src/cli/handlers/scenario.test.ts +456 -0
  190. package/src/cli/handlers/scenario.ts +330 -0
  191. package/src/cli/handlers/search-drift.test.ts +263 -0
  192. package/src/cli/handlers/search.ts +150 -1
  193. package/src/cli/main.test.ts +23 -0
  194. package/src/cli/main.ts +81 -1
  195. package/src/cli/mcp/op-tools.ts +17 -6
  196. package/src/cli/mcp/resource-handlers.ts +13 -5
  197. package/src/cli/mcp/server.test.ts +265 -2
  198. package/src/cli/mcp/server.ts +84 -7
  199. package/src/cli/mcp/types.ts +27 -1
  200. package/src/cli/registry.ts +47 -3
  201. package/src/codegen/docs-rule-scanning.test.ts +42 -0
  202. package/src/codegen/docs-rule-scanning.ts +25 -2
  203. package/src/components/README.md +7 -0
  204. package/src/components/capability.ts +17 -2
  205. package/src/components/cli-support.test.ts +17 -0
  206. package/src/components/cli-support.ts +13 -1
  207. package/src/components/component-schema.test.ts +32 -0
  208. package/src/components/component.schema.json +6 -0
  209. package/src/components/component.test.ts +21 -0
  210. package/src/components/component.ts +15 -0
  211. package/src/components/driver.ts +12 -4
  212. package/src/components/run-progress.ts +9 -5
  213. package/src/components/verbs/index.ts +6 -1
  214. package/src/components/verbs/run-agent.test.ts +683 -0
  215. package/src/components/verbs/run-agent.ts +786 -0
  216. package/src/components/verbs/sign.test.ts +19 -0
  217. package/src/components/verbs/sign.ts +34 -2
  218. package/src/composite.ts +31 -2
  219. package/src/discovery/collect.ts +11 -2
  220. package/src/discovery/fold-import.test.ts +54 -0
  221. package/src/discovery/fold-import.ts +178 -38
  222. package/src/discovery/fold-rank.test.ts +197 -0
  223. package/src/discovery/fold-rank.ts +346 -0
  224. package/src/discovery/index.ts +16 -1
  225. package/src/discovery/param-deps.test.ts +118 -0
  226. package/src/discovery/param-deps.ts +170 -0
  227. package/src/fold/fold.test.ts +6 -2
  228. package/src/fold/fold.ts +184 -3
  229. package/src/fold/subset.test.ts +82 -19
  230. package/src/fold/subset.ts +79 -41
  231. package/src/lexicon-schema.ts +3 -0
  232. package/src/lexicon.ts +154 -2
  233. package/src/lifecycle/assert-live.test.ts +125 -0
  234. package/src/lifecycle/assert-live.ts +154 -0
  235. package/src/lifecycle/change-set.ts +35 -3
  236. package/src/lifecycle/converge-ledger.test.ts +199 -0
  237. package/src/lifecycle/converge-ledger.ts +179 -0
  238. package/src/lifecycle/deep-diff.test.ts +79 -1
  239. package/src/lifecycle/deep-diff.ts +23 -0
  240. package/src/lifecycle/deep-observe.ts +13 -2
  241. package/src/lifecycle/disruption.test.ts +186 -0
  242. package/src/lifecycle/disruption.ts +224 -0
  243. package/src/lifecycle/gate-ledger.test.ts +103 -0
  244. package/src/lifecycle/gate-ledger.ts +140 -0
  245. package/src/lifecycle/git.test.ts +430 -0
  246. package/src/lifecycle/git.ts +446 -84
  247. package/src/lifecycle/index.ts +6 -0
  248. package/src/lifecycle/lease.test.ts +343 -0
  249. package/src/lifecycle/lease.ts +270 -0
  250. package/src/lifecycle/replay.test.ts +25 -0
  251. package/src/lifecycle/replay.ts +11 -3
  252. package/src/lifecycle/scenario-eval.test.ts +199 -0
  253. package/src/lifecycle/scenario-eval.ts +158 -0
  254. package/src/lifecycle/scenario.test.ts +195 -0
  255. package/src/lifecycle/scenario.ts +321 -0
  256. package/src/lifecycle/symptoms.test.ts +116 -0
  257. package/src/lifecycle/symptoms.ts +126 -0
  258. package/src/lint/output-docs.test.ts +220 -0
  259. package/src/lint/output-docs.ts +204 -0
  260. package/src/lint/post-synth.test.ts +97 -0
  261. package/src/lint/post-synth.ts +45 -0
  262. package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
  263. package/src/lint/rules/comp/comp.test.ts +49 -1
  264. package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
  265. package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
  266. package/src/lsp/lexicon-providers.test.ts +44 -0
  267. package/src/lsp/lexicon-providers.ts +11 -1
  268. package/src/op/activity-contract.test.ts +180 -0
  269. package/src/op/activity-contract.ts +278 -0
  270. package/src/op/builders-exports.test.ts +17 -1
  271. package/src/op/builders.ts +59 -5
  272. package/src/op/converge-rule.test.ts +179 -0
  273. package/src/op/converge-rule.ts +311 -0
  274. package/src/op/generate-pipeline.test.ts +53 -0
  275. package/src/op/generate-pipeline.ts +99 -0
  276. package/src/op/index.ts +30 -0
  277. package/src/op/local-executor.test.ts +92 -0
  278. package/src/op/local-executor.ts +52 -10
  279. package/src/op/local-output.ts +1 -1
  280. package/src/op/op-verb-class.test.ts +126 -0
  281. package/src/op/op-verb-class.ts +115 -0
  282. package/src/op/operator.test.ts +346 -0
  283. package/src/op/operator.ts +213 -0
  284. package/src/op/step-output-ref.test.ts +334 -0
  285. package/src/op/step-output-ref.ts +453 -0
  286. package/src/op/types.ts +18 -1
  287. package/src/provenance.test.ts +151 -4
  288. package/src/provenance.ts +118 -4
  289. package/src/runtime-adapter.ts +31 -10
  290. package/src/serializer.ts +18 -0
  291. package/src/testing.test.ts +89 -2
  292. package/src/testing.ts +63 -3
  293. package/src/toml.test.ts +157 -384
  294. package/src/toml.ts +371 -5
@@ -47,7 +47,8 @@ describe("McpServer", () => {
47
47
 
48
48
  expect(response.error).toBeUndefined();
49
49
  const result = response.result as Record<string, unknown>;
50
- expect(result.protocolVersion).toBe("2024-11-05");
50
+ // No requested version → negotiates to the latest supported revision (#1194).
51
+ expect(result.protocolVersion).toBe("2026-07-28");
51
52
  expect(result.capabilities).toBeDefined();
52
53
  expect((result.serverInfo as Record<string, unknown>).name).toBe("chant");
53
54
  expect((result.serverInfo as Record<string, unknown>).version).toBe("0.1.0");
@@ -60,11 +61,273 @@ describe("McpServer", () => {
60
61
  method: "initialize",
61
62
  params: {},
62
63
  });
63
- const result = response.result as Record<string, unknown>;
64
+ const result = response!.result as Record<string, unknown>;
64
65
  const caps = result.capabilities as Record<string, unknown>;
65
66
  expect(caps.tools).toBeDefined();
66
67
  expect(caps.resources).toBeDefined();
67
68
  });
69
+
70
+ // -----------------------------------------------------------------------
71
+ // Dual-revision negotiation (#1194)
72
+ // -----------------------------------------------------------------------
73
+
74
+ test("a 2024-11-05 client requesting its own version gets it back unchanged", async () => {
75
+ const response = await server.handleRequest({
76
+ jsonrpc: "2.0",
77
+ id: 1,
78
+ method: "initialize",
79
+ params: { protocolVersion: "2024-11-05", clientInfo: { name: "old-client", version: "1.0" } },
80
+ });
81
+ expect(response!.error).toBeUndefined();
82
+ const result = response!.result as Record<string, unknown>;
83
+ expect(result.protocolVersion).toBe("2024-11-05");
84
+ });
85
+
86
+ test("a 2026-07-28 client requesting its version gets the latest revision", async () => {
87
+ const response = await server.handleRequest({
88
+ jsonrpc: "2.0",
89
+ id: 1,
90
+ method: "initialize",
91
+ params: { _meta: { protocolVersion: "2026-07-28" } },
92
+ });
93
+ expect(response!.error).toBeUndefined();
94
+ const result = response!.result as Record<string, unknown>;
95
+ expect(result.protocolVersion).toBe("2026-07-28");
96
+ });
97
+
98
+ test("an unrecognized requested version falls back to the latest supported", async () => {
99
+ const response = await server.handleRequest({
100
+ jsonrpc: "2.0",
101
+ id: 1,
102
+ method: "initialize",
103
+ params: { protocolVersion: "1999-01-01" },
104
+ });
105
+ const result = response!.result as Record<string, unknown>;
106
+ expect(result.protocolVersion).toBe("2026-07-28");
107
+ });
108
+
109
+ test("no requested version at all defaults to the latest supported", async () => {
110
+ const response = await server.handleRequest({
111
+ jsonrpc: "2.0",
112
+ id: 1,
113
+ method: "initialize",
114
+ params: {},
115
+ });
116
+ const result = response!.result as Record<string, unknown>;
117
+ expect(result.protocolVersion).toBe("2026-07-28");
118
+ });
119
+ });
120
+
121
+ // -----------------------------------------------------------------------
122
+ // _meta parsing (#1194)
123
+ // -----------------------------------------------------------------------
124
+
125
+ describe("_meta parsing", () => {
126
+ test("reads requested protocol version from _meta on initialize", async () => {
127
+ const response = await server.handleRequest({
128
+ jsonrpc: "2.0",
129
+ id: 1,
130
+ method: "initialize",
131
+ params: { _meta: { protocolVersion: "2024-11-05" } },
132
+ });
133
+ const result = response!.result as Record<string, unknown>;
134
+ expect(result.protocolVersion).toBe("2024-11-05");
135
+ });
136
+
137
+ test("_meta protocolVersion takes precedence over a legacy top-level one", async () => {
138
+ const response = await server.handleRequest({
139
+ jsonrpc: "2.0",
140
+ id: 1,
141
+ method: "initialize",
142
+ params: { protocolVersion: "2024-11-05", _meta: { protocolVersion: "2026-07-28" } },
143
+ });
144
+ const result = response!.result as Record<string, unknown>;
145
+ expect(result.protocolVersion).toBe("2026-07-28");
146
+ });
147
+
148
+ test("client identity in _meta does not interfere with tool dispatch", async () => {
149
+ const response = await server.handleRequest({
150
+ jsonrpc: "2.0",
151
+ id: 1,
152
+ method: "tools/call",
153
+ params: {
154
+ name: "search",
155
+ arguments: { query: "bucket" },
156
+ _meta: {
157
+ protocolVersion: "2026-07-28",
158
+ "io.modelcontextprotocol/clientInfo": { name: "some-agent", version: "3.0" },
159
+ },
160
+ },
161
+ });
162
+ expect(response!.error).toBeUndefined();
163
+ const result = response!.result as { content: Array<{ text: string }>; isError?: boolean };
164
+ expect(result.isError).toBeUndefined();
165
+ const parsed = JSON.parse(result.content[0].text);
166
+ expect(parsed.query).toBe("bucket");
167
+ });
168
+
169
+ test("a request with no _meta and no legacy fields is unaffected", async () => {
170
+ const response = await server.handleRequest({
171
+ jsonrpc: "2.0",
172
+ id: 1,
173
+ method: "tools/list",
174
+ params: {},
175
+ });
176
+ expect(response!.error).toBeUndefined();
177
+ });
178
+ });
179
+
180
+ // -----------------------------------------------------------------------
181
+ // Notification silence (#1194)
182
+ // -----------------------------------------------------------------------
183
+
184
+ describe("notifications", () => {
185
+ test("a message with no id gets no response", async () => {
186
+ const response = await server.handleRequest({
187
+ jsonrpc: "2.0",
188
+ method: "notifications/initialized",
189
+ params: {},
190
+ });
191
+ expect(response).toBeNull();
192
+ });
193
+
194
+ test("an id-less message is not routed through dispatch as Unknown method", async () => {
195
+ // notifications/initialized isn't a dispatch case; silence must come
196
+ // from the id check, not from swallowing a dispatch error.
197
+ const response = await server.handleRequest({
198
+ jsonrpc: "2.0",
199
+ method: "notifications/anything",
200
+ });
201
+ expect(response).toBeNull();
202
+ });
203
+
204
+ test("a message with id 0 (falsy but present) still gets a response", async () => {
205
+ const response = await server.handleRequest({
206
+ jsonrpc: "2.0",
207
+ id: 0,
208
+ method: "tools/list",
209
+ });
210
+ expect(response).not.toBeNull();
211
+ expect(response!.id).toBe(0);
212
+ });
213
+ });
214
+
215
+ // -----------------------------------------------------------------------
216
+ // server/discover (#1194)
217
+ // -----------------------------------------------------------------------
218
+
219
+ describe("server/discover", () => {
220
+ test("a 2026-07-28 client can discover capabilities without calling initialize", async () => {
221
+ const response = await server.handleRequest({
222
+ jsonrpc: "2.0",
223
+ id: 1,
224
+ method: "server/discover",
225
+ params: { _meta: { protocolVersion: "2026-07-28" } },
226
+ });
227
+ expect(response!.error).toBeUndefined();
228
+ const result = response!.result as Record<string, unknown>;
229
+ expect(result.protocolVersion).toBe("2026-07-28");
230
+ expect(result.serverInfo).toBeDefined();
231
+ expect(result.capabilities).toBeDefined();
232
+ expect(Array.isArray(result.tools)).toBe(true);
233
+ expect(Array.isArray(result.resources)).toBe(true);
234
+ });
235
+
236
+ test("discover's tools list matches tools/list", async () => {
237
+ const discover = await server.handleRequest({ jsonrpc: "2.0", id: 1, method: "server/discover", params: {} });
238
+ const list = await server.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
239
+ const discoverNames = (discover!.result as { tools: Array<{ name: string }> }).tools.map((t) => t.name).sort();
240
+ const listNames = (list!.result as { tools: Array<{ name: string }> }).tools.map((t) => t.name).sort();
241
+ expect(discoverNames).toEqual(listNames);
242
+ });
243
+
244
+ test("discover's resources list matches resources/list, including plugin contributions", async () => {
245
+ const plugin = createMockPlugin({
246
+ name: "test-lex",
247
+ mcpResources: () => [
248
+ {
249
+ uri: "catalog",
250
+ name: "Test Catalog",
251
+ description: "Test resource catalog",
252
+ mimeType: "application/json",
253
+ handler: async () => "[]",
254
+ },
255
+ ],
256
+ });
257
+ const s = new McpServer([plugin]);
258
+ const discover = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "server/discover", params: {} });
259
+ const list = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "resources/list" });
260
+ const discoverUris = (discover!.result as { resources: Array<{ uri: string }> }).resources.map((r) => r.uri).sort();
261
+ const listUris = (list!.result as { resources: Array<{ uri: string }> }).resources.map((r) => r.uri).sort();
262
+ expect(discoverUris).toEqual(listUris);
263
+ expect(discoverUris).toContain("chant://test-lex/catalog");
264
+ });
265
+
266
+ test("negotiates protocol version the same way as initialize", async () => {
267
+ const response = await server.handleRequest({
268
+ jsonrpc: "2.0",
269
+ id: 1,
270
+ method: "server/discover",
271
+ params: { protocolVersion: "2024-11-05" },
272
+ });
273
+ const result = response!.result as Record<string, unknown>;
274
+ expect(result.protocolVersion).toBe("2024-11-05");
275
+ });
276
+ });
277
+
278
+ // -----------------------------------------------------------------------
279
+ // Structured tool output (#1194)
280
+ // -----------------------------------------------------------------------
281
+
282
+ describe("structured tool output", () => {
283
+ test("an object-returning handler gets structuredContent alongside text", async () => {
284
+ const response = await server.handleRequest({
285
+ jsonrpc: "2.0",
286
+ id: 1,
287
+ method: "tools/call",
288
+ params: { name: "search", arguments: { query: "bucket" } },
289
+ });
290
+ const result = response!.result as { content: Array<{ text: string }>; structuredContent?: Record<string, unknown> };
291
+ expect(result.structuredContent).toBeDefined();
292
+ expect(result.structuredContent!.query).toBe("bucket");
293
+ const parsedText = JSON.parse(result.content[0].text);
294
+ expect(result.structuredContent).toEqual(parsedText);
295
+ });
296
+
297
+ test("a string-returning handler has no structuredContent", async () => {
298
+ const plugin = createMockPlugin({
299
+ name: "test-lex",
300
+ mcpTools: () => [
301
+ {
302
+ name: "greet",
303
+ description: "Greet",
304
+ inputSchema: { type: "object", properties: {} },
305
+ handler: async () => "hello from plugin",
306
+ },
307
+ ],
308
+ });
309
+ const s = new McpServer([plugin]);
310
+ const response = await s.handleRequest({
311
+ jsonrpc: "2.0",
312
+ id: 1,
313
+ method: "tools/call",
314
+ params: { name: "test-lex:greet", arguments: {} },
315
+ });
316
+ const result = response!.result as { structuredContent?: unknown };
317
+ expect(result.structuredContent).toBeUndefined();
318
+ });
319
+
320
+ test("an isError response has no structuredContent", async () => {
321
+ const response = await server.handleRequest({
322
+ jsonrpc: "2.0",
323
+ id: 1,
324
+ method: "tools/call",
325
+ params: { name: "unknown-tool", arguments: {} },
326
+ });
327
+ const result = response!.result as { isError: boolean; structuredContent?: unknown };
328
+ expect(result.isError).toBe(true);
329
+ expect(result.structuredContent).toBeUndefined();
330
+ });
68
331
  });
69
332
 
70
333
  describe("error handling", () => {
@@ -7,11 +7,50 @@ import { explainTool, handleExplain } from "./tools/explain";
7
7
  import { scaffoldTool, createScaffoldHandler } from "./tools/scaffold";
8
8
  import { searchTool, createSearchHandler } from "./tools/search";
9
9
  import type { LexiconPlugin } from "../../lexicon";
10
- import type { McpRequest, McpResponse, ToolDefinition, ToolHandler, ResourceDefinition } from "./types";
10
+ import type { McpRequest, McpResponse, McpRequestMeta, ToolDefinition, ToolHandler, ResourceDefinition } from "./types";
11
11
  import { createSnapshotTool, createDiffTool } from "./lifecycle-tools";
12
12
  import { createOpListTool, createOpRunTool, createOpStatusTool, createOpSignalTool, createOpReportTool } from "./op-tools";
13
13
  import { buildResourcesList, handleResourcesRead } from "./resource-handlers";
14
14
 
15
+ /**
16
+ * Protocol versions this server understands, newest first. `initialize` and
17
+ * `server/discover` both negotiate against this list rather than assuming
18
+ * the client's revision (#1194).
19
+ */
20
+ const SUPPORTED_PROTOCOL_VERSIONS = ["2026-07-28", "2024-11-05"] as const;
21
+ const LATEST_PROTOCOL_VERSION = SUPPORTED_PROTOCOL_VERSIONS[0];
22
+
23
+ /**
24
+ * Pick the protocol version to answer with: the client's requested version
25
+ * when we support it, otherwise our latest. A 2024-11-05 client that asks
26
+ * for `2024-11-05` gets it back unchanged; a 2026-07-28 client — or one
27
+ * that never says — gets the latest revision (#1194).
28
+ */
29
+ export function negotiateProtocolVersion(requested: string | undefined): string {
30
+ if (requested && (SUPPORTED_PROTOCOL_VERSIONS as readonly string[]).includes(requested)) {
31
+ return requested;
32
+ }
33
+ return LATEST_PROTOCOL_VERSION;
34
+ }
35
+
36
+ /**
37
+ * Parse the `_meta` envelope the 2026-07-28 revision puts on every request
38
+ * (client identity, requested protocol version), falling back to the
39
+ * top-level `protocolVersion`/`clientInfo` fields a prior-revision client
40
+ * sends on `initialize`. Read-side only — the server holds no handshake
41
+ * state to update (#1194).
42
+ */
43
+ export function parseMeta(params: Record<string, unknown>): { protocolVersion?: string; clientInfo?: { name: string; version?: string } } {
44
+ const meta = (params._meta ?? {}) as McpRequestMeta;
45
+ const protocolVersion =
46
+ (typeof meta.protocolVersion === "string" ? meta.protocolVersion : undefined) ??
47
+ (typeof params.protocolVersion === "string" ? params.protocolVersion : undefined);
48
+ const clientInfo =
49
+ meta["io.modelcontextprotocol/clientInfo"] ??
50
+ (params.clientInfo as { name: string; version?: string } | undefined);
51
+ return { protocolVersion, clientInfo };
52
+ }
53
+
15
54
  /**
16
55
  * The name a lexicon's MCP tool is registered under: `<lexicon>:<verb>`,
17
56
  * whether or not the lexicon already wrote the prefix itself (#1341).
@@ -135,9 +174,19 @@ export class McpServer {
135
174
  }
136
175
 
137
176
  /**
138
- * Handle incoming MCP request
177
+ * Handle incoming MCP request.
178
+ *
179
+ * A message with no `id` is a notification — `notifications/initialized`
180
+ * being the one every client sends — and gets no response in any
181
+ * protocol revision (#1194). Returns `null` for those; callers (see
182
+ * {@link start}) simply skip writing anything back.
139
183
  */
140
- async handleRequest(request: McpRequest): Promise<McpResponse> {
184
+ async handleRequest(request: McpRequest & { id: string | number }): Promise<McpResponse>;
185
+ async handleRequest(request: McpRequest): Promise<McpResponse | null>;
186
+ async handleRequest(request: McpRequest): Promise<McpResponse | null> {
187
+ if (request.id === undefined) {
188
+ return null;
189
+ }
141
190
  try {
142
191
  const result = await this.dispatch(request.method, request.params ?? {});
143
192
  return {
@@ -157,16 +206,35 @@ export class McpServer {
157
206
  }
158
207
  }
159
208
 
209
+ /**
210
+ * The `initialize` result — also the basis of `server/discover` below.
211
+ */
212
+ private buildInitializeResult(params: Record<string, unknown>): Record<string, unknown> {
213
+ const { protocolVersion } = parseMeta(params);
214
+ return {
215
+ protocolVersion: negotiateProtocolVersion(protocolVersion),
216
+ capabilities: { tools: {}, resources: {} },
217
+ serverInfo: { name: "chant", version: "0.1.0" },
218
+ };
219
+ }
220
+
160
221
  /**
161
222
  * Dispatch request to appropriate handler
162
223
  */
163
224
  private async dispatch(method: string, params: Record<string, unknown>): Promise<unknown> {
164
225
  switch (method) {
165
226
  case "initialize":
227
+ // Answered for prior-revision clients too — negotiated, not hard-coded (#1194).
228
+ return this.buildInitializeResult(params);
229
+
230
+ case "server/discover":
231
+ // On-demand capability discovery (#1194): the initialize result
232
+ // merged with the tools/resources listings, so a 2026-07-28 client
233
+ // never has to call `initialize` at all.
166
234
  return {
167
- protocolVersion: "2024-11-05",
168
- capabilities: { tools: {}, resources: {} },
169
- serverInfo: { name: "chant", version: "0.1.0" },
235
+ ...this.buildInitializeResult(params),
236
+ tools: Array.from(this.tools.values()),
237
+ resources: buildResourcesList(this.pluginResources).resources,
170
238
  };
171
239
 
172
240
  case "tools/list":
@@ -203,6 +271,7 @@ export class McpServer {
203
271
 
204
272
  try {
205
273
  const result = await handler(toolParams);
274
+ const isStructured = typeof result === "object" && result !== null;
206
275
  return {
207
276
  content: [
208
277
  {
@@ -210,6 +279,11 @@ export class McpServer {
210
279
  text: typeof result === "string" ? result : JSON.stringify(result, null, 2),
211
280
  },
212
281
  ],
282
+ // Structured output (2025-06-18+): handlers already return plain
283
+ // objects that get stringified above, so agents that want the
284
+ // parsed shape directly get it here too, alongside the text block
285
+ // kept for backward compatibility (#1194).
286
+ ...(isStructured ? { structuredContent: result } : {}),
213
287
  };
214
288
  } catch (error) {
215
289
  return {
@@ -238,7 +312,10 @@ export class McpServer {
238
312
  try {
239
313
  const request = JSON.parse(line) as McpRequest;
240
314
  const response = await this.handleRequest(request);
241
- console.log(JSON.stringify(response));
315
+ // Notifications (#1194) get no response at all — not even an empty one.
316
+ if (response !== null) {
317
+ console.log(JSON.stringify(response));
318
+ }
242
319
  } catch (error) {
243
320
  const errorResponse: McpResponse = {
244
321
  jsonrpc: "2.0",
@@ -1,9 +1,13 @@
1
1
  /**
2
2
  * MCP message types
3
+ *
4
+ * `id` is optional per JSON-RPC 2.0: a message with no `id` is a
5
+ * notification (e.g. `notifications/initialized`) and gets no response,
6
+ * in every protocol revision (#1194).
3
7
  */
4
8
  export interface McpRequest {
5
9
  jsonrpc: "2.0";
6
- id: string | number;
10
+ id?: string | number;
7
11
  method: string;
8
12
  params?: Record<string, unknown>;
9
13
  }
@@ -19,6 +23,18 @@ export interface McpResponse {
19
23
  };
20
24
  }
21
25
 
26
+ /**
27
+ * The `_meta` envelope the 2026-07-28 revision puts on every request so it
28
+ * is self-contained: client identity and the client's requested protocol
29
+ * version travel here instead of living in handshake state (#1194). A
30
+ * prior-revision client instead sends `protocolVersion`/`clientInfo` as
31
+ * top-level `initialize` params — both forms are read by {@link parseMeta}.
32
+ */
33
+ export interface McpRequestMeta {
34
+ protocolVersion?: string;
35
+ "io.modelcontextprotocol/clientInfo"?: { name: string; version?: string };
36
+ }
37
+
22
38
  /**
23
39
  * Tool definition for MCP
24
40
  */
@@ -30,6 +46,16 @@ export interface ToolDefinition {
30
46
  properties: Record<string, unknown>;
31
47
  required?: string[];
32
48
  };
49
+ /**
50
+ * JSON schema for `structuredContent` (2025-06-18+), when a tool's result
51
+ * shape is obvious and stable enough to declare. Optional and additive —
52
+ * most tools omit it (#1194).
53
+ */
54
+ outputSchema?: {
55
+ type: "object";
56
+ properties: Record<string, unknown>;
57
+ required?: string[];
58
+ };
33
59
  }
34
60
 
35
61
  /**
@@ -26,7 +26,20 @@ export interface ParsedArgs {
26
26
  temporal?: boolean;
27
27
  /** `chant run` — emit the structured OpRunResult as JSON on stdout. */
28
28
  json?: boolean;
29
- /** `chant run --components <name|all> --progress-json` — stream one NDJSON `RunProgressEvent` (../../components/run-progress.ts) per line to stdout while the run executes (local executor only), so a consumer can render live wave/component/phase/step progress instead of tailing raw logs. Purely additive: run semantics, ordering, and exit code are unchanged; omitted (undefined, not false) when the flag isn't passed. */
29
+ /**
30
+ * `chant run --components <name|all> --progress-json` (local executor) —
31
+ * stream one NDJSON `RunProgressEvent` (../../components/run-progress.ts)
32
+ * per line to stdout while the run executes, so a consumer can render live
33
+ * wave/component/phase/step progress instead of tailing raw logs.
34
+ *
35
+ * `chant run <name> --temporal --progress-json` (chant #1676) — the same
36
+ * flag on an Op's durable path streams one NDJSON `StepRecord`
37
+ * (../../op/local-executor.ts, reconstructed from workflow history by
38
+ * ../handlers/op-progress.ts) per settled step instead.
39
+ *
40
+ * Both are purely additive: run semantics, ordering, and exit code are
41
+ * unchanged; omitted (undefined, not false) when the flag isn't passed.
42
+ */
30
43
  progressJson?: boolean;
31
44
  live: boolean;
32
45
  /** `chant migrate --from <name>` (default "github") */
@@ -185,6 +198,25 @@ export interface ParsedArgs {
185
198
  * what is declared, which is a broader read and a different claim.
186
199
  */
187
200
  ambient?: boolean;
201
+ /**
202
+ * `chant search "<q>" --at <ref> --check-live --env <name>` (#1268) —
203
+ * additionally read the estate live and diff the matched rows against the
204
+ * snapshot the answer came from, reusing `diffLive` (the same engine
205
+ * `lifecycle diff --live` uses) scoped to just those rows. Requires `--at`.
206
+ */
207
+ checkLive?: boolean;
208
+ /**
209
+ * `chant search "<q>" --live --check-snapshot --env <name>` (#1268) — the
210
+ * reverse of `--check-live`: answer live, diff the matched rows against the
211
+ * most recently recorded snapshot. Requires `--live`.
212
+ */
213
+ checkSnapshot?: boolean;
214
+ /**
215
+ * `chant search "<q>" --check-live|--check-snapshot --fail-on-drift`
216
+ * (#1268) — exit non-zero when the scoped check finds drift, so it is usable
217
+ * as a CI gate. Meaningless without one of the two flags above.
218
+ */
219
+ failOnDrift?: boolean;
188
220
 
189
221
  /** `chant dev surface-diff --run-examples` — also run the example build harness */
190
222
  runExamples?: boolean;
@@ -194,9 +226,9 @@ export interface ParsedArgs {
194
226
  check?: boolean;
195
227
  /** `chant dev surface-diff --update-snapshot --bump` — bump the lexicon's package.json version by the drift severity so the accepted surface is publishable (#616). */
196
228
  bump?: boolean;
197
- /** `chant components release record --component <name>` (#568) — component name for the release record being appended. */
229
+ /** `chant components release record --component <name>` (#568) — component name for the release record being appended. Also `chant components export <env> --component <name>` (#929) — which component's most recent recorded build to export. */
198
230
  component?: string;
199
- /** `chant components release record --digest <sha256:...>` (#568) — artifact digest to record, joining this release to the build archive/ledger. */
231
+ /** `chant components release record --digest <sha256:...>` (#568) — artifact digest to record, joining this release to the build archive/ledger. Also `chant components export --digest <manifestDigest>` (#929) — a build archive manifest digest to export directly, bypassing env/component resolution. */
200
232
  digest?: string;
201
233
  /** `chant components release record --git-sha <sha>` (#568) — git commit the deploy was built from. */
202
234
  gitSha?: string;
@@ -218,6 +250,10 @@ export interface ParsedArgs {
218
250
  fold?: boolean;
219
251
  /** `chant build --sandbox` (#1045 Phase 2) — opt-in: run-fallback source files (or every file, without `--fold`) execute together, isolated, in one sandboxed child process instead of in-process. Also settable project-wide via `chant.config.ts`'s `build.sandbox: true`; the flag always wins when set. Default (flag omitted): in-process execution, unchanged. */
220
252
  sandbox?: boolean;
253
+ /** `chant build --fold --fold-rank` (#1083) — after a `--fold` build, print blockers ranked by dominator retained-count over the forward import-failure graph, plus the separate reverse-taint bucket (chant #1044). Bare boolean form; see {@link foldRankCollapsedFile} for the file-writing form. No-op without `--fold`. */
254
+ foldRank?: boolean;
255
+ /** `chant build --fold --fold-rank <path>` (#1083) — same ranking as {@link foldRank}, ALSO exported in Brendan Gregg collapsed stack format (weighted by retained count) to `<path>`, so it renders in any flame/icicle viewer. Mutually exclusive with the bare-boolean form at the parse level (same context-sensitive lookahead as `--report`), but the text report still prints either way. */
256
+ foldRankCollapsedFile?: string;
221
257
  /** `chant build --param name=value` (#1064) — repeatable. Bound to `params.<name>` (`@intentius/chant/params`) for source to reference, after validation against `chant.config.ts`'s declared `buildParams`. Highest precedence over `--params-file`/a declared `env` mapping/the declared `default`. */
222
258
  param?: string[];
223
259
  /** `chant build --params-file <path>` (#1064) — a JSON file of `{ "name": value }` build-time parameter values. Second precedence, after `--param`. */
@@ -228,6 +264,14 @@ export interface ParsedArgs {
228
264
  * forgejo today) — the same generator `chant build --components --generate
229
265
  * <lexicon>` uses, reused rather than re-derived. */
230
266
  projection?: string;
267
+ /** `chant operator --interval <duration>` (#1485) — poll interval between rounds, e.g. "30s", "5m". Default: 60s. */
268
+ interval?: string;
269
+ /** `chant operator --lease-ttl <duration>` (#1485) — how long an acquired lease is valid before it's reclaimable by another operator. Default: 5m. */
270
+ leaseTtl?: string;
271
+ /** `chant operator --once` (#1485) — run a single round and exit, instead of looping until Ctrl-C. Also the offline test/cron-invoker story. */
272
+ once?: boolean;
273
+ /** `chant approve <op> <gate> --note <text>` (#1485) — optional free-text context recorded on the gate-resolution fact (e.g. a PR URL). */
274
+ note?: string;
231
275
  }
232
276
 
233
277
  /**
@@ -0,0 +1,42 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "vitest";
2
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { scanRules } from "./docs-rule-scanning";
6
+
7
+ // #1938: a post-synth check's `description` string containing a
8
+ // backslash-escaped quote used to truncate at the first embedded `"`,
9
+ // garbling the generated docs table entry.
10
+ describe("scanRules — post-synth description extraction (#1938)", () => {
11
+ let dir: string;
12
+
13
+ beforeAll(() => {
14
+ dir = mkdtempSync(join(tmpdir(), "docs-rule-scanning-"));
15
+ mkdirSync(join(dir, "lint", "post-synth"), { recursive: true });
16
+ writeFileSync(
17
+ join(dir, "lint", "post-synth", "wk8505.ts"),
18
+ [
19
+ "export const wk8505 = {",
20
+ ' id: "WK8505",',
21
+ ' description: "A rule\'s \\"why\\" must be non-empty, and a backslash like C:\\\\path must survive intact",',
22
+ " check() { return []; },",
23
+ "};",
24
+ "",
25
+ ].join("\n"),
26
+ );
27
+ });
28
+
29
+ afterAll(() => {
30
+ rmSync(dir, { recursive: true, force: true });
31
+ });
32
+
33
+ test("extracts the full description past an escaped quote and a backslash", () => {
34
+ const rules = scanRules(dir);
35
+ const rule = rules.find((r) => r.id === "WK8505");
36
+
37
+ expect(rule).toBeDefined();
38
+ expect(rule?.description).toBe(
39
+ 'A rule\'s "why" must be non-empty, and a backslash like C:\\path must survive intact',
40
+ );
41
+ });
42
+ });
@@ -92,7 +92,7 @@ function scanDir(dir: string, type: "lint" | "post-synth", out: ScannedRule[]):
92
92
  } else {
93
93
  // Extract from PostSynthCheck objects: id, description
94
94
  const idMatch = content.match(/id:\s*"([^"]+)"/);
95
- const descMatch = content.match(/description:\s*"([^"]+)"/);
95
+ const descMatch = content.match(/description:\s*"((?:[^"\\]|\\.)*)"/);
96
96
 
97
97
  if (idMatch) {
98
98
  out.push({
@@ -100,7 +100,7 @@ function scanDir(dir: string, type: "lint" | "post-synth", out: ScannedRule[]):
100
100
  id: idMatch[1],
101
101
  severity: "error",
102
102
  category: "post-synth",
103
- description: descMatch?.[1] ?? idMatch[1],
103
+ description: descMatch ? unescapeStringLiteral(descMatch[1]) : idMatch[1],
104
104
  type: "post-synth",
105
105
  },
106
106
  source: content,
@@ -110,6 +110,29 @@ function scanDir(dir: string, type: "lint" | "post-synth", out: ScannedRule[]):
110
110
  }
111
111
  }
112
112
 
113
+ /**
114
+ * Undo the backslash escaping of a double-quoted JS/TS string literal body
115
+ * captured by a regex like `/description:\s*"((?:[^"\\]|\\.)*)"/` — the
116
+ * capture group still has the source's escape sequences (`\"`, `\\`, `\n`,
117
+ * ...) in it verbatim, so a description written as `"say \"hi\""` would
118
+ * otherwise render in generated docs as `say \"hi\"` instead of `say "hi"`.
119
+ */
120
+ function unescapeStringLiteral(raw: string): string {
121
+ return raw.replace(/\\(.)/g, (_, ch: string) => {
122
+ switch (ch) {
123
+ case "n":
124
+ return "\n";
125
+ case "t":
126
+ return "\t";
127
+ case "r":
128
+ return "\r";
129
+ default:
130
+ // \" -> ", \\ -> \, \' -> ', and anything else -> itself unescaped
131
+ return ch;
132
+ }
133
+ });
134
+ }
135
+
113
136
  function extractDescriptionFromComment(
114
137
  content: string,
115
138
  ruleId: string,
@@ -33,6 +33,13 @@ adopting chant.
33
33
  work for a component whose own name isn't the entity/resource name it
34
34
  deploys, falling back to `name` itself when omitted. See
35
35
  docs/src/content/docs/components/observability.mdx.
36
+ - **Composite mapping** — `composites` (#1492), optional: the composite kind
37
+ name(s) this component's deployed resources come from, when a consumer
38
+ joining a composite graph to the component DAG (e.g. `chant graph
39
+ --components --format ir`'s node `attrs.composites`, ../cli/handlers/
40
+ graph.ts) can't infer it by kebab-casing `name` against each composite's
41
+ kind. Omitted entirely (the default), that naming convention stays in
42
+ effect with no behavior change.
36
43
  - **The three archetypes** — service (build → publish → apply → verify),
37
44
  infra (apply → verify, no build), and producer/library (build → publish
38
45
  only, no service apply). `deploy` being a plain composition is what makes