@intentius/chant 0.0.5 → 0.0.8

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 (38) hide show
  1. package/bin/chant +20 -0
  2. package/package.json +18 -17
  3. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
  4. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
  5. package/src/cli/commands/build.ts +1 -2
  6. package/src/cli/commands/import.ts +2 -2
  7. package/src/cli/commands/init-lexicon.test.ts +0 -3
  8. package/src/cli/commands/init-lexicon.ts +1 -79
  9. package/src/cli/commands/init.ts +14 -3
  10. package/src/cli/commands/list.ts +2 -2
  11. package/src/cli/commands/update.ts +5 -3
  12. package/src/cli/conflict-check.test.ts +0 -1
  13. package/src/cli/handlers/dev.ts +1 -9
  14. package/src/cli/main.ts +13 -3
  15. package/src/cli/mcp/server.test.ts +207 -4
  16. package/src/cli/mcp/server.ts +6 -0
  17. package/src/cli/mcp/tools/explain.ts +134 -0
  18. package/src/cli/mcp/tools/scaffold.ts +107 -0
  19. package/src/cli/mcp/tools/search.ts +98 -0
  20. package/src/codegen/generate-registry.test.ts +1 -1
  21. package/src/codegen/generate-registry.ts +2 -3
  22. package/src/codegen/generate-typescript.test.ts +6 -6
  23. package/src/codegen/generate-typescript.ts +2 -6
  24. package/src/codegen/generate.ts +1 -12
  25. package/src/codegen/typecheck.ts +6 -11
  26. package/src/config.ts +4 -0
  27. package/src/index.ts +1 -1
  28. package/src/lexicon-integrity.ts +5 -4
  29. package/src/lexicon.ts +2 -6
  30. package/src/lint/config.ts +8 -6
  31. package/src/runtime-adapter.ts +158 -0
  32. package/src/serializer-walker.test.ts +0 -9
  33. package/src/serializer-walker.ts +1 -3
  34. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
  35. package/src/codegen/case.test.ts +0 -30
  36. package/src/codegen/case.ts +0 -11
  37. package/src/codegen/rollback.test.ts +0 -92
  38. package/src/codegen/rollback.ts +0 -115
@@ -81,7 +81,7 @@ describe("McpServer", () => {
81
81
  // -----------------------------------------------------------------------
82
82
 
83
83
  describe("tools/list", () => {
84
- test("returns core tools (build, lint, import)", async () => {
84
+ test("returns core tools (build, lint, import, explain, scaffold, search)", async () => {
85
85
  const response = await server.handleRequest({
86
86
  jsonrpc: "2.0",
87
87
  id: 1,
@@ -94,6 +94,9 @@ describe("McpServer", () => {
94
94
  expect(toolNames).toContain("build");
95
95
  expect(toolNames).toContain("lint");
96
96
  expect(toolNames).toContain("import");
97
+ expect(toolNames).toContain("explain");
98
+ expect(toolNames).toContain("scaffold");
99
+ expect(toolNames).toContain("search");
97
100
  });
98
101
 
99
102
  test("each tool has name, description, and inputSchema", async () => {
@@ -148,6 +151,46 @@ describe("McpServer", () => {
148
151
  expect(props.source).toBeDefined();
149
152
  expect(props.output).toBeDefined();
150
153
  });
154
+
155
+ test("explain tool schema has path and format properties", async () => {
156
+ const response = await server.handleRequest({
157
+ jsonrpc: "2.0",
158
+ id: 1,
159
+ method: "tools/list",
160
+ });
161
+ const result = response.result as { tools: Array<{ name: string; inputSchema: Record<string, unknown> }> };
162
+ const tool = result.tools.find((t) => t.name === "explain")!;
163
+ const props = tool.inputSchema.properties as Record<string, unknown>;
164
+ expect(props.path).toBeDefined();
165
+ expect(props.format).toBeDefined();
166
+ });
167
+
168
+ test("scaffold tool schema has pattern and lexicon properties", async () => {
169
+ const response = await server.handleRequest({
170
+ jsonrpc: "2.0",
171
+ id: 1,
172
+ method: "tools/list",
173
+ });
174
+ const result = response.result as { tools: Array<{ name: string; inputSchema: Record<string, unknown> }> };
175
+ const tool = result.tools.find((t) => t.name === "scaffold")!;
176
+ const props = tool.inputSchema.properties as Record<string, unknown>;
177
+ expect(props.pattern).toBeDefined();
178
+ expect(props.lexicon).toBeDefined();
179
+ });
180
+
181
+ test("search tool schema has query, lexicon, and limit properties", async () => {
182
+ const response = await server.handleRequest({
183
+ jsonrpc: "2.0",
184
+ id: 1,
185
+ method: "tools/list",
186
+ });
187
+ const result = response.result as { tools: Array<{ name: string; inputSchema: Record<string, unknown> }> };
188
+ const tool = result.tools.find((t) => t.name === "search")!;
189
+ const props = tool.inputSchema.properties as Record<string, unknown>;
190
+ expect(props.query).toBeDefined();
191
+ expect(props.lexicon).toBeDefined();
192
+ expect(props.limit).toBeDefined();
193
+ });
151
194
  });
152
195
 
153
196
  describe("tools/call", () => {
@@ -194,6 +237,166 @@ describe("McpServer", () => {
194
237
  expect(result.isError).toBe(true);
195
238
  expect(result.content[0].text).toContain("Error:");
196
239
  });
240
+
241
+ test("calls explain tool on empty directory", async () => {
242
+ const response = await server.handleRequest({
243
+ jsonrpc: "2.0",
244
+ id: 1,
245
+ method: "tools/call",
246
+ params: { name: "explain", arguments: { path: testDir } },
247
+ });
248
+
249
+ expect(response.error).toBeUndefined();
250
+ const result = response.result as { content: Array<{ type: string; text: string }> };
251
+ expect(result.content[0].type).toBe("text");
252
+ expect(result.content[0].text).toContain("Project Summary");
253
+ });
254
+
255
+ test("calls explain tool with json format", async () => {
256
+ const response = await server.handleRequest({
257
+ jsonrpc: "2.0",
258
+ id: 1,
259
+ method: "tools/call",
260
+ params: { name: "explain", arguments: { path: testDir, format: "json" } },
261
+ });
262
+
263
+ expect(response.error).toBeUndefined();
264
+ const result = response.result as { content: Array<{ text: string }> };
265
+ const parsed = JSON.parse(result.content[0].text);
266
+ expect(parsed.totalEntities).toBe(0);
267
+ expect(parsed.sourceFiles).toBeDefined();
268
+ });
269
+
270
+ test("calls scaffold tool with generic fallback", async () => {
271
+ const response = await server.handleRequest({
272
+ jsonrpc: "2.0",
273
+ id: 1,
274
+ method: "tools/call",
275
+ params: { name: "scaffold", arguments: { pattern: "my-service" } },
276
+ });
277
+
278
+ expect(response.error).toBeUndefined();
279
+ const result = response.result as { content: Array<{ text: string }> };
280
+ const parsed = JSON.parse(result.content[0].text);
281
+ expect(parsed.pattern).toBe("my-service");
282
+ expect(parsed.files).toBeDefined();
283
+ expect(parsed.files.length).toBeGreaterThan(0);
284
+ });
285
+
286
+ test("calls search tool with no plugins", async () => {
287
+ const response = await server.handleRequest({
288
+ jsonrpc: "2.0",
289
+ id: 1,
290
+ method: "tools/call",
291
+ params: { name: "search", arguments: { query: "bucket" } },
292
+ });
293
+
294
+ expect(response.error).toBeUndefined();
295
+ const result = response.result as { content: Array<{ text: string }> };
296
+ const parsed = JSON.parse(result.content[0].text);
297
+ expect(parsed.query).toBe("bucket");
298
+ expect(parsed.total).toBe(0);
299
+ expect(parsed.results).toEqual([]);
300
+ });
301
+ });
302
+
303
+ // -----------------------------------------------------------------------
304
+ // Search tool with plugins
305
+ // -----------------------------------------------------------------------
306
+
307
+ describe("search with plugins", () => {
308
+ test("searches plugin resource catalogs", async () => {
309
+ const plugin = createMockPlugin({
310
+ name: "test-lex",
311
+ mcpResources: () => [
312
+ {
313
+ uri: "resource-catalog",
314
+ name: "Test Catalog",
315
+ description: "Test resource catalog",
316
+ mimeType: "application/json",
317
+ handler: async () => JSON.stringify([
318
+ { className: "Bucket", resourceType: "AWS::S3::Bucket", kind: "resource" },
319
+ { className: "Table", resourceType: "AWS::DynamoDB::Table", kind: "resource" },
320
+ ]),
321
+ },
322
+ ],
323
+ });
324
+
325
+ const s = new McpServer([plugin]);
326
+ const response = await s.handleRequest({
327
+ jsonrpc: "2.0",
328
+ id: 1,
329
+ method: "tools/call",
330
+ params: { name: "search", arguments: { query: "bucket" } },
331
+ });
332
+
333
+ const result = response.result as { content: Array<{ text: string }> };
334
+ const parsed = JSON.parse(result.content[0].text);
335
+ expect(parsed.total).toBe(1);
336
+ expect(parsed.results[0].className).toBe("Bucket");
337
+ expect(parsed.results[0].lexicon).toBe("test-lex");
338
+ });
339
+
340
+ test("search respects limit parameter", async () => {
341
+ const entries = Array.from({ length: 30 }, (_, i) => ({
342
+ className: `Type${i}`,
343
+ resourceType: `NS::Type${i}`,
344
+ kind: "resource",
345
+ }));
346
+ const plugin = createMockPlugin({
347
+ name: "big",
348
+ mcpResources: () => [
349
+ {
350
+ uri: "resource-catalog",
351
+ name: "Big Catalog",
352
+ description: "Big catalog",
353
+ mimeType: "application/json",
354
+ handler: async () => JSON.stringify(entries),
355
+ },
356
+ ],
357
+ });
358
+
359
+ const s = new McpServer([plugin]);
360
+ const response = await s.handleRequest({
361
+ jsonrpc: "2.0",
362
+ id: 1,
363
+ method: "tools/call",
364
+ params: { name: "search", arguments: { query: "type", limit: 5 } },
365
+ });
366
+
367
+ const parsed = JSON.parse((response.result as { content: Array<{ text: string }> }).content[0].text);
368
+ expect(parsed.total).toBe(30);
369
+ expect(parsed.results.length).toBe(5);
370
+ });
371
+ });
372
+
373
+ // -----------------------------------------------------------------------
374
+ // Scaffold tool with plugins
375
+ // -----------------------------------------------------------------------
376
+
377
+ describe("scaffold with plugins", () => {
378
+ test("matches plugin init templates", async () => {
379
+ const plugin = createMockPlugin({
380
+ name: "test-lex",
381
+ initTemplates: () => ({
382
+ "config.ts": "export const config = {};",
383
+ "data-bucket.ts": "export const dataBucket = {};",
384
+ }),
385
+ });
386
+
387
+ const s = new McpServer([plugin]);
388
+ const response = await s.handleRequest({
389
+ jsonrpc: "2.0",
390
+ id: 1,
391
+ method: "tools/call",
392
+ params: { name: "scaffold", arguments: { pattern: "bucket", lexicon: "test-lex" } },
393
+ });
394
+
395
+ const parsed = JSON.parse((response.result as { content: Array<{ text: string }> }).content[0].text);
396
+ expect(parsed.lexicon).toBe("test-lex");
397
+ expect(parsed.files.length).toBe(1);
398
+ expect(parsed.files[0].filename).toBe("data-bucket.ts");
399
+ });
197
400
  });
198
401
 
199
402
  // -----------------------------------------------------------------------
@@ -729,8 +932,8 @@ describe("McpServer", () => {
729
932
 
730
933
  const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 2, method: "tools/list" });
731
934
  const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
732
- expect(tools).toHaveLength(3);
733
- expect(tools.map((t) => t.name).sort()).toEqual(["build", "import", "lint"]);
935
+ expect(tools).toHaveLength(6);
936
+ expect(tools.map((t) => t.name).sort()).toEqual(["build", "explain", "import", "lint", "scaffold", "search"]);
734
937
 
735
938
  const resourcesRes = await s.handleRequest({ jsonrpc: "2.0", id: 3, method: "resources/list" });
736
939
  const resources = (resourcesRes.result as { resources: Array<{ uri: string }> }).resources;
@@ -741,7 +944,7 @@ describe("McpServer", () => {
741
944
  const s = new McpServer([]);
742
945
  const toolsRes = await s.handleRequest({ jsonrpc: "2.0", id: 1, method: "tools/list" });
743
946
  const tools = (toolsRes.result as { tools: Array<{ name: string }> }).tools;
744
- expect(tools).toHaveLength(3);
947
+ expect(tools).toHaveLength(6);
745
948
  });
746
949
  });
747
950
  });
@@ -3,6 +3,9 @@ import { resolve } from "node:path";
3
3
  import { buildTool, handleBuild } from "./tools/build";
4
4
  import { lintTool, handleLint } from "./tools/lint";
5
5
  import { importTool, handleImport } from "./tools/import";
6
+ import { explainTool, handleExplain } from "./tools/explain";
7
+ import { scaffoldTool, createScaffoldHandler } from "./tools/scaffold";
8
+ import { searchTool, createSearchHandler } from "./tools/search";
6
9
  import { getContext } from "./resources/context";
7
10
  import type { LexiconPlugin } from "../../lexicon";
8
11
  import type { McpToolContribution, McpResourceContribution } from "../../mcp/types";
@@ -70,6 +73,9 @@ export class McpServer {
70
73
  this.registerTool(buildTool, handleBuild);
71
74
  this.registerTool(lintTool, handleLint);
72
75
  this.registerTool(importTool, handleImport);
76
+ this.registerTool(explainTool, handleExplain);
77
+ this.registerTool(scaffoldTool, createScaffoldHandler(plugins ?? []));
78
+ this.registerTool(searchTool, createSearchHandler(plugins ?? []));
73
79
 
74
80
  // Register plugin contributions
75
81
  if (plugins) {
@@ -0,0 +1,134 @@
1
+ import { resolve } from "path";
2
+ import { discover } from "../../../discovery/index";
3
+
4
+ /**
5
+ * Explain tool definition for MCP
6
+ */
7
+ export const explainTool = {
8
+ name: "explain",
9
+ description: "Analyze a chant project directory and return a structured summary of all discovered entities",
10
+ inputSchema: {
11
+ type: "object" as const,
12
+ properties: {
13
+ path: {
14
+ type: "string",
15
+ description: "Path to the infrastructure directory to analyze",
16
+ },
17
+ format: {
18
+ type: "string",
19
+ enum: ["markdown", "json"],
20
+ description: "Output format (default: markdown)",
21
+ },
22
+ },
23
+ required: ["path"],
24
+ },
25
+ };
26
+
27
+ /**
28
+ * Handle explain tool invocation
29
+ */
30
+ export async function handleExplain(params: Record<string, unknown>): Promise<unknown> {
31
+ const path = params.path as string;
32
+ const format = (params.format as "markdown" | "json") ?? "markdown";
33
+
34
+ const infraPath = resolve(path);
35
+ const result = await discover(infraPath);
36
+
37
+ // Group entities by lexicon and kind
38
+ const byLexicon = new Map<string, { resources: string[]; properties: string[] }>();
39
+
40
+ for (const [name, entity] of result.entities) {
41
+ const lexicon = entity.lexicon ?? "unknown";
42
+ if (!byLexicon.has(lexicon)) {
43
+ byLexicon.set(lexicon, { resources: [], properties: [] });
44
+ }
45
+ const group = byLexicon.get(lexicon)!;
46
+ if (entity.kind === "property") {
47
+ group.properties.push(name);
48
+ } else {
49
+ group.resources.push(name);
50
+ }
51
+ }
52
+
53
+ // Collect dependency info
54
+ const crossResourceDeps: Array<{ from: string; to: string }> = [];
55
+ for (const [from, deps] of result.dependencies) {
56
+ for (const to of deps) {
57
+ crossResourceDeps.push({ from, to });
58
+ }
59
+ }
60
+
61
+ const summary = {
62
+ sourceFiles: result.sourceFiles,
63
+ totalEntities: result.entities.size,
64
+ lexicons: Object.fromEntries(
65
+ Array.from(byLexicon.entries()).map(([lexicon, group]) => [
66
+ lexicon,
67
+ {
68
+ resourceCount: group.resources.length,
69
+ propertyCount: group.properties.length,
70
+ resources: group.resources,
71
+ properties: group.properties,
72
+ },
73
+ ]),
74
+ ),
75
+ dependencies: crossResourceDeps,
76
+ errors: result.errors.map((e) => e.message),
77
+ };
78
+
79
+ if (format === "json") {
80
+ return summary;
81
+ }
82
+
83
+ // Markdown format
84
+ const lines: string[] = [];
85
+ lines.push(`# Project Summary`);
86
+ lines.push("");
87
+ lines.push(`- **Source files:** ${result.sourceFiles.length}`);
88
+ lines.push(`- **Total entities:** ${result.entities.size}`);
89
+ lines.push("");
90
+
91
+ for (const [lexicon, group] of byLexicon) {
92
+ lines.push(`## Lexicon: ${lexicon}`);
93
+ lines.push("");
94
+ lines.push(`- Resources: ${group.resources.length}`);
95
+ lines.push(`- Properties: ${group.properties.length}`);
96
+ lines.push("");
97
+
98
+ if (group.resources.length > 0) {
99
+ lines.push("### Resources");
100
+ for (const name of group.resources) {
101
+ lines.push(`- \`${name}\``);
102
+ }
103
+ lines.push("");
104
+ }
105
+
106
+ if (group.properties.length > 0) {
107
+ lines.push("### Properties");
108
+ for (const name of group.properties) {
109
+ lines.push(`- \`${name}\``);
110
+ }
111
+ lines.push("");
112
+ }
113
+ }
114
+
115
+ if (crossResourceDeps.length > 0) {
116
+ lines.push("## Dependencies");
117
+ lines.push("");
118
+ for (const dep of crossResourceDeps) {
119
+ lines.push(`- \`${dep.from}\` → \`${dep.to}\``);
120
+ }
121
+ lines.push("");
122
+ }
123
+
124
+ if (result.errors.length > 0) {
125
+ lines.push("## Errors");
126
+ lines.push("");
127
+ for (const err of result.errors) {
128
+ lines.push(`- ${err.message}`);
129
+ }
130
+ lines.push("");
131
+ }
132
+
133
+ return lines.join("\n");
134
+ }
@@ -0,0 +1,107 @@
1
+ import type { LexiconPlugin } from "../../../lexicon";
2
+
3
+ /**
4
+ * Scaffold tool definition for MCP
5
+ */
6
+ export const scaffoldTool = {
7
+ name: "scaffold",
8
+ description: "Generate starter code for a common infrastructure pattern",
9
+ inputSchema: {
10
+ type: "object" as const,
11
+ properties: {
12
+ pattern: {
13
+ type: "string",
14
+ description: "Infrastructure pattern to scaffold (e.g. 's3-bucket', 'lambda', 'pipeline')",
15
+ },
16
+ lexicon: {
17
+ type: "string",
18
+ description: "Lexicon to use for scaffolding (e.g. 'aws', 'gitlab'). Auto-detected if omitted.",
19
+ },
20
+ },
21
+ required: ["pattern"],
22
+ },
23
+ };
24
+
25
+ /**
26
+ * Create a scaffold handler with access to loaded plugins
27
+ */
28
+ export function createScaffoldHandler(
29
+ plugins: LexiconPlugin[],
30
+ ): (params: Record<string, unknown>) => Promise<unknown> {
31
+ return async (params) => {
32
+ const pattern = params.pattern as string;
33
+ const lexiconName = params.lexicon as string | undefined;
34
+
35
+ // Try to find a matching plugin
36
+ const candidates = lexiconName
37
+ ? plugins.filter((p) => p.name === lexiconName)
38
+ : plugins;
39
+
40
+ // Search plugin init templates for a pattern match
41
+ for (const plugin of candidates) {
42
+ const templates = plugin.initTemplates?.();
43
+ if (!templates) continue;
44
+
45
+ // Match template filenames against the pattern (case-insensitive substring)
46
+ const lowerPattern = pattern.toLowerCase();
47
+ const matched: Array<{ filename: string; content: string }> = [];
48
+
49
+ for (const [filename, content] of Object.entries(templates)) {
50
+ if (filename.toLowerCase().includes(lowerPattern)) {
51
+ matched.push({ filename, content });
52
+ }
53
+ }
54
+
55
+ if (matched.length > 0) {
56
+ return {
57
+ lexicon: plugin.name,
58
+ pattern,
59
+ files: matched,
60
+ };
61
+ }
62
+ }
63
+
64
+ // Fall back to a generic skeleton
65
+ const configContent = `/**
66
+ * Shared configuration for ${pattern}
67
+ */
68
+
69
+ // TODO: Import resource types from your lexicon
70
+ // import { ... } from "@intentius/chant-lexicon-<name>";
71
+
72
+ export const config = {
73
+ // Add shared configuration here
74
+ };
75
+ `;
76
+
77
+ const resourceContent = `/**
78
+ * ${pattern} resource definition
79
+ */
80
+
81
+ // TODO: Import resource types from your lexicon
82
+ // import { ... } from "@intentius/chant-lexicon-<name>";
83
+ // import { config } from "./config";
84
+
85
+ // export const ${toCamelCase(pattern)} = new ResourceType({
86
+ // // Add properties here
87
+ // });
88
+ `;
89
+
90
+ return {
91
+ lexicon: lexiconName ?? null,
92
+ pattern,
93
+ files: [
94
+ { filename: "config.ts", content: configContent },
95
+ { filename: `${pattern}.ts`, content: resourceContent },
96
+ ],
97
+ note: "No lexicon-specific template found. Generic skeleton provided — fill in imports and resource types.",
98
+ };
99
+ };
100
+ }
101
+
102
+ function toCamelCase(s: string): string {
103
+ return s
104
+ .split(/[-_]/)
105
+ .map((part, i) => (i === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
106
+ .join("");
107
+ }
@@ -0,0 +1,98 @@
1
+ import type { LexiconPlugin } from "../../../lexicon";
2
+
3
+ /**
4
+ * Search tool definition for MCP
5
+ */
6
+ export const searchTool = {
7
+ name: "search",
8
+ description: "Search the resource catalog across loaded lexicons by keyword",
9
+ inputSchema: {
10
+ type: "object" as const,
11
+ properties: {
12
+ query: {
13
+ type: "string",
14
+ description: "Search query — matches against resource type, class name, and kind",
15
+ },
16
+ lexicon: {
17
+ type: "string",
18
+ description: "Filter results to a specific lexicon (e.g. 'aws', 'gitlab')",
19
+ },
20
+ limit: {
21
+ type: "number",
22
+ description: "Maximum number of results to return (default: 20)",
23
+ },
24
+ },
25
+ required: ["query"],
26
+ },
27
+ };
28
+
29
+ interface CatalogEntry {
30
+ className: string;
31
+ resourceType: string;
32
+ kind?: string;
33
+ }
34
+
35
+ /**
36
+ * Create a search handler with access to loaded plugins
37
+ */
38
+ export function createSearchHandler(
39
+ plugins: LexiconPlugin[],
40
+ ): (params: Record<string, unknown>) => Promise<unknown> {
41
+ return async (params) => {
42
+ const query = params.query as string;
43
+ const lexiconFilter = params.lexicon as string | undefined;
44
+ const limit = (params.limit as number) ?? 20;
45
+
46
+ const lowerQuery = query.toLowerCase();
47
+ const results: Array<CatalogEntry & { lexicon: string; score: number }> = [];
48
+
49
+ const candidates = lexiconFilter
50
+ ? plugins.filter((p) => p.name === lexiconFilter)
51
+ : plugins;
52
+
53
+ for (const plugin of candidates) {
54
+ const resources = plugin.mcpResources?.() ?? [];
55
+ const catalog = resources.find((r) => r.uri === "resource-catalog");
56
+ if (!catalog) continue;
57
+
58
+ let entries: CatalogEntry[];
59
+ try {
60
+ const raw = await catalog.handler();
61
+ entries = JSON.parse(raw);
62
+ } catch {
63
+ continue;
64
+ }
65
+
66
+ for (const entry of entries) {
67
+ const fields = [
68
+ entry.resourceType?.toLowerCase() ?? "",
69
+ entry.className?.toLowerCase() ?? "",
70
+ entry.kind?.toLowerCase() ?? "",
71
+ ];
72
+
73
+ const match = fields.some((f) => f.includes(lowerQuery));
74
+ if (!match) continue;
75
+
76
+ // Score: prefix match on resourceType or className ranks higher
77
+ const isPrefix = fields.some((f) => f.startsWith(lowerQuery));
78
+ const score = isPrefix ? 1 : 0;
79
+
80
+ results.push({ ...entry, lexicon: plugin.name, score });
81
+ }
82
+ }
83
+
84
+ // Sort: prefix matches first, then alphabetical by resourceType
85
+ results.sort((a, b) => {
86
+ if (a.score !== b.score) return b.score - a.score;
87
+ return (a.resourceType ?? "").localeCompare(b.resourceType ?? "");
88
+ });
89
+
90
+ const limited = results.slice(0, limit);
91
+
92
+ return {
93
+ query,
94
+ total: results.length,
95
+ results: limited.map(({ score: _score, ...entry }) => entry),
96
+ };
97
+ };
98
+ }
@@ -51,7 +51,7 @@ describe("buildRegistry", () => {
51
51
  const entries = buildRegistry(resources, naming, testConfig);
52
52
 
53
53
  expect(entries["Bucket"]).toBeDefined();
54
- expect(entries["Bucket"].attrs).toEqual({ arn: "Arn", domainName: "DomainName" });
54
+ expect(entries["Bucket"].attrs).toEqual({ Arn: "Arn", DomainName: "DomainName" });
55
55
  });
56
56
 
57
57
  test("omits attrs when empty", () => {
@@ -8,7 +8,6 @@
8
8
 
9
9
  import type { NamingStrategy } from "./naming";
10
10
  import { propertyTypeName, extractDefName } from "./naming";
11
- import { toCamelCase } from "./case";
12
11
  import { constraintsIsEmpty, type PropertyConstraints } from "./json-schema";
13
12
 
14
13
  export interface RegistryResource {
@@ -47,12 +46,12 @@ export function buildRegistry<E>(
47
46
  const tsName = naming.resolve(typeName);
48
47
  if (!tsName) continue;
49
48
 
50
- // Build attrs map: camelCase → raw name
49
+ // Build attrs map: name → raw name (identity mapping)
51
50
  let attrs: Record<string, string> | undefined;
52
51
  if (r.attributes.length > 0) {
53
52
  attrs = {};
54
53
  for (const a of r.attributes) {
55
- attrs[toCamelCase(a.name)] = a.name;
54
+ attrs[a.name] = a.name;
56
55
  }
57
56
  }
58
57
 
@@ -18,8 +18,8 @@ describe("writeResourceClass", () => {
18
18
  );
19
19
  const output = lines.join("\n");
20
20
  expect(output).toContain("export declare class Bucket {");
21
- expect(output).toContain("bucketName: string;");
22
- expect(output).toContain("readonly arn: string;");
21
+ expect(output).toContain("BucketName: string;");
22
+ expect(output).toContain("readonly Arn: string;");
23
23
  expect(output).toContain("}");
24
24
  });
25
25
 
@@ -34,7 +34,7 @@ describe("writeResourceClass", () => {
34
34
  remap,
35
35
  );
36
36
  const output = lines.join("\n");
37
- expect(output).toContain("readonly config: BucketConfig;");
37
+ expect(output).toContain("readonly Config: BucketConfig;");
38
38
  });
39
39
  });
40
40
 
@@ -48,7 +48,7 @@ describe("writePropertyClass", () => {
48
48
  );
49
49
  const output = lines.join("\n");
50
50
  expect(output).toContain("export declare class BucketConfig {");
51
- expect(output).toContain("enabled?: boolean;");
51
+ expect(output).toContain("Enabled?: boolean;");
52
52
  expect(output).toContain("}");
53
53
  });
54
54
  });
@@ -71,8 +71,8 @@ describe("writeConstructor", () => {
71
71
  undefined,
72
72
  );
73
73
  const output = lines.join("\n");
74
- const reqIdx = output.indexOf("required:");
75
- const optIdx = output.indexOf("optional?:");
74
+ const reqIdx = output.indexOf("Required:");
75
+ const optIdx = output.indexOf("Optional?:");
76
76
  expect(reqIdx).toBeLessThan(optIdx);
77
77
  });
78
78