@intentius/chant 0.49.0 → 0.50.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.
- package/dist/audit/catalog.d.ts +13 -3
- package/dist/audit/catalog.d.ts.map +1 -1
- package/dist/audit/core.d.ts +9 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +6 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/fetch.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +6 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/report.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/audit/secrets.d.ts +95 -0
- package/dist/audit/secrets.d.ts.map +1 -0
- package/dist/audit/wrangler.d.ts +33 -0
- package/dist/audit/wrangler.d.ts.map +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts +23 -0
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +31 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +11 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +32 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -0
- package/dist/cli/handlers/scenario.d.ts +39 -0
- package/dist/cli/handlers/scenario.d.ts.map +1 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +35 -2
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +29 -1
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
- package/dist/components/capability.d.ts +17 -2
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +7 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +15 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/verbs/index.d.ts +6 -1
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts +499 -0
- package/dist/components/verbs/run-agent.d.ts.map +1 -0
- package/dist/components/verbs/sign.d.ts +30 -0
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/composite.d.ts +6 -1
- package/dist/composite.d.ts.map +1 -1
- package/dist/discovery/collect.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -1
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-rank.d.ts +66 -0
- package/dist/discovery/fold-rank.d.ts.map +1 -0
- package/dist/discovery/index.d.ts +15 -0
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +17 -0
- package/dist/discovery/param-deps.d.ts.map +1 -0
- package/dist/fold/fold.d.ts +55 -2
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +21 -14
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +2 -0
- package/dist/lexicon-schema.d.ts.map +1 -1
- package/dist/lexicon.d.ts +93 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +90 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
- package/dist/lifecycle/deep-diff.d.ts +18 -0
- package/dist/lifecycle/deep-diff.d.ts.map +1 -1
- package/dist/lifecycle/deep-observe.d.ts +9 -1
- package/dist/lifecycle/deep-observe.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +33 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
- package/dist/lifecycle/git.d.ts +145 -21
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +113 -0
- package/dist/lifecycle/lease.d.ts.map +1 -0
- package/dist/lifecycle/scenario-eval.d.ts +42 -0
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
- package/dist/lifecycle/scenario.d.ts +163 -0
- package/dist/lifecycle/scenario.d.ts.map +1 -0
- package/dist/lifecycle/symptoms.d.ts +63 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -0
- package/dist/lint/output-docs.d.ts +94 -0
- package/dist/lint/output-docs.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +29 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
- package/dist/lsp/lexicon-providers.d.ts +7 -0
- package/dist/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +139 -0
- package/dist/op/activity-contract.d.ts.map +1 -0
- package/dist/op/builders.d.ts +42 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/converge-rule.d.ts +161 -0
- package/dist/op/converge-rule.d.ts.map +1 -0
- package/dist/op/generate-pipeline.d.ts +39 -0
- package/dist/op/generate-pipeline.d.ts.map +1 -0
- package/dist/op/index.d.ts +14 -0
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts +42 -0
- package/dist/op/op-verb-class.d.ts.map +1 -0
- package/dist/op/operator.d.ts +128 -0
- package/dist/op/operator.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +187 -0
- package/dist/op/step-output-ref.d.ts.map +1 -0
- package/dist/op/types.d.ts +18 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/provenance.d.ts +73 -3
- package/dist/provenance.d.ts.map +1 -1
- package/dist/runtime-adapter.d.ts +7 -1
- package/dist/runtime-adapter.d.ts.map +1 -1
- package/dist/serializer.d.ts +18 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/toml.d.ts +40 -5
- package/dist/toml.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/audit/catalog.test.ts +1 -1
- package/src/audit/catalog.ts +75 -3
- package/src/audit/core.ts +9 -0
- package/src/audit/discover.ts +29 -2
- package/src/audit/fetch.test.ts +216 -3
- package/src/audit/fetch.ts +270 -59
- package/src/audit/report-html.ts +5 -2
- package/src/audit/report-model.ts +9 -0
- package/src/audit/report.test.ts +22 -0
- package/src/audit/report.ts +3 -2
- package/src/audit/rules-doc.ts +2 -0
- package/src/audit/secrets.test.ts +303 -0
- package/src/audit/secrets.ts +406 -0
- package/src/audit/wrangler.test.ts +230 -0
- package/src/audit/wrangler.ts +290 -0
- package/src/build.ts +8 -3
- package/src/cli/command-group.ts +1 -1
- package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
- package/src/cli/commands/audit.test.ts +215 -1
- package/src/cli/commands/audit.ts +86 -17
- package/src/cli/commands/build.test.ts +167 -2
- package/src/cli/commands/build.ts +114 -23
- package/src/cli/handlers/build.ts +2 -0
- package/src/cli/handlers/components.test.ts +199 -1
- package/src/cli/handlers/components.ts +160 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +10 -1
- package/src/cli/handlers/lifecycle.ts +12 -4
- package/src/cli/handlers/operator.test.ts +255 -0
- package/src/cli/handlers/operator.ts +240 -0
- package/src/cli/handlers/scenario.test.ts +456 -0
- package/src/cli/handlers/scenario.ts +330 -0
- package/src/cli/main.test.ts +23 -0
- package/src/cli/main.ts +72 -1
- package/src/cli/mcp/server.test.ts +265 -2
- package/src/cli/mcp/server.ts +84 -7
- package/src/cli/mcp/types.ts +27 -1
- package/src/cli/registry.ts +14 -2
- package/src/codegen/docs-rule-scanning.test.ts +42 -0
- package/src/codegen/docs-rule-scanning.ts +25 -2
- package/src/components/README.md +7 -0
- package/src/components/capability.ts +17 -2
- package/src/components/cli-support.test.ts +17 -0
- package/src/components/cli-support.ts +13 -1
- package/src/components/component-schema.test.ts +32 -0
- package/src/components/component.schema.json +6 -0
- package/src/components/component.test.ts +21 -0
- package/src/components/component.ts +15 -0
- package/src/components/driver.ts +12 -4
- package/src/components/verbs/index.ts +6 -1
- package/src/components/verbs/run-agent.test.ts +683 -0
- package/src/components/verbs/run-agent.ts +786 -0
- package/src/components/verbs/sign.test.ts +19 -0
- package/src/components/verbs/sign.ts +34 -2
- package/src/composite.ts +31 -2
- package/src/discovery/collect.ts +11 -2
- package/src/discovery/fold-import.test.ts +54 -0
- package/src/discovery/fold-import.ts +178 -38
- package/src/discovery/fold-rank.test.ts +197 -0
- package/src/discovery/fold-rank.ts +346 -0
- package/src/discovery/index.ts +16 -1
- package/src/discovery/param-deps.test.ts +118 -0
- package/src/discovery/param-deps.ts +170 -0
- package/src/fold/fold.test.ts +6 -2
- package/src/fold/fold.ts +184 -3
- package/src/fold/subset.test.ts +82 -19
- package/src/fold/subset.ts +79 -41
- package/src/lexicon-schema.ts +3 -0
- package/src/lexicon.ts +103 -2
- package/src/lifecycle/converge-ledger.test.ts +199 -0
- package/src/lifecycle/converge-ledger.ts +179 -0
- package/src/lifecycle/deep-diff.test.ts +79 -1
- package/src/lifecycle/deep-diff.ts +23 -0
- package/src/lifecycle/deep-observe.ts +13 -2
- package/src/lifecycle/gate-ledger.test.ts +103 -0
- package/src/lifecycle/gate-ledger.ts +140 -0
- package/src/lifecycle/git.test.ts +430 -0
- package/src/lifecycle/git.ts +446 -84
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/lease.test.ts +343 -0
- package/src/lifecycle/lease.ts +270 -0
- package/src/lifecycle/scenario-eval.test.ts +199 -0
- package/src/lifecycle/scenario-eval.ts +158 -0
- package/src/lifecycle/scenario.test.ts +195 -0
- package/src/lifecycle/scenario.ts +321 -0
- package/src/lifecycle/symptoms.test.ts +116 -0
- package/src/lifecycle/symptoms.ts +126 -0
- package/src/lint/output-docs.test.ts +220 -0
- package/src/lint/output-docs.ts +204 -0
- package/src/lint/post-synth.test.ts +97 -0
- package/src/lint/post-synth.ts +45 -0
- package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
- package/src/lint/rules/comp/comp.test.ts +49 -1
- package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
- package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
- package/src/lsp/lexicon-providers.test.ts +44 -0
- package/src/lsp/lexicon-providers.ts +11 -1
- package/src/op/activity-contract.test.ts +180 -0
- package/src/op/activity-contract.ts +278 -0
- package/src/op/builders-exports.test.ts +17 -1
- package/src/op/builders.ts +59 -5
- package/src/op/converge-rule.test.ts +179 -0
- package/src/op/converge-rule.ts +311 -0
- package/src/op/generate-pipeline.test.ts +53 -0
- package/src/op/generate-pipeline.ts +99 -0
- package/src/op/index.ts +30 -0
- package/src/op/local-executor.test.ts +92 -0
- package/src/op/local-executor.ts +45 -9
- package/src/op/op-verb-class.test.ts +126 -0
- package/src/op/op-verb-class.ts +115 -0
- package/src/op/operator.test.ts +346 -0
- package/src/op/operator.ts +213 -0
- package/src/op/step-output-ref.test.ts +334 -0
- package/src/op/step-output-ref.ts +453 -0
- package/src/op/types.ts +18 -1
- package/src/provenance.test.ts +151 -4
- package/src/provenance.ts +118 -4
- package/src/runtime-adapter.ts +31 -10
- package/src/serializer.ts +18 -0
- package/src/toml.test.ts +157 -384
- 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
|
-
|
|
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
|
|
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", () => {
|
package/src/cli/mcp/server.ts
CHANGED
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
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",
|
package/src/cli/mcp/types.ts
CHANGED
|
@@ -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
|
|
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
|
/**
|
package/src/cli/registry.ts
CHANGED
|
@@ -194,9 +194,9 @@ export interface ParsedArgs {
|
|
|
194
194
|
check?: boolean;
|
|
195
195
|
/** `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
196
|
bump?: boolean;
|
|
197
|
-
/** `chant components release record --component <name>` (#568) — component name for the release record being appended. */
|
|
197
|
+
/** `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
198
|
component?: string;
|
|
199
|
-
/** `chant components release record --digest <sha256:...>` (#568) — artifact digest to record, joining this release to the build archive/ledger. */
|
|
199
|
+
/** `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
200
|
digest?: string;
|
|
201
201
|
/** `chant components release record --git-sha <sha>` (#568) — git commit the deploy was built from. */
|
|
202
202
|
gitSha?: string;
|
|
@@ -218,6 +218,10 @@ export interface ParsedArgs {
|
|
|
218
218
|
fold?: boolean;
|
|
219
219
|
/** `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
220
|
sandbox?: boolean;
|
|
221
|
+
/** `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`. */
|
|
222
|
+
foldRank?: boolean;
|
|
223
|
+
/** `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. */
|
|
224
|
+
foldRankCollapsedFile?: string;
|
|
221
225
|
/** `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
226
|
param?: string[];
|
|
223
227
|
/** `chant build --params-file <path>` (#1064) — a JSON file of `{ "name": value }` build-time parameter values. Second precedence, after `--param`. */
|
|
@@ -228,6 +232,14 @@ export interface ParsedArgs {
|
|
|
228
232
|
* forgejo today) — the same generator `chant build --components --generate
|
|
229
233
|
* <lexicon>` uses, reused rather than re-derived. */
|
|
230
234
|
projection?: string;
|
|
235
|
+
/** `chant operator --interval <duration>` (#1485) — poll interval between rounds, e.g. "30s", "5m". Default: 60s. */
|
|
236
|
+
interval?: string;
|
|
237
|
+
/** `chant operator --lease-ttl <duration>` (#1485) — how long an acquired lease is valid before it's reclaimable by another operator. Default: 5m. */
|
|
238
|
+
leaseTtl?: string;
|
|
239
|
+
/** `chant operator --once` (#1485) — run a single round and exit, instead of looping until Ctrl-C. Also the offline test/cron-invoker story. */
|
|
240
|
+
once?: boolean;
|
|
241
|
+
/** `chant approve <op> <gate> --note <text>` (#1485) — optional free-text context recorded on the gate-resolution fact (e.g. a PR URL). */
|
|
242
|
+
note?: string;
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
/**
|
|
@@ -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
|
|
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,
|
package/src/components/README.md
CHANGED
|
@@ -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
|
|
@@ -44,8 +44,23 @@ export interface Capability<In = unknown, Out = unknown> {
|
|
|
44
44
|
readonly kind: string;
|
|
45
45
|
/** Perform the operation. */
|
|
46
46
|
run(ctx: DeployContext, input: In): Promise<Out>;
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Optional paired compensation, invoked in reverse order on saga rollback.
|
|
49
|
+
* `output`, when supplied, is the exact value this step's own `run()` call
|
|
50
|
+
* returned (#1944, epic #1564 phase 4) — a serializable identity channel a
|
|
51
|
+
* capability can use to recover state `rollback` needs when it cannot rely
|
|
52
|
+
* on in-process object identity between its `run`/`rollback` calls. The
|
|
53
|
+
* local interpret driver (../driver.ts) always threads it through; the
|
|
54
|
+
* durable Temporal path (lexicons/temporal/src/component-op/*.ts) threads
|
|
55
|
+
* it across the Activity boundary as plain JSON, which is exactly the case
|
|
56
|
+
* this exists for — see ./verbs/run-agent.ts's "Rollback identity" doc
|
|
57
|
+
* comment for the motivating gap (a fresh sprite's checkpoint id, recorded
|
|
58
|
+
* only in an in-process `WeakMap` keyed by `run()`'s `input` object, never
|
|
59
|
+
* survives to a `rollback()` call that runs as a separate Activity with its
|
|
60
|
+
* own freshly-resolved `input`). Optional and additive: a capability that
|
|
61
|
+
* never needs it (most of them) simply ignores the third parameter.
|
|
62
|
+
*/
|
|
63
|
+
rollback?(ctx: DeployContext, input: In, output?: Out): Promise<void>;
|
|
49
64
|
/**
|
|
50
65
|
* How this verb relates to rollback, for the COMP003 composition check.
|
|
51
66
|
* Usually derivable and left unset: a capability with a `rollback` method is
|
|
@@ -371,6 +371,23 @@ export const backup =
|
|
|
371
371
|
expect(result.liveNames?.postgres).toEqual(["pgDeployment", "pgService", "pgClaim"]);
|
|
372
372
|
expect(result.liveNames?.plain).toEqual(["plain"]);
|
|
373
373
|
});
|
|
374
|
+
|
|
375
|
+
test("carries each component's declared composites, absent (not defaulted) when undeclared (#1492)", async () => {
|
|
376
|
+
await writeFile(
|
|
377
|
+
join(testDir, "aws-plane.component.ts"),
|
|
378
|
+
`export const awsPlane = { name: "aws-plane", composites: ["ArtifactBucket", "OperatorRole"], dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "test" }] }] };`,
|
|
379
|
+
);
|
|
380
|
+
await writeFile(
|
|
381
|
+
join(testDir, "plain.component.ts"),
|
|
382
|
+
`export const plain = { name: "plain", dependsOn: [], deploy: [{ phase: "Apply", steps: [{ kind: "shell", reason: "test" }] }] };`,
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
const result = await computeComponentGraph(testDir);
|
|
386
|
+
|
|
387
|
+
expect(result.success).toBe(true);
|
|
388
|
+
expect(result.composites?.["aws-plane"]).toEqual(["ArtifactBucket", "OperatorRole"]);
|
|
389
|
+
expect(result.composites?.plain).toBeUndefined();
|
|
390
|
+
});
|
|
374
391
|
});
|
|
375
392
|
|
|
376
393
|
// ── runComponents (#585) ─────────────────────────────────────────────────────
|