@intentius/chant 0.0.18 → 0.0.22
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/bin/chant +4 -1
- package/package.json +20 -1
- package/src/build.test.ts +4 -2
- package/src/build.ts +3 -0
- package/src/builder.test.ts +3 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -3
- package/src/cli/commands/build.ts +5 -12
- package/src/cli/commands/diff.test.ts +2 -1
- package/src/cli/commands/diff.ts +2 -1
- package/src/cli/commands/init-lexicon.test.ts +0 -9
- package/src/cli/commands/init-lexicon.ts +0 -94
- package/src/cli/commands/init.ts +2 -20
- package/src/cli/handlers/build.ts +3 -3
- package/src/cli/handlers/lint.ts +2 -2
- package/src/cli/handlers/spell.ts +396 -0
- package/src/cli/handlers/state.ts +230 -0
- package/src/cli/lsp/server.test.ts +4 -0
- package/src/cli/main.ts +37 -3
- package/src/cli/mcp/server.test.ts +13 -9
- package/src/cli/mcp/server.ts +220 -6
- package/src/cli/mcp/tools/build.ts +2 -1
- package/src/cli/plugins.ts +1 -1
- package/src/cli/reporters/stylish.test.ts +2 -2
- package/src/cli/reporters/stylish.ts +1 -1
- package/src/composite.test.ts +1 -1
- package/src/config.ts +4 -0
- package/src/declarable.test.ts +2 -1
- package/src/declarable.ts +1 -1
- package/src/discovery/graph.test.ts +40 -0
- package/src/discovery/import.test.ts +5 -5
- package/src/discovery/resolve.test.ts +20 -0
- package/src/discovery/resolve.ts +2 -2
- package/src/index.ts +2 -0
- package/src/lexicon.ts +24 -0
- package/src/lint/rule-options.test.ts +3 -3
- package/src/lint/rule-registry.test.ts +1 -1
- package/src/lint/rules/composite-scope.ts +1 -1
- package/src/serializer-walker.ts +2 -1
- package/src/spell/discovery.ts +183 -0
- package/src/spell/index.ts +3 -0
- package/src/spell/prompt.ts +133 -0
- package/src/spell/types.ts +89 -0
- package/src/state/digest.ts +88 -0
- package/src/state/git.ts +317 -0
- package/src/state/index.ts +4 -0
- package/src/state/snapshot.ts +179 -0
- package/src/state/types.ts +59 -0
- package/src/types.ts +2 -1
- package/src/utils.test.ts +16 -3
- package/src/utils.ts +31 -1
- package/src/validation.test.ts +11 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/getting-started.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/lint-rules.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/serialization.mdx +0 -6
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/actions/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/composites/.gitkeep +0 -0
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +0 -11
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +0 -10
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +0 -10
- package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/post-synth/.gitkeep +0 -0
|
@@ -10,6 +10,10 @@ function createMockPlugin(overrides?: Partial<LexiconPlugin>): LexiconPlugin {
|
|
|
10
10
|
return {
|
|
11
11
|
name: "mock",
|
|
12
12
|
serializer: { name: "mock", serialize: () => "" } as unknown as Serializer,
|
|
13
|
+
generate: async () => {},
|
|
14
|
+
validate: async () => {},
|
|
15
|
+
coverage: async () => {},
|
|
16
|
+
package: async () => {},
|
|
13
17
|
...overrides,
|
|
14
18
|
};
|
|
15
19
|
}
|
|
@@ -401,11 +405,11 @@ describe("McpServer", () => {
|
|
|
401
405
|
test("passes template name to initTemplates", async () => {
|
|
402
406
|
const plugin = createMockPlugin({
|
|
403
407
|
name: "test-lex",
|
|
404
|
-
initTemplates: (template?: string) => {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return { src
|
|
408
|
+
initTemplates: (template?: string | undefined) => {
|
|
409
|
+
const src: Record<string, string> = template === "special"
|
|
410
|
+
? { "special.ts": "export const special = {};" }
|
|
411
|
+
: { "default.ts": "export const def = {};" };
|
|
412
|
+
return { src };
|
|
409
413
|
},
|
|
410
414
|
});
|
|
411
415
|
|
|
@@ -958,19 +962,19 @@ describe("McpServer", () => {
|
|
|
958
962
|
|
|
959
963
|
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
|
|
960
964
|
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
961
|
-
expect(tools).toHaveLength(
|
|
962
|
-
expect(tools.map((t) => t.name).sort()).toEqual(["build", "explain", "import", "lint", "scaffold", "search"]);
|
|
965
|
+
expect(tools).toHaveLength(9);
|
|
966
|
+
expect(tools.map((t) => t.name).sort()).toEqual(["build", "explain", "import", "lint", "scaffold", "search", "spell-done", "state-diff", "state-snapshot"]);
|
|
963
967
|
|
|
964
968
|
const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
|
|
965
969
|
const resources = (resourcesRes.result as { resources: Array<{ uri: string }> }).resources;
|
|
966
|
-
expect(resources).toHaveLength(
|
|
970
|
+
expect(resources).toHaveLength(7);
|
|
967
971
|
});
|
|
968
972
|
|
|
969
973
|
test("server with empty plugins array works", async () => {
|
|
970
974
|
const s = new McpServer([]);
|
|
971
975
|
const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
|
|
972
976
|
const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
|
|
973
|
-
expect(tools).toHaveLength(
|
|
977
|
+
expect(tools).toHaveLength(9);
|
|
974
978
|
});
|
|
975
979
|
});
|
|
976
980
|
});
|
package/src/cli/mcp/server.ts
CHANGED
|
@@ -9,6 +9,14 @@ import { searchTool, createSearchHandler } from "./tools/search";
|
|
|
9
9
|
import { getContext } from "./resources/context";
|
|
10
10
|
import type { LexiconPlugin } from "../../lexicon";
|
|
11
11
|
import type { McpToolContribution, McpResourceContribution } from "../../mcp/types";
|
|
12
|
+
import { readSnapshot, readEnvironmentSnapshots } from "../../state/git";
|
|
13
|
+
import { build } from "../../build";
|
|
14
|
+
import { computeBuildDigest, diffDigests } from "../../state/digest";
|
|
15
|
+
import { takeSnapshot } from "../../state/snapshot";
|
|
16
|
+
import type { StateSnapshot } from "../../state/types";
|
|
17
|
+
import { discoverSpells } from "../../spell/discovery";
|
|
18
|
+
import { generatePrompt } from "../../spell/prompt";
|
|
19
|
+
import { getRuntime } from "../../runtime-adapter";
|
|
12
20
|
|
|
13
21
|
/**
|
|
14
22
|
* MCP message types
|
|
@@ -31,12 +39,6 @@ interface McpResponse {
|
|
|
31
39
|
};
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
interface McpNotification {
|
|
35
|
-
jsonrpc: "2.0";
|
|
36
|
-
method: string;
|
|
37
|
-
params?: Record<string, unknown>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
42
|
/**
|
|
41
43
|
* Tool definition for MCP
|
|
42
44
|
*/
|
|
@@ -77,6 +79,117 @@ export class McpServer {
|
|
|
77
79
|
this.registerTool(scaffoldTool, createScaffoldHandler(plugins ?? []));
|
|
78
80
|
this.registerTool(searchTool, createSearchHandler(plugins ?? []));
|
|
79
81
|
|
|
82
|
+
// Register state tools
|
|
83
|
+
this.registerTool(
|
|
84
|
+
{
|
|
85
|
+
name: "state-snapshot",
|
|
86
|
+
description: "Capture deployed state for an environment",
|
|
87
|
+
inputSchema: {
|
|
88
|
+
type: "object",
|
|
89
|
+
properties: {
|
|
90
|
+
environment: { type: "string", description: "Target environment" },
|
|
91
|
+
lexicon: { type: "string", description: "Optional — snapshot all lexicons if omitted" },
|
|
92
|
+
},
|
|
93
|
+
required: ["environment"],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
async (params) => {
|
|
97
|
+
const env = params.environment as string;
|
|
98
|
+
const lexiconFilter = params.lexicon as string | undefined;
|
|
99
|
+
const targetPlugins = lexiconFilter
|
|
100
|
+
? (plugins ?? []).filter((p) => p.name === lexiconFilter)
|
|
101
|
+
: (plugins ?? []);
|
|
102
|
+
const pluginsWithDescribe = targetPlugins.filter((p) => p.describeResources);
|
|
103
|
+
if (pluginsWithDescribe.length === 0) return "No plugins implement describeResources";
|
|
104
|
+
const serializers = (plugins ?? []).map((p) => p.serializer);
|
|
105
|
+
const buildResult = await build(resolve("."), serializers);
|
|
106
|
+
if (buildResult.errors.length > 0) return "Build failed";
|
|
107
|
+
const result = await takeSnapshot(env, pluginsWithDescribe, buildResult);
|
|
108
|
+
return { snapshots: result.snapshots.length, warnings: result.warnings, errors: result.errors };
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
this.registerTool(
|
|
113
|
+
{
|
|
114
|
+
name: "state-diff",
|
|
115
|
+
description: "Compare current build declarations against last snapshot's digest",
|
|
116
|
+
inputSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
environment: { type: "string", description: "Target environment" },
|
|
120
|
+
lexicon: { type: "string", description: "Optional — diff all lexicons if omitted" },
|
|
121
|
+
},
|
|
122
|
+
required: ["environment"],
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
async (params) => {
|
|
126
|
+
const env = params.environment as string;
|
|
127
|
+
const lexiconFilter = params.lexicon as string | undefined;
|
|
128
|
+
const serializers = (plugins ?? []).map((p) => p.serializer);
|
|
129
|
+
const buildResult = await build(resolve("."), serializers);
|
|
130
|
+
if (buildResult.errors.length > 0) return "Build failed";
|
|
131
|
+
const currentDigest = computeBuildDigest(buildResult);
|
|
132
|
+
const lexicons = lexiconFilter ? [lexiconFilter] : buildResult.manifest.lexicons;
|
|
133
|
+
const results: Record<string, unknown> = {};
|
|
134
|
+
for (const lex of lexicons) {
|
|
135
|
+
const content = await readSnapshot(env, lex);
|
|
136
|
+
let previousDigest = undefined;
|
|
137
|
+
if (content) {
|
|
138
|
+
const snapshot: StateSnapshot = JSON.parse(content);
|
|
139
|
+
previousDigest = snapshot.digest;
|
|
140
|
+
}
|
|
141
|
+
results[lex] = diffDigests(currentDigest, previousDigest);
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
// Register spell tools
|
|
148
|
+
this.registerTool(
|
|
149
|
+
{
|
|
150
|
+
name: "spell-done",
|
|
151
|
+
description: "Mark a spell task as done",
|
|
152
|
+
inputSchema: {
|
|
153
|
+
type: "object",
|
|
154
|
+
properties: {
|
|
155
|
+
name: { type: "string", description: "Spell name" },
|
|
156
|
+
taskNumber: { type: "number", description: "Task number (1-based)" },
|
|
157
|
+
},
|
|
158
|
+
required: ["name", "taskNumber"],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
async (params) => {
|
|
162
|
+
const { readFileSync, writeFileSync } = await import("node:fs");
|
|
163
|
+
const { spells } = await discoverSpells();
|
|
164
|
+
const name = params.name as string;
|
|
165
|
+
const taskNumber = params.taskNumber as number;
|
|
166
|
+
const spell = spells.get(name);
|
|
167
|
+
if (!spell) return `Spell "${name}" not found`;
|
|
168
|
+
if (taskNumber < 1 || taskNumber > spell.definition.tasks.length) {
|
|
169
|
+
return `Invalid task number ${taskNumber}`;
|
|
170
|
+
}
|
|
171
|
+
const task = spell.definition.tasks[taskNumber - 1];
|
|
172
|
+
if (task.done) return `Task ${taskNumber} is already done`;
|
|
173
|
+
|
|
174
|
+
const content = readFileSync(spell.filePath, "utf-8");
|
|
175
|
+
let count = 0;
|
|
176
|
+
const rewritten = content.replace(
|
|
177
|
+
/task\(("[^"]*"|'[^']*'|`[^`]*`)((?:\s*,\s*\{[^}]*\})?)\)/g,
|
|
178
|
+
(match, desc, opts) => {
|
|
179
|
+
count++;
|
|
180
|
+
if (count !== taskNumber) return match;
|
|
181
|
+
if (opts && opts.includes("done:")) {
|
|
182
|
+
return match.replace(/done:\s*false/, "done: true");
|
|
183
|
+
}
|
|
184
|
+
return `task(${desc}, { done: true })`;
|
|
185
|
+
},
|
|
186
|
+
);
|
|
187
|
+
if (rewritten === content) return `Could not rewrite task ${taskNumber}`;
|
|
188
|
+
writeFileSync(spell.filePath, rewritten);
|
|
189
|
+
return `Task ${taskNumber} marked done: "${task.description}"`;
|
|
190
|
+
},
|
|
191
|
+
);
|
|
192
|
+
|
|
80
193
|
// Register plugin contributions
|
|
81
194
|
if (plugins) {
|
|
82
195
|
for (const plugin of plugins) {
|
|
@@ -268,6 +381,36 @@ export class McpServer {
|
|
|
268
381
|
description: "List of available chant examples",
|
|
269
382
|
mimeType: "application/json",
|
|
270
383
|
},
|
|
384
|
+
{
|
|
385
|
+
uri: "chant://spells",
|
|
386
|
+
name: "Spells",
|
|
387
|
+
description: "List all spells with status, tasks, and lexicon",
|
|
388
|
+
mimeType: "application/json",
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
uri: "chant://spell/{name}",
|
|
392
|
+
name: "Spell details",
|
|
393
|
+
description: "Show spell definition and status",
|
|
394
|
+
mimeType: "application/json",
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
uri: "chant://spell/{name}/prompt",
|
|
398
|
+
name: "Spell bootstrap prompt",
|
|
399
|
+
description: "Bootstrap prompt for agent consumption",
|
|
400
|
+
mimeType: "text/markdown",
|
|
401
|
+
},
|
|
402
|
+
{
|
|
403
|
+
uri: "chant://state/{environment}",
|
|
404
|
+
name: "State (all lexicons)",
|
|
405
|
+
description: "All lexicon snapshots for an environment",
|
|
406
|
+
mimeType: "application/json",
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
uri: "chant://state/{environment}/{lexicon}",
|
|
410
|
+
name: "State (single lexicon)",
|
|
411
|
+
description: "Single lexicon snapshot for an environment",
|
|
412
|
+
mimeType: "application/json",
|
|
413
|
+
},
|
|
271
414
|
];
|
|
272
415
|
|
|
273
416
|
// Merge plugin resources
|
|
@@ -322,6 +465,77 @@ export class McpServer {
|
|
|
322
465
|
};
|
|
323
466
|
}
|
|
324
467
|
|
|
468
|
+
// Spell resources
|
|
469
|
+
if (uri === "chant://spells") {
|
|
470
|
+
const { spells } = await discoverSpells();
|
|
471
|
+
const list = Array.from(spells.entries()).map(([name, s]) => ({
|
|
472
|
+
name,
|
|
473
|
+
status: s.status,
|
|
474
|
+
tasks: `${s.definition.tasks.filter((t) => t.done).length}/${s.definition.tasks.length}`,
|
|
475
|
+
lexicon: s.definition.lexicon ?? null,
|
|
476
|
+
overview: s.definition.overview,
|
|
477
|
+
}));
|
|
478
|
+
return {
|
|
479
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(list, null, 2) }],
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (uri.startsWith("chant://spell/") && uri.endsWith("/prompt")) {
|
|
484
|
+
const name = uri.replace("chant://spell/", "").replace("/prompt", "");
|
|
485
|
+
const { spells } = await discoverSpells();
|
|
486
|
+
const spell = spells.get(name);
|
|
487
|
+
if (!spell) throw new Error(`Spell "${name}" not found`);
|
|
488
|
+
const rt = getRuntime();
|
|
489
|
+
const gitRootResult = await rt.spawn(["git", "rev-parse", "--show-toplevel"]);
|
|
490
|
+
const gitRoot = gitRootResult.stdout.trim();
|
|
491
|
+
const prompt = await generatePrompt(spell.definition, { gitRoot });
|
|
492
|
+
return {
|
|
493
|
+
contents: [{ uri, mimeType: "text/markdown", text: prompt }],
|
|
494
|
+
};
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (uri.startsWith("chant://spell/")) {
|
|
498
|
+
const name = uri.replace("chant://spell/", "");
|
|
499
|
+
const { spells } = await discoverSpells();
|
|
500
|
+
const spell = spells.get(name);
|
|
501
|
+
if (!spell) throw new Error(`Spell "${name}" not found`);
|
|
502
|
+
return {
|
|
503
|
+
contents: [{
|
|
504
|
+
uri,
|
|
505
|
+
mimeType: "application/json",
|
|
506
|
+
text: JSON.stringify({
|
|
507
|
+
...spell.definition,
|
|
508
|
+
status: spell.status,
|
|
509
|
+
filePath: spell.filePath,
|
|
510
|
+
}, null, 2),
|
|
511
|
+
}],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// State resources: chant://state/{environment} and chant://state/{environment}/{lexicon}
|
|
516
|
+
if (uri.startsWith("chant://state/")) {
|
|
517
|
+
const parts = uri.replace("chant://state/", "").split("/");
|
|
518
|
+
const environment = parts[0];
|
|
519
|
+
const lexicon = parts[1];
|
|
520
|
+
|
|
521
|
+
if (lexicon) {
|
|
522
|
+
const content = await readSnapshot(environment, lexicon);
|
|
523
|
+
if (!content) throw new Error(`No snapshot found for ${environment}/${lexicon}`);
|
|
524
|
+
return {
|
|
525
|
+
contents: [{ uri, mimeType: "application/json", text: content }],
|
|
526
|
+
};
|
|
527
|
+
} else {
|
|
528
|
+
const snapshots = await readEnvironmentSnapshots(environment);
|
|
529
|
+
const result: Record<string, unknown> = {};
|
|
530
|
+
for (const [lex, content] of snapshots) {
|
|
531
|
+
result[lex] = JSON.parse(content);
|
|
532
|
+
}
|
|
533
|
+
return {
|
|
534
|
+
contents: [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }],
|
|
535
|
+
};
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
325
539
|
if (uri.startsWith("chant://examples/")) {
|
|
326
540
|
// Look up example in plugin resources
|
|
327
541
|
const name = uri.replace("chant://examples/", "");
|
|
@@ -58,7 +58,8 @@ export async function handleBuild(params: Record<string, unknown>): Promise<unkn
|
|
|
58
58
|
// Combine all lexicon outputs
|
|
59
59
|
const combined: Record<string, unknown> = {};
|
|
60
60
|
for (const [lexiconName, lexiconOutput] of result.outputs) {
|
|
61
|
-
|
|
61
|
+
const raw = lexiconOutput;
|
|
62
|
+
combined[lexiconName] = JSON.parse(typeof raw === "string" ? raw : raw.primary);
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
let output = JSON.stringify(combined, null, 2);
|
package/src/cli/plugins.ts
CHANGED
|
@@ -17,9 +17,9 @@ describe("formatStylish", () => {
|
|
|
17
17
|
}
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
-
test("returns
|
|
20
|
+
test("returns summary line for no diagnostics", () => {
|
|
21
21
|
const result = formatStylish([]);
|
|
22
|
-
expect(result).toBe("");
|
|
22
|
+
expect(result).toBe("\u2713 No problems found");
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
test("formats single diagnostic", () => {
|
package/src/composite.test.ts
CHANGED
|
@@ -116,7 +116,7 @@ describe("Composite", () => {
|
|
|
116
116
|
});
|
|
117
117
|
|
|
118
118
|
const instance = MyComp({});
|
|
119
|
-
const roleProps = instance.members.role
|
|
119
|
+
const roleProps = (instance.members.role as MockResource).props;
|
|
120
120
|
expect(roleProps.bucketArn).toBeInstanceOf(AttrRef);
|
|
121
121
|
// The AttrRef's parent should be the bucket instance
|
|
122
122
|
expect((roleProps.bucketArn as AttrRef).attribute).toBe("Arn");
|
package/src/config.ts
CHANGED
|
@@ -9,6 +9,7 @@ import type { LintConfig } from "./lint/config";
|
|
|
9
9
|
export const ChantConfigSchema = z.object({
|
|
10
10
|
runtime: z.enum(["bun", "node"]).optional(),
|
|
11
11
|
lexicons: z.array(z.string().min(1)).optional(),
|
|
12
|
+
environments: z.array(z.string().min(1)).optional(),
|
|
12
13
|
lint: z.record(z.string(), z.unknown()).optional(),
|
|
13
14
|
}).passthrough();
|
|
14
15
|
|
|
@@ -24,6 +25,9 @@ export interface ChantConfig {
|
|
|
24
25
|
/** Lexicon package names to load (e.g. ["aws"]) */
|
|
25
26
|
lexicons?: string[];
|
|
26
27
|
|
|
28
|
+
/** Environment names (e.g. ["staging", "prod"]) */
|
|
29
|
+
environments?: string[];
|
|
30
|
+
|
|
27
31
|
/** Lint configuration (rules, extends, overrides, plugins) */
|
|
28
32
|
lint?: LintConfig;
|
|
29
33
|
}
|
package/src/declarable.test.ts
CHANGED
|
@@ -12,7 +12,8 @@ describe("DECLARABLE_MARKER", () => {
|
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
test("uses Symbol.for for global registry", () => {
|
|
15
|
-
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
expect(DECLARABLE_MARKER).toBe(Symbol.for("chant.declarable") as any);
|
|
16
17
|
});
|
|
17
18
|
});
|
|
18
19
|
|
package/src/declarable.ts
CHANGED
|
@@ -9,7 +9,7 @@ export const DECLARABLE_MARKER = Symbol.for("chant.declarable");
|
|
|
9
9
|
export interface Declarable {
|
|
10
10
|
readonly lexicon: string;
|
|
11
11
|
readonly entityType: string;
|
|
12
|
-
readonly kind?: "resource" | "property";
|
|
12
|
+
readonly kind?: "resource" | "property" | "output";
|
|
13
13
|
readonly [DECLARABLE_MARKER]: true;
|
|
14
14
|
}
|
|
15
15
|
|
|
@@ -12,6 +12,7 @@ describe("buildDependencyGraph", () => {
|
|
|
12
12
|
|
|
13
13
|
test("returns graph with no dependencies for single entity", () => {
|
|
14
14
|
const entity: Declarable = {
|
|
15
|
+
lexicon: "test",
|
|
15
16
|
entityType: "test",
|
|
16
17
|
[DECLARABLE_MARKER]: true,
|
|
17
18
|
};
|
|
@@ -25,11 +26,13 @@ describe("buildDependencyGraph", () => {
|
|
|
25
26
|
|
|
26
27
|
test("returns graph with no dependencies for multiple unrelated entities", () => {
|
|
27
28
|
const entity1: Declarable = {
|
|
29
|
+
lexicon: "test",
|
|
28
30
|
entityType: "test",
|
|
29
31
|
[DECLARABLE_MARKER]: true,
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
const entity2: Declarable = {
|
|
35
|
+
lexicon: "test",
|
|
33
36
|
entityType: "test",
|
|
34
37
|
[DECLARABLE_MARKER]: true,
|
|
35
38
|
};
|
|
@@ -47,11 +50,13 @@ describe("buildDependencyGraph", () => {
|
|
|
47
50
|
|
|
48
51
|
test("detects dependency from AttrRef", () => {
|
|
49
52
|
const parent: Declarable = {
|
|
53
|
+
lexicon: "test",
|
|
50
54
|
entityType: "parent",
|
|
51
55
|
[DECLARABLE_MARKER]: true,
|
|
52
56
|
};
|
|
53
57
|
|
|
54
58
|
const child: Declarable & { ref: AttrRef } = {
|
|
59
|
+
lexicon: "test",
|
|
55
60
|
entityType: "child",
|
|
56
61
|
[DECLARABLE_MARKER]: true,
|
|
57
62
|
ref: new AttrRef(parent, "someAttr"),
|
|
@@ -71,11 +76,13 @@ describe("buildDependencyGraph", () => {
|
|
|
71
76
|
|
|
72
77
|
test("detects dependency from direct Declarable reference", () => {
|
|
73
78
|
const entity1: Declarable = {
|
|
79
|
+
lexicon: "test",
|
|
74
80
|
entityType: "type1",
|
|
75
81
|
[DECLARABLE_MARKER]: true,
|
|
76
82
|
};
|
|
77
83
|
|
|
78
84
|
const entity2: Declarable & { dependency: Declarable } = {
|
|
85
|
+
lexicon: "test",
|
|
79
86
|
entityType: "type2",
|
|
80
87
|
[DECLARABLE_MARKER]: true,
|
|
81
88
|
dependency: entity1,
|
|
@@ -95,16 +102,19 @@ describe("buildDependencyGraph", () => {
|
|
|
95
102
|
|
|
96
103
|
test("detects multiple dependencies from one entity", () => {
|
|
97
104
|
const entity1: Declarable = {
|
|
105
|
+
lexicon: "test",
|
|
98
106
|
entityType: "type1",
|
|
99
107
|
[DECLARABLE_MARKER]: true,
|
|
100
108
|
};
|
|
101
109
|
|
|
102
110
|
const entity2: Declarable = {
|
|
111
|
+
lexicon: "test",
|
|
103
112
|
entityType: "type2",
|
|
104
113
|
[DECLARABLE_MARKER]: true,
|
|
105
114
|
};
|
|
106
115
|
|
|
107
116
|
const entity3: Declarable & { dep1: Declarable; dep2: Declarable } = {
|
|
117
|
+
lexicon: "test",
|
|
108
118
|
entityType: "type3",
|
|
109
119
|
[DECLARABLE_MARKER]: true,
|
|
110
120
|
dep1: entity1,
|
|
@@ -126,11 +136,13 @@ describe("buildDependencyGraph", () => {
|
|
|
126
136
|
|
|
127
137
|
test("detects dependencies in nested objects", () => {
|
|
128
138
|
const entity1: Declarable = {
|
|
139
|
+
lexicon: "test",
|
|
129
140
|
entityType: "type1",
|
|
130
141
|
[DECLARABLE_MARKER]: true,
|
|
131
142
|
};
|
|
132
143
|
|
|
133
144
|
const entity2: Declarable & { nested: { deep: Declarable } } = {
|
|
145
|
+
lexicon: "test",
|
|
134
146
|
entityType: "type2",
|
|
135
147
|
[DECLARABLE_MARKER]: true,
|
|
136
148
|
nested: {
|
|
@@ -150,16 +162,19 @@ describe("buildDependencyGraph", () => {
|
|
|
150
162
|
|
|
151
163
|
test("detects dependencies in arrays", () => {
|
|
152
164
|
const entity1: Declarable = {
|
|
165
|
+
lexicon: "test",
|
|
153
166
|
entityType: "type1",
|
|
154
167
|
[DECLARABLE_MARKER]: true,
|
|
155
168
|
};
|
|
156
169
|
|
|
157
170
|
const entity2: Declarable = {
|
|
171
|
+
lexicon: "test",
|
|
158
172
|
entityType: "type2",
|
|
159
173
|
[DECLARABLE_MARKER]: true,
|
|
160
174
|
};
|
|
161
175
|
|
|
162
176
|
const entity3: Declarable & { deps: Declarable[] } = {
|
|
177
|
+
lexicon: "test",
|
|
163
178
|
entityType: "type3",
|
|
164
179
|
[DECLARABLE_MARKER]: true,
|
|
165
180
|
deps: [entity1, entity2],
|
|
@@ -179,16 +194,19 @@ describe("buildDependencyGraph", () => {
|
|
|
179
194
|
|
|
180
195
|
test("detects mixed AttrRef and Declarable dependencies", () => {
|
|
181
196
|
const entity1: Declarable = {
|
|
197
|
+
lexicon: "test",
|
|
182
198
|
entityType: "type1",
|
|
183
199
|
[DECLARABLE_MARKER]: true,
|
|
184
200
|
};
|
|
185
201
|
|
|
186
202
|
const entity2: Declarable = {
|
|
203
|
+
lexicon: "test",
|
|
187
204
|
entityType: "type2",
|
|
188
205
|
[DECLARABLE_MARKER]: true,
|
|
189
206
|
};
|
|
190
207
|
|
|
191
208
|
const entity3: Declarable & { ref: AttrRef; dep: Declarable } = {
|
|
209
|
+
lexicon: "test",
|
|
192
210
|
entityType: "type3",
|
|
193
211
|
[DECLARABLE_MARKER]: true,
|
|
194
212
|
ref: new AttrRef(entity1, "attr"),
|
|
@@ -209,17 +227,20 @@ describe("buildDependencyGraph", () => {
|
|
|
209
227
|
|
|
210
228
|
test("handles transitive dependencies correctly", () => {
|
|
211
229
|
const entity1: Declarable = {
|
|
230
|
+
lexicon: "test",
|
|
212
231
|
entityType: "type1",
|
|
213
232
|
[DECLARABLE_MARKER]: true,
|
|
214
233
|
};
|
|
215
234
|
|
|
216
235
|
const entity2: Declarable & { dep: Declarable } = {
|
|
236
|
+
lexicon: "test",
|
|
217
237
|
entityType: "type2",
|
|
218
238
|
[DECLARABLE_MARKER]: true,
|
|
219
239
|
dep: entity1,
|
|
220
240
|
};
|
|
221
241
|
|
|
222
242
|
const entity3: Declarable & { dep: Declarable } = {
|
|
243
|
+
lexicon: "test",
|
|
223
244
|
entityType: "type3",
|
|
224
245
|
[DECLARABLE_MARKER]: true,
|
|
225
246
|
dep: entity2,
|
|
@@ -241,16 +262,19 @@ describe("buildDependencyGraph", () => {
|
|
|
241
262
|
|
|
242
263
|
test("ignores non-entity declarables", () => {
|
|
243
264
|
const entity: Declarable = {
|
|
265
|
+
lexicon: "test",
|
|
244
266
|
entityType: "test",
|
|
245
267
|
[DECLARABLE_MARKER]: true,
|
|
246
268
|
};
|
|
247
269
|
|
|
248
270
|
const notInEntities: Declarable = {
|
|
271
|
+
lexicon: "test",
|
|
249
272
|
entityType: "external",
|
|
250
273
|
[DECLARABLE_MARKER]: true,
|
|
251
274
|
};
|
|
252
275
|
|
|
253
276
|
const entityWithExternal: Declarable & { dep: Declarable } = {
|
|
277
|
+
lexicon: "test",
|
|
254
278
|
entityType: "test",
|
|
255
279
|
[DECLARABLE_MARKER]: true,
|
|
256
280
|
dep: notInEntities,
|
|
@@ -267,11 +291,13 @@ describe("buildDependencyGraph", () => {
|
|
|
267
291
|
|
|
268
292
|
test("ignores AttrRef with parent not in entities", () => {
|
|
269
293
|
const externalParent: Declarable = {
|
|
294
|
+
lexicon: "test",
|
|
270
295
|
entityType: "external",
|
|
271
296
|
[DECLARABLE_MARKER]: true,
|
|
272
297
|
};
|
|
273
298
|
|
|
274
299
|
const entity: Declarable & { ref: AttrRef } = {
|
|
300
|
+
lexicon: "test",
|
|
275
301
|
entityType: "test",
|
|
276
302
|
[DECLARABLE_MARKER]: true,
|
|
277
303
|
ref: new AttrRef(externalParent, "attr"),
|
|
@@ -285,11 +311,13 @@ describe("buildDependencyGraph", () => {
|
|
|
285
311
|
|
|
286
312
|
test("handles circular references without infinite loop", () => {
|
|
287
313
|
const entity1: Declarable & { other?: Declarable } = {
|
|
314
|
+
lexicon: "test",
|
|
288
315
|
entityType: "type1",
|
|
289
316
|
[DECLARABLE_MARKER]: true,
|
|
290
317
|
};
|
|
291
318
|
|
|
292
319
|
const entity2: Declarable & { other: Declarable } = {
|
|
320
|
+
lexicon: "test",
|
|
293
321
|
entityType: "type2",
|
|
294
322
|
[DECLARABLE_MARKER]: true,
|
|
295
323
|
other: entity1,
|
|
@@ -309,6 +337,7 @@ describe("buildDependencyGraph", () => {
|
|
|
309
337
|
|
|
310
338
|
test("handles self-reference without infinite loop", () => {
|
|
311
339
|
const entity: Declarable & { self?: Declarable } = {
|
|
340
|
+
lexicon: "test",
|
|
312
341
|
entityType: "test",
|
|
313
342
|
[DECLARABLE_MARKER]: true,
|
|
314
343
|
};
|
|
@@ -329,6 +358,7 @@ describe("buildDependencyGraph", () => {
|
|
|
329
358
|
bool: boolean;
|
|
330
359
|
nul: null;
|
|
331
360
|
} = {
|
|
361
|
+
lexicon: "test",
|
|
332
362
|
entityType: "test",
|
|
333
363
|
[DECLARABLE_MARKER]: true,
|
|
334
364
|
str: "value",
|
|
@@ -348,6 +378,7 @@ describe("buildDependencyGraph", () => {
|
|
|
348
378
|
// parent is the resource itself (e.g. bucket.arn, bucket.bucketName).
|
|
349
379
|
// These are not real dependencies — they're just attribute accessors.
|
|
350
380
|
const resource: Declarable & { arn?: AttrRef; bucketName?: AttrRef } = {
|
|
381
|
+
lexicon: "test",
|
|
351
382
|
entityType: "AWS::S3::Bucket",
|
|
352
383
|
[DECLARABLE_MARKER]: true,
|
|
353
384
|
};
|
|
@@ -364,6 +395,7 @@ describe("buildDependencyGraph", () => {
|
|
|
364
395
|
// A resource has its own AttrRefs (self-pointing) AND a property that
|
|
365
396
|
// references a different entity. Only the cross-resource dep should appear.
|
|
366
397
|
const defaults: Declarable = {
|
|
398
|
+
lexicon: "test",
|
|
367
399
|
entityType: "AWS::S3::VersioningConfiguration",
|
|
368
400
|
[DECLARABLE_MARKER]: true,
|
|
369
401
|
};
|
|
@@ -372,6 +404,7 @@ describe("buildDependencyGraph", () => {
|
|
|
372
404
|
arn?: AttrRef;
|
|
373
405
|
versioningConfiguration?: Declarable;
|
|
374
406
|
} = {
|
|
407
|
+
lexicon: "test",
|
|
375
408
|
entityType: "AWS::S3::Bucket",
|
|
376
409
|
[DECLARABLE_MARKER]: true,
|
|
377
410
|
};
|
|
@@ -391,6 +424,7 @@ describe("buildDependencyGraph", () => {
|
|
|
391
424
|
|
|
392
425
|
test("ignores plain objects without markers", () => {
|
|
393
426
|
const entity: Declarable & { data: { key: string } } = {
|
|
427
|
+
lexicon: "test",
|
|
394
428
|
entityType: "test",
|
|
395
429
|
[DECLARABLE_MARKER]: true,
|
|
396
430
|
data: { key: "value" },
|
|
@@ -404,6 +438,7 @@ describe("buildDependencyGraph", () => {
|
|
|
404
438
|
|
|
405
439
|
test("handles AttrRef with garbage collected parent gracefully", () => {
|
|
406
440
|
const entity: Declarable & { ref: AttrRef } = {
|
|
441
|
+
lexicon: "test",
|
|
407
442
|
entityType: "test",
|
|
408
443
|
[DECLARABLE_MARKER]: true,
|
|
409
444
|
ref: new AttrRef({}, "attr"), // Using plain object that will be GC'd
|
|
@@ -418,6 +453,7 @@ describe("buildDependencyGraph", () => {
|
|
|
418
453
|
|
|
419
454
|
test("detects dependencies deeply nested in arrays and objects", () => {
|
|
420
455
|
const entity1: Declarable = {
|
|
456
|
+
lexicon: "test",
|
|
421
457
|
entityType: "type1",
|
|
422
458
|
[DECLARABLE_MARKER]: true,
|
|
423
459
|
};
|
|
@@ -425,6 +461,7 @@ describe("buildDependencyGraph", () => {
|
|
|
425
461
|
const entity2: Declarable & {
|
|
426
462
|
complex: { nested: { array: Array<{ item: Declarable }> } };
|
|
427
463
|
} = {
|
|
464
|
+
lexicon: "test",
|
|
428
465
|
entityType: "type2",
|
|
429
466
|
[DECLARABLE_MARKER]: true,
|
|
430
467
|
complex: {
|
|
@@ -446,17 +483,20 @@ describe("buildDependencyGraph", () => {
|
|
|
446
483
|
|
|
447
484
|
test("does not traverse into referenced declarables", () => {
|
|
448
485
|
const entity1: Declarable = {
|
|
486
|
+
lexicon: "test",
|
|
449
487
|
entityType: "type1",
|
|
450
488
|
[DECLARABLE_MARKER]: true,
|
|
451
489
|
};
|
|
452
490
|
|
|
453
491
|
const entity2: Declarable & { internal: { data: string } } = {
|
|
492
|
+
lexicon: "test",
|
|
454
493
|
entityType: "type2",
|
|
455
494
|
[DECLARABLE_MARKER]: true,
|
|
456
495
|
internal: { data: "should not traverse this" },
|
|
457
496
|
};
|
|
458
497
|
|
|
459
498
|
const entity3: Declarable & { dep: Declarable } = {
|
|
499
|
+
lexicon: "test",
|
|
460
500
|
entityType: "type3",
|
|
461
501
|
[DECLARABLE_MARKER]: true,
|
|
462
502
|
dep: entity2,
|