@intentius/chant 0.0.18 → 0.0.24

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 (87) hide show
  1. package/bin/chant +4 -1
  2. package/package.json +20 -1
  3. package/src/build.test.ts +4 -2
  4. package/src/build.ts +3 -0
  5. package/src/builder.test.ts +3 -0
  6. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +0 -3
  7. package/src/cli/commands/build.ts +5 -12
  8. package/src/cli/commands/diff.test.ts +2 -1
  9. package/src/cli/commands/diff.ts +2 -1
  10. package/src/cli/commands/init-lexicon/templates/codegen.ts +188 -0
  11. package/src/cli/commands/init-lexicon/templates/docs.ts +81 -0
  12. package/src/cli/commands/init-lexicon/templates/examples.ts +35 -0
  13. package/src/cli/commands/init-lexicon/templates/lint.ts +30 -0
  14. package/src/cli/commands/init-lexicon/templates/lsp.ts +39 -0
  15. package/src/cli/commands/init-lexicon/templates/plugin.ts +110 -0
  16. package/src/cli/commands/init-lexicon/templates/project.ts +182 -0
  17. package/src/cli/commands/init-lexicon/templates/spec.ts +57 -0
  18. package/src/cli/commands/init-lexicon/templates/tests.ts +70 -0
  19. package/src/cli/commands/init-lexicon.test.ts +0 -9
  20. package/src/cli/commands/init-lexicon.ts +12 -868
  21. package/src/cli/commands/init.ts +2 -20
  22. package/src/cli/conflict-check.test.ts +43 -0
  23. package/src/cli/handlers/build.ts +3 -3
  24. package/src/cli/handlers/lint.ts +2 -2
  25. package/src/cli/handlers/spell.ts +396 -0
  26. package/src/cli/handlers/state.ts +230 -0
  27. package/src/cli/lsp/server.test.ts +4 -0
  28. package/src/cli/main.ts +37 -3
  29. package/src/cli/mcp/resource-handlers.ts +227 -0
  30. package/src/cli/mcp/server.test.ts +13 -9
  31. package/src/cli/mcp/server.ts +24 -199
  32. package/src/cli/mcp/state-tools.ts +138 -0
  33. package/src/cli/mcp/tools/build.ts +2 -1
  34. package/src/cli/mcp/types.ts +45 -0
  35. package/src/cli/plugins.ts +1 -1
  36. package/src/cli/reporters/stylish.test.ts +2 -2
  37. package/src/cli/reporters/stylish.ts +1 -1
  38. package/src/codegen/docs-file-markers.ts +69 -0
  39. package/src/codegen/docs-rule-scanning.ts +159 -0
  40. package/src/codegen/docs-sections.ts +159 -0
  41. package/src/codegen/docs-sidebar.ts +56 -0
  42. package/src/codegen/docs-types.ts +79 -0
  43. package/src/codegen/docs.ts +9 -495
  44. package/src/composite.test.ts +76 -1
  45. package/src/composite.ts +37 -0
  46. package/src/config.ts +4 -0
  47. package/src/declarable.test.ts +2 -1
  48. package/src/declarable.ts +1 -1
  49. package/src/discovery/collect.test.ts +34 -0
  50. package/src/discovery/collect.ts +12 -0
  51. package/src/discovery/graph.test.ts +40 -0
  52. package/src/discovery/import.test.ts +5 -5
  53. package/src/discovery/resolve.test.ts +20 -0
  54. package/src/discovery/resolve.ts +2 -2
  55. package/src/index.ts +2 -0
  56. package/src/lexicon-plugin-helpers.ts +130 -0
  57. package/src/lexicon.ts +24 -0
  58. package/src/lint/rule-options.test.ts +3 -3
  59. package/src/lint/rule-registry.test.ts +1 -1
  60. package/src/lint/rules/composite-scope.ts +1 -1
  61. package/src/serializer-walker.ts +2 -1
  62. package/src/spell/discovery.ts +183 -0
  63. package/src/spell/index.ts +3 -0
  64. package/src/spell/prompt.ts +133 -0
  65. package/src/spell/types.ts +89 -0
  66. package/src/state/digest.ts +88 -0
  67. package/src/state/git.ts +317 -0
  68. package/src/state/index.ts +4 -0
  69. package/src/state/snapshot.ts +179 -0
  70. package/src/state/types.ts +59 -0
  71. package/src/toml-emit.ts +182 -0
  72. package/src/toml-parse.ts +370 -0
  73. package/src/toml-utils.ts +60 -0
  74. package/src/toml.ts +5 -602
  75. package/src/types.ts +2 -1
  76. package/src/utils.test.ts +16 -3
  77. package/src/utils.ts +31 -1
  78. package/src/validation.test.ts +11 -0
  79. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/getting-started.mdx +0 -6
  80. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/lint-rules.mdx +0 -6
  81. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/serialization.mdx +0 -6
  82. package/src/cli/commands/__fixtures__/init-lexicon-output/src/actions/.gitkeep +0 -0
  83. package/src/cli/commands/__fixtures__/init-lexicon-output/src/composites/.gitkeep +0 -0
  84. package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +0 -11
  85. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +0 -10
  86. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +0 -10
  87. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/post-synth/.gitkeep +0 -0
@@ -54,6 +54,40 @@ describe("collectEntities", () => {
54
54
  expect(result.get("entity3")).toBe(entity3);
55
55
  });
56
56
 
57
+ test("collects arrays of declarable entities with indexed names", () => {
58
+ const e0 = createMockEntity("type");
59
+ const e1 = createMockEntity("type");
60
+ const e2 = createMockEntity("type");
61
+
62
+ const modules = [
63
+ {
64
+ file: "test.ts",
65
+ exports: { myResources: [e0, e1, e2] },
66
+ },
67
+ ];
68
+
69
+ const result = collectEntities(modules);
70
+ expect(result.size).toBe(3);
71
+ expect(result.get("myResources_0")).toBe(e0);
72
+ expect(result.get("myResources_1")).toBe(e1);
73
+ expect(result.get("myResources_2")).toBe(e2);
74
+ });
75
+
76
+ test("ignores non-declarable items within arrays", () => {
77
+ const e0 = createMockEntity("type");
78
+
79
+ const modules = [
80
+ {
81
+ file: "test.ts",
82
+ exports: { mixed: [e0, "string", 42, null] },
83
+ },
84
+ ];
85
+
86
+ const result = collectEntities(modules);
87
+ expect(result.size).toBe(1);
88
+ expect(result.get("mixed_0")).toBe(e0);
89
+ });
90
+
57
91
  test("ignores non-declarable exports", () => {
58
92
  const entity = createMockEntity("test");
59
93
 
@@ -34,6 +34,18 @@ export function collectEntities(
34
34
  } else {
35
35
  entities.set(name, value);
36
36
  }
37
+ } else if (Array.isArray(value)) {
38
+ // Arrays of Declarables — each element gets an indexed name: exportName_0, exportName_1, ...
39
+ for (let i = 0; i < value.length; i++) {
40
+ const item = value[i];
41
+ if (isDeclarable(item)) {
42
+ const indexedName = `${name}_${i}`;
43
+ if (entities.has(indexedName) && entities.get(indexedName) !== item) {
44
+ throw new DiscoveryError(file, `Duplicate entity name "${indexedName}"`, "resolution");
45
+ }
46
+ entities.set(indexedName, item);
47
+ }
48
+ }
37
49
  } else if (isCompositeInstance(value)) {
38
50
  const expanded = expandComposite(name, value);
39
51
  for (const [expandedName, entity] of expanded) {
@@ -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,
@@ -28,8 +28,8 @@ describe("importModule", () => {
28
28
 
29
29
  const module = await importModule(filePath);
30
30
  expect(module.default).toBeDefined();
31
- expect(module.default.name).toBe("test");
32
- expect(module.default.value).toBe(123);
31
+ expect((module.default as any).name).toBe("test");
32
+ expect((module.default as any).value).toBe(123);
33
33
  });
34
34
  });
35
35
 
@@ -125,7 +125,7 @@ describe("importModule", () => {
125
125
 
126
126
  const module = await importModule(filePath);
127
127
  expect(module.MyClass).toBeDefined();
128
- const instance = new module.MyClass(100);
128
+ const instance = new (module.MyClass as any)(100);
129
129
  expect(instance.getValue()).toBe(100);
130
130
  });
131
131
  });
@@ -142,8 +142,8 @@ describe("importModule", () => {
142
142
  const module = await importModule(filePath);
143
143
  expect(module.add).toBeInstanceOf(Function);
144
144
  expect(module.multiply).toBeInstanceOf(Function);
145
- expect(module.add(2, 3)).toBe(5);
146
- expect(module.multiply(4, 5)).toBe(20);
145
+ expect((module.add as Function)(2, 3)).toBe(5);
146
+ expect((module.multiply as Function)(4, 5)).toBe(20);
147
147
  });
148
148
  });
149
149
 
@@ -8,11 +8,13 @@ import { LOGICAL_NAME_SYMBOL, getLogicalName } from "../utils";
8
8
  describe("resolveAttrRefs", () => {
9
9
  test("sets logical names on all entities", () => {
10
10
  const entity1: Declarable = {
11
+ lexicon: "test",
11
12
  entityType: "Test1",
12
13
  [DECLARABLE_MARKER]: true,
13
14
  };
14
15
 
15
16
  const entity2: Declarable = {
17
+ lexicon: "test",
16
18
  entityType: "Test2",
17
19
  [DECLARABLE_MARKER]: true,
18
20
  };
@@ -30,11 +32,13 @@ describe("resolveAttrRefs", () => {
30
32
 
31
33
  test("resolves AttrRef with parent in entities collection", () => {
32
34
  const parent: Declarable = {
35
+ lexicon: "test",
33
36
  entityType: "Parent",
34
37
  [DECLARABLE_MARKER]: true,
35
38
  };
36
39
 
37
40
  const child: Declarable & { ref: AttrRef } = {
41
+ lexicon: "test",
38
42
  entityType: "Child",
39
43
  [DECLARABLE_MARKER]: true,
40
44
  ref: new AttrRef(parent, "Arn"),
@@ -55,11 +59,13 @@ describe("resolveAttrRefs", () => {
55
59
 
56
60
  test("resolves multiple AttrRefs on same entity", () => {
57
61
  const parent: Declarable = {
62
+ lexicon: "test",
58
63
  entityType: "Parent",
59
64
  [DECLARABLE_MARKER]: true,
60
65
  };
61
66
 
62
67
  const child: Declarable & { arn: AttrRef; name: AttrRef } = {
68
+ lexicon: "test",
63
69
  entityType: "Child",
64
70
  [DECLARABLE_MARKER]: true,
65
71
  arn: new AttrRef(parent, "Arn"),
@@ -83,16 +89,19 @@ describe("resolveAttrRefs", () => {
83
89
 
84
90
  test("resolves AttrRefs with different parents", () => {
85
91
  const parent1: Declarable = {
92
+ lexicon: "test",
86
93
  entityType: "Parent1",
87
94
  [DECLARABLE_MARKER]: true,
88
95
  };
89
96
 
90
97
  const parent2: Declarable = {
98
+ lexicon: "test",
91
99
  entityType: "Parent2",
92
100
  [DECLARABLE_MARKER]: true,
93
101
  };
94
102
 
95
103
  const child: Declarable & { ref1: AttrRef; ref2: AttrRef } = {
104
+ lexicon: "test",
96
105
  entityType: "Child",
97
106
  [DECLARABLE_MARKER]: true,
98
107
  ref1: new AttrRef(parent1, "Arn"),
@@ -119,6 +128,7 @@ describe("resolveAttrRefs", () => {
119
128
  const parent = {}; // Not a Declarable, not in entities
120
129
 
121
130
  const child: Declarable & { ref: AttrRef } = {
131
+ lexicon: "test",
122
132
  entityType: "Child",
123
133
  [DECLARABLE_MARKER]: true,
124
134
  ref: new AttrRef(parent, "Arn"),
@@ -133,11 +143,13 @@ describe("resolveAttrRefs", () => {
133
143
 
134
144
  test("handles entities with no AttrRefs", () => {
135
145
  const entity1: Declarable = {
146
+ lexicon: "test",
136
147
  entityType: "Test1",
137
148
  [DECLARABLE_MARKER]: true,
138
149
  };
139
150
 
140
151
  const entity2: Declarable & { prop: string } = {
152
+ lexicon: "test",
141
153
  entityType: "Test2",
142
154
  [DECLARABLE_MARKER]: true,
143
155
  prop: "value",
@@ -162,17 +174,20 @@ describe("resolveAttrRefs", () => {
162
174
 
163
175
  test("resolves chain of entities with AttrRefs", () => {
164
176
  const root: Declarable = {
177
+ lexicon: "test",
165
178
  entityType: "Root",
166
179
  [DECLARABLE_MARKER]: true,
167
180
  };
168
181
 
169
182
  const middle: Declarable & { rootRef: AttrRef } = {
183
+ lexicon: "test",
170
184
  entityType: "Middle",
171
185
  [DECLARABLE_MARKER]: true,
172
186
  rootRef: new AttrRef(root, "Id"),
173
187
  };
174
188
 
175
189
  const leaf: Declarable & { middleRef: AttrRef } = {
190
+ lexicon: "test",
176
191
  entityType: "Leaf",
177
192
  [DECLARABLE_MARKER]: true,
178
193
  middleRef: new AttrRef(middle, "Name"),
@@ -196,6 +211,7 @@ describe("resolveAttrRefs", () => {
196
211
 
197
212
  test("handles entity referencing itself", () => {
198
213
  const entity: Declarable & { selfRef: AttrRef } = {
214
+ lexicon: "test",
199
215
  entityType: "SelfReferencing",
200
216
  [DECLARABLE_MARKER]: true,
201
217
  selfRef: null as unknown as AttrRef, // Will be set below
@@ -214,6 +230,7 @@ describe("resolveAttrRefs", () => {
214
230
 
215
231
  test("preserves logical name symbol on entities", () => {
216
232
  const entity: Declarable = {
233
+ lexicon: "test",
217
234
  entityType: "Test",
218
235
  [DECLARABLE_MARKER]: true,
219
236
  };
@@ -228,6 +245,7 @@ describe("resolveAttrRefs", () => {
228
245
 
229
246
  test("uses export name as logical name", () => {
230
247
  const entity: Declarable = {
248
+ lexicon: "test",
231
249
  entityType: "Test",
232
250
  [DECLARABLE_MARKER]: true,
233
251
  };
@@ -243,11 +261,13 @@ describe("resolveAttrRefs", () => {
243
261
 
244
262
  test("handles complex attribute names in AttrRef", () => {
245
263
  const parent: Declarable = {
264
+ lexicon: "test",
246
265
  entityType: "Parent",
247
266
  [DECLARABLE_MARKER]: true,
248
267
  };
249
268
 
250
269
  const child: Declarable & { ref: AttrRef } = {
270
+ lexicon: "test",
251
271
  entityType: "Child",
252
272
  [DECLARABLE_MARKER]: true,
253
273
  ref: new AttrRef(parent, "Outputs.WebsiteURL"),
@@ -1,6 +1,6 @@
1
1
  import type { Declarable } from "../declarable";
2
2
  import { AttrRef } from "../attrref";
3
- import { LOGICAL_NAME_SYMBOL, getAttributes } from "../utils";
3
+ import { LOGICAL_NAME_SYMBOL, getAttributes, isAttrRefLike } from "../utils";
4
4
 
5
5
  /**
6
6
  * Resolves all AttrRef instances in a collection of entities
@@ -22,7 +22,7 @@ export function resolveAttrRefs(entities: Map<string, Declarable>): void {
22
22
  for (const attrName of attributes) {
23
23
  const attrRef = (entity as unknown as Record<string, unknown>)[attrName];
24
24
 
25
- if (attrRef instanceof AttrRef) {
25
+ if (isAttrRefLike(attrRef)) {
26
26
  const parent = attrRef.parent.deref();
27
27
 
28
28
  if (!parent) {
package/src/index.ts CHANGED
@@ -59,3 +59,5 @@ export * from "./child-project";
59
59
  export * from "./lsp/types";
60
60
  export * from "./lsp/lexicon-providers";
61
61
  export * from "./mcp/types";
62
+ export * from "./state/index";
63
+ export * from "./spell/index";
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Shared helpers for lexicon plugin implementations.
3
+ *
4
+ * Eliminates boilerplate across the 8 lexicon plugins by providing
5
+ * factory functions for common plugin methods: skills loading,
6
+ * MCP diff tool, and MCP catalog resource.
7
+ */
8
+
9
+ import { readFileSync } from "fs";
10
+ import { join, dirname } from "path";
11
+ import { fileURLToPath } from "url";
12
+ import type { SkillDefinition } from "./lexicon";
13
+ import type { McpToolContribution, McpResourceContribution } from "./mcp/types";
14
+ import type { Serializer } from "./serializer";
15
+
16
+ // ── Skills Loader ─────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Metadata for a skill file on disk. Spread into the resulting SkillDefinition
20
+ * after the file content is read.
21
+ */
22
+ export type SkillFileSpec = Omit<SkillDefinition, "content"> & {
23
+ /** Filename relative to the skills directory (e.g. "chant-aws.md") */
24
+ file: string;
25
+ };
26
+
27
+ /**
28
+ * Create a skills loader that reads .md files from a lexicon's skills directory.
29
+ *
30
+ * Usage in a plugin:
31
+ * ```ts
32
+ * import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
33
+ *
34
+ * const loadSkills = createSkillsLoader(import.meta.url, [
35
+ * { file: "chant-aws.md", name: "chant-aws", description: "..." },
36
+ * ]);
37
+ *
38
+ * // In plugin:
39
+ * skills() { return loadSkills(); }
40
+ * ```
41
+ */
42
+ export function createSkillsLoader(
43
+ importMetaUrl: string,
44
+ specs: SkillFileSpec[],
45
+ ): () => SkillDefinition[] {
46
+ return () => {
47
+ const skillsDir = join(dirname(fileURLToPath(importMetaUrl)), "skills");
48
+ return specs.map(({ file, ...meta }) => {
49
+ try {
50
+ const content = readFileSync(join(skillsDir, file), "utf-8");
51
+ return { ...meta, content };
52
+ } catch {
53
+ return { ...meta, content: "" };
54
+ }
55
+ });
56
+ };
57
+ }
58
+
59
+ // ── MCP Diff Tool ─────────────────────────────────────────────────
60
+
61
+ /**
62
+ * Create an MCP diff tool contribution for a lexicon.
63
+ *
64
+ * All lexicons (except Azure) expose an identical "diff" tool that compares
65
+ * current build output against previous output using the lexicon's serializer.
66
+ */
67
+ export function createDiffTool(
68
+ serializer: Serializer,
69
+ description: string,
70
+ ): McpToolContribution {
71
+ return {
72
+ name: "diff",
73
+ description,
74
+ inputSchema: {
75
+ type: "object" as const,
76
+ properties: {
77
+ path: {
78
+ type: "string",
79
+ description: "Path to the infrastructure project directory",
80
+ },
81
+ },
82
+ },
83
+ async handler(params: Record<string, unknown>): Promise<unknown> {
84
+ const { diffCommand } = await import("./cli/commands/diff");
85
+ const result = await diffCommand({
86
+ path: (params.path as string) ?? ".",
87
+ serializers: [serializer],
88
+ });
89
+ return result;
90
+ },
91
+ };
92
+ }
93
+
94
+ // ── MCP Catalog Resource ──────────────────────────────────────────
95
+
96
+ /**
97
+ * Create an MCP resource that serves the lexicon's meta.json as a catalog.
98
+ *
99
+ * Most lexicons expose a "resource-catalog" resource with identical structure.
100
+ *
101
+ * @param importMetaUrl — The plugin's import.meta.url (used to locate generated JSON)
102
+ * @param name — Display name (e.g. "AWS Resource Catalog")
103
+ * @param description — Resource description
104
+ * @param lexiconJsonFile — Filename of the generated lexicon JSON (e.g. "lexicon-aws.json")
105
+ */
106
+ export function createCatalogResource(
107
+ importMetaUrl: string,
108
+ name: string,
109
+ description: string,
110
+ lexiconJsonFile: string,
111
+ ): McpResourceContribution {
112
+ return {
113
+ uri: "resource-catalog",
114
+ name,
115
+ description,
116
+ mimeType: "application/json",
117
+ async handler(): Promise<string> {
118
+ const dir = dirname(fileURLToPath(importMetaUrl));
119
+ const lexicon = JSON.parse(
120
+ readFileSync(join(dir, "generated", lexiconJsonFile), "utf-8"),
121
+ ) as Record<string, { resourceType: string; kind: string }>;
122
+ const entries = Object.entries(lexicon).map(([className, entry]) => ({
123
+ className,
124
+ resourceType: entry.resourceType,
125
+ kind: entry.kind,
126
+ }));
127
+ return JSON.stringify(entries);
128
+ },
129
+ };
130
+ }
package/src/lexicon.ts CHANGED
@@ -193,6 +193,30 @@ export interface LexiconPlugin {
193
193
 
194
194
  /** Return MCP resource contributions */
195
195
  mcpResources?(): McpResourceContribution[];
196
+
197
+ // State
198
+ /** Query deployed resources and return API metadata. Opt-in. */
199
+ describeResources?(options: {
200
+ environment: string;
201
+ buildOutput: string;
202
+ entityNames: string[];
203
+ }): Promise<Record<string, ResourceMetadata>>;
204
+ }
205
+
206
+ /**
207
+ * Metadata about a deployed resource, returned by describeResources.
208
+ */
209
+ export interface ResourceMetadata {
210
+ /** Entity type (e.g. AWS::S3::Bucket, K8s::Apps::Deployment) */
211
+ type: string;
212
+ /** Provider-assigned physical ID (ARN, resource ID, pod name) */
213
+ physicalId?: string;
214
+ /** Provider-specific status string */
215
+ status: string;
216
+ /** ISO timestamp of last update */
217
+ lastUpdated?: string;
218
+ /** Cloud-assigned output properties */
219
+ attributes?: Record<string, unknown>;
196
220
  }
197
221
 
198
222
  /**