@intentius/chant 0.0.5 → 0.0.9

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 (91) hide show
  1. package/bin/chant +20 -0
  2. package/package.json +18 -17
  3. package/src/bench.test.ts +1 -1
  4. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +0 -25
  5. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +0 -25
  6. package/src/cli/commands/build.ts +1 -2
  7. package/src/cli/commands/doctor.ts +8 -3
  8. package/src/cli/commands/import.ts +2 -2
  9. package/src/cli/commands/init-lexicon.test.ts +0 -3
  10. package/src/cli/commands/init-lexicon.ts +1 -79
  11. package/src/cli/commands/init.test.ts +44 -4
  12. package/src/cli/commands/init.ts +69 -26
  13. package/src/cli/commands/lint.ts +27 -13
  14. package/src/cli/commands/list.ts +2 -2
  15. package/src/cli/commands/update.ts +5 -3
  16. package/src/cli/conflict-check.test.ts +0 -1
  17. package/src/cli/handlers/dev.ts +1 -9
  18. package/src/cli/handlers/init.ts +1 -0
  19. package/src/cli/lsp/server.ts +1 -1
  20. package/src/cli/main.ts +17 -3
  21. package/src/cli/mcp/server.test.ts +233 -4
  22. package/src/cli/mcp/server.ts +6 -0
  23. package/src/cli/mcp/tools/explain.ts +134 -0
  24. package/src/cli/mcp/tools/scaffold.ts +125 -0
  25. package/src/cli/mcp/tools/search.ts +98 -0
  26. package/src/cli/registry.ts +1 -0
  27. package/src/cli/reporters/stylish.test.ts +212 -1
  28. package/src/cli/reporters/stylish.ts +133 -36
  29. package/src/codegen/docs-rules.test.ts +112 -0
  30. package/src/codegen/docs-rules.ts +129 -0
  31. package/src/codegen/docs.ts +3 -1
  32. package/src/codegen/generate-registry.test.ts +1 -1
  33. package/src/codegen/generate-registry.ts +2 -3
  34. package/src/codegen/generate-typescript.test.ts +70 -6
  35. package/src/codegen/generate-typescript.ts +15 -9
  36. package/src/codegen/generate.ts +1 -12
  37. package/src/codegen/package.ts +1 -1
  38. package/src/codegen/typecheck.ts +6 -11
  39. package/src/composite.test.ts +83 -16
  40. package/src/composite.ts +7 -5
  41. package/src/config.ts +4 -0
  42. package/src/detectLexicon.test.ts +2 -2
  43. package/src/discovery/collect.test.ts +2 -2
  44. package/src/discovery/collect.ts +1 -1
  45. package/src/index.ts +2 -1
  46. package/src/lexicon-integrity.ts +5 -4
  47. package/src/lexicon-schema.ts +8 -0
  48. package/src/lexicon.ts +15 -7
  49. package/src/lint/config.ts +8 -6
  50. package/src/lint/declarative.ts +6 -0
  51. package/src/lint/engine.test.ts +287 -11
  52. package/src/lint/engine.ts +101 -23
  53. package/src/lint/rule-registry.test.ts +112 -0
  54. package/src/lint/rule-registry.ts +118 -0
  55. package/src/lint/rule.ts +8 -0
  56. package/src/lint/rules/cor017-composite-name-match.ts +2 -1
  57. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +4 -3
  58. package/src/lint/rules/declarable-naming-convention.ts +1 -0
  59. package/src/lint/rules/evl001-non-literal-expression.ts +1 -0
  60. package/src/lint/rules/evl002-control-flow-resource.ts +1 -0
  61. package/src/lint/rules/evl003-dynamic-property-access.ts +1 -0
  62. package/src/lint/rules/evl004-spread-non-const.ts +1 -0
  63. package/src/lint/rules/evl005-resource-block-body.ts +1 -0
  64. package/src/lint/rules/evl007-invalid-siblings.ts +1 -0
  65. package/src/lint/rules/evl009-composite-no-constant.ts +1 -0
  66. package/src/lint/rules/evl010-composite-no-transform.ts +1 -0
  67. package/src/lint/rules/export-required.ts +1 -0
  68. package/src/lint/rules/file-declarable-limit.ts +1 -0
  69. package/src/lint/rules/flat-declarations.test.ts +8 -7
  70. package/src/lint/rules/flat-declarations.ts +2 -3
  71. package/src/lint/rules/no-cyclic-declarable-ref.ts +1 -0
  72. package/src/lint/rules/no-redundant-type-import.ts +1 -0
  73. package/src/lint/rules/no-redundant-value-cast.ts +1 -0
  74. package/src/lint/rules/no-string-ref.ts +1 -0
  75. package/src/lint/rules/no-unused-declarable-import.ts +1 -0
  76. package/src/lint/rules/no-unused-declarable.test.ts +8 -0
  77. package/src/lint/rules/no-unused-declarable.ts +4 -0
  78. package/src/lint/rules/single-concern-file.ts +1 -0
  79. package/src/lsp/lexicon-providers.ts +7 -0
  80. package/src/lsp/types.ts +1 -0
  81. package/src/resource-attributes.test.ts +79 -0
  82. package/src/resource-attributes.ts +42 -0
  83. package/src/runtime-adapter.ts +158 -0
  84. package/src/runtime.ts +4 -3
  85. package/src/serializer-walker.test.ts +0 -9
  86. package/src/serializer-walker.ts +1 -3
  87. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
  88. package/src/codegen/case.test.ts +0 -30
  89. package/src/codegen/case.ts +0 -11
  90. package/src/codegen/rollback.test.ts +0 -92
  91. package/src/codegen/rollback.ts +0 -115
@@ -0,0 +1,112 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { generateRuleCatalog, generateRuleDetailPage } from "./docs-rules";
3
+ import type { RuleEntry } from "../lint/rule-registry";
4
+
5
+ const mockEntries: RuleEntry[] = [
6
+ {
7
+ id: "COR001",
8
+ description: "No inline objects in Declarable constructors",
9
+ category: "style",
10
+ defaultSeverity: "warning",
11
+ source: "core",
12
+ phase: "pre-synth",
13
+ hasAutoFix: false,
14
+ },
15
+ {
16
+ id: "WAW018",
17
+ description: "S3 bucket missing public access block",
18
+ category: "security",
19
+ defaultSeverity: "error",
20
+ source: "aws",
21
+ phase: "post-synth",
22
+ hasAutoFix: false,
23
+ helpUri: "https://chant.dev/lint-rules/waw018",
24
+ },
25
+ {
26
+ id: "COR008",
27
+ description: "Export required for declarable instances",
28
+ category: "correctness",
29
+ defaultSeverity: "warning",
30
+ source: "core",
31
+ phase: "pre-synth",
32
+ hasAutoFix: true,
33
+ },
34
+ ];
35
+
36
+ describe("generateRuleCatalog", () => {
37
+ test("generates valid MDX with frontmatter", () => {
38
+ const mdx = generateRuleCatalog(mockEntries);
39
+
40
+ expect(mdx).toContain("---");
41
+ expect(mdx).toContain('title: "Rule Reference"');
42
+ expect(mdx).toContain("**3** rules");
43
+ });
44
+
45
+ test("groups rules by category", () => {
46
+ const mdx = generateRuleCatalog(mockEntries);
47
+
48
+ expect(mdx).toContain("## Correctness");
49
+ expect(mdx).toContain("## Security");
50
+ expect(mdx).toContain("## Style");
51
+ });
52
+
53
+ test("includes table headers", () => {
54
+ const mdx = generateRuleCatalog(mockEntries);
55
+
56
+ expect(mdx).toContain("| ID | Description | Severity | Phase | Fix |");
57
+ });
58
+
59
+ test("renders rule entries with correct data", () => {
60
+ const mdx = generateRuleCatalog(mockEntries);
61
+
62
+ expect(mdx).toContain("COR001");
63
+ expect(mdx).toContain("No inline objects");
64
+ expect(mdx).toContain("WAW018");
65
+ expect(mdx).toContain("public access");
66
+ });
67
+
68
+ test("marks auto-fix rules", () => {
69
+ const mdx = generateRuleCatalog(mockEntries);
70
+ const lines = mdx.split("\n");
71
+ const cor008Line = lines.find((l) => l.includes("COR008"));
72
+ expect(cor008Line).toContain("Yes");
73
+ });
74
+
75
+ test("renders helpUri as link", () => {
76
+ const mdx = generateRuleCatalog(mockEntries);
77
+ expect(mdx).toContain("[`WAW018`](https://chant.dev/lint-rules/waw018)");
78
+ });
79
+ });
80
+
81
+ describe("generateRuleDetailPage", () => {
82
+ test("generates valid MDX with frontmatter", () => {
83
+ const mdx = generateRuleDetailPage(mockEntries[0]);
84
+
85
+ expect(mdx).toContain("---");
86
+ expect(mdx).toContain("COR001");
87
+ expect(mdx).toContain("No inline objects");
88
+ });
89
+
90
+ test("includes metadata table", () => {
91
+ const mdx = generateRuleDetailPage(mockEntries[0]);
92
+
93
+ expect(mdx).toContain("| **Severity** | warning |");
94
+ expect(mdx).toContain("| **Category** | style |");
95
+ expect(mdx).toContain("| **Phase** | pre-synth |");
96
+ });
97
+
98
+ test("includes configuration example", () => {
99
+ const mdx = generateRuleDetailPage(mockEntries[0]);
100
+
101
+ expect(mdx).toContain("## Configuration");
102
+ expect(mdx).toContain('"COR001"');
103
+ });
104
+
105
+ test("includes disable syntax", () => {
106
+ const mdx = generateRuleDetailPage(mockEntries[0]);
107
+
108
+ expect(mdx).toContain("## Disabling");
109
+ expect(mdx).toContain("chant-disable COR001");
110
+ expect(mdx).toContain("chant-disable-next-line COR001 -- reason");
111
+ });
112
+ });
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Rule documentation auto-generation.
3
+ *
4
+ * Generates MDX pages from a RuleEntry registry:
5
+ * - A catalog page with all rules grouped by category
6
+ * - Per-rule detail pages with configuration examples and disable syntax
7
+ */
8
+
9
+ import type { RuleEntry } from "../lint/rule-registry";
10
+
11
+ /**
12
+ * Generate a rule catalog MDX page with all rules grouped by category.
13
+ */
14
+ export function generateRuleCatalog(
15
+ entries: RuleEntry[],
16
+ displayName = "chant",
17
+ ): string {
18
+ const lines: string[] = [
19
+ "---",
20
+ `title: "Rule Reference"`,
21
+ `description: "Complete reference of all lint rules and post-synth checks"`,
22
+ "---",
23
+ "",
24
+ `This page lists all **${entries.length}** rules available in ${displayName}.`,
25
+ "",
26
+ ];
27
+
28
+ // Group by category
29
+ const byCategory = new Map<string, RuleEntry[]>();
30
+ for (const entry of entries) {
31
+ const cat = entry.category;
32
+ const existing = byCategory.get(cat) ?? [];
33
+ existing.push(entry);
34
+ byCategory.set(cat, existing);
35
+ }
36
+
37
+ // Render each category
38
+ const categoryOrder = ["correctness", "security", "style", "performance"];
39
+ const sortedCategories = [...byCategory.keys()].sort(
40
+ (a, b) => (categoryOrder.indexOf(a) ?? 99) - (categoryOrder.indexOf(b) ?? 99),
41
+ );
42
+
43
+ for (const cat of sortedCategories) {
44
+ const catEntries = byCategory.get(cat)!;
45
+ const title = cat.charAt(0).toUpperCase() + cat.slice(1);
46
+
47
+ lines.push(
48
+ `## ${title}`,
49
+ "",
50
+ "| ID | Description | Severity | Phase | Fix |",
51
+ "|----|-------------|----------|-------|-----|",
52
+ );
53
+
54
+ for (const entry of catEntries.sort((a, b) => a.id.localeCompare(b.id))) {
55
+ const fix = entry.hasAutoFix ? "Yes" : "";
56
+ const link = entry.helpUri
57
+ ? `[\`${entry.id}\`](${entry.helpUri})`
58
+ : `\`${entry.id}\``;
59
+ lines.push(
60
+ `| ${link} | ${entry.description} | ${entry.defaultSeverity} | ${entry.phase} | ${fix} |`,
61
+ );
62
+ }
63
+
64
+ lines.push("");
65
+ }
66
+
67
+ return lines.join("\n");
68
+ }
69
+
70
+ /**
71
+ * Generate a per-rule detail MDX page.
72
+ */
73
+ export function generateRuleDetailPage(entry: RuleEntry): string {
74
+ const lines: string[] = [
75
+ "---",
76
+ `title: "${entry.id}: ${entry.description}"`,
77
+ `description: "${entry.description}"`,
78
+ "---",
79
+ "",
80
+ `# ${entry.id}`,
81
+ "",
82
+ entry.description,
83
+ "",
84
+ "| Property | Value |",
85
+ "|----------|-------|",
86
+ `| **ID** | \`${entry.id}\` |`,
87
+ `| **Severity** | ${entry.defaultSeverity} |`,
88
+ `| **Category** | ${entry.category} |`,
89
+ `| **Phase** | ${entry.phase} |`,
90
+ `| **Source** | ${entry.source} |`,
91
+ `| **Auto-fix** | ${entry.hasAutoFix ? "Yes" : "No"} |`,
92
+ "",
93
+ ];
94
+
95
+ // Configuration example
96
+ lines.push(
97
+ "## Configuration",
98
+ "",
99
+ "Override severity in your `chant.config.ts`:",
100
+ "",
101
+ "```ts",
102
+ "// chant.config.ts",
103
+ `rules: {`,
104
+ ` "${entry.id}": "warning", // or "error", "info", "off"`,
105
+ `}`,
106
+ "```",
107
+ "",
108
+ );
109
+
110
+ // Disable syntax
111
+ lines.push(
112
+ "## Disabling",
113
+ "",
114
+ "Suppress this rule with inline comments:",
115
+ "",
116
+ "```ts",
117
+ `// chant-disable ${entry.id}`,
118
+ `// ... entire file suppressed for ${entry.id}`,
119
+ "",
120
+ `const x = 1; // chant-disable-line ${entry.id}`,
121
+ "",
122
+ `// chant-disable-next-line ${entry.id} -- reason for suppression`,
123
+ `const y = 2;`,
124
+ "```",
125
+ "",
126
+ );
127
+
128
+ return lines.join("\n");
129
+ }
@@ -32,7 +32,7 @@ export interface DocsConfig {
32
32
  /** Custom sections to append to overview page */
33
33
  extraSections?: Array<{ title: string; content: string }>;
34
34
  /** Standalone pages added to the sidebar after Overview */
35
- extraPages?: Array<{ slug: string; title: string; description?: string; content: string }>;
35
+ extraPages?: Array<{ slug: string; title: string; description?: string; content: string; sidebar?: boolean }>;
36
36
  /** Slugs of auto-generated pages to suppress (e.g. "pseudo-parameters") */
37
37
  suppressPages?: string[];
38
38
  /** Source directory for scanning rule files (defaults to srcDir sibling of distDir) */
@@ -385,6 +385,7 @@ function buildSidebar(
385
385
  // Extra pages from lexicon config (appear after Overview)
386
386
  if (config.extraPages) {
387
387
  for (const page of config.extraPages) {
388
+ if (page.sidebar === false) continue;
388
389
  items.push({ label: page.title, slug: page.slug });
389
390
  }
390
391
  }
@@ -447,6 +448,7 @@ function generateOverview(
447
448
  // Extra pages listed first in reference links
448
449
  if (config.extraPages && config.extraPages.length > 0) {
449
450
  for (const page of config.extraPages) {
451
+ if (page.sidebar === false) continue;
450
452
  lines.push(`- [${page.title}](./${page.slug})`);
451
453
  }
452
454
  }
@@ -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
 
@@ -88,6 +88,70 @@ describe("writeConstructor", () => {
88
88
  });
89
89
  });
90
90
 
91
+ describe("writeConstructor with resourceAttributesType", () => {
92
+ test("empty props with attributes type emits both params", () => {
93
+ const lines: string[] = [];
94
+ writeConstructor(lines, [], undefined, "CFResourceAttributes");
95
+ expect(lines.join("\n")).toContain(
96
+ "constructor(props: Record<string, unknown>, attributes?: CFResourceAttributes);",
97
+ );
98
+ });
99
+
100
+ test("props with attributes type emits second param after closing brace", () => {
101
+ const lines: string[] = [];
102
+ writeConstructor(
103
+ lines,
104
+ [{ name: "BucketName", type: "string", required: true }],
105
+ undefined,
106
+ "CFResourceAttributes",
107
+ );
108
+ const output = lines.join("\n");
109
+ expect(output).toContain("constructor(props: {");
110
+ expect(output).toContain("BucketName: string;");
111
+ expect(output).toContain("}, attributes?: CFResourceAttributes);");
112
+ });
113
+
114
+ test("without attributes type, closing brace has no second param", () => {
115
+ const lines: string[] = [];
116
+ writeConstructor(
117
+ lines,
118
+ [{ name: "Name", type: "string", required: true }],
119
+ undefined,
120
+ );
121
+ const output = lines.join("\n");
122
+ expect(output).toContain(" });");
123
+ expect(output).not.toContain("attributes?");
124
+ });
125
+ });
126
+
127
+ describe("writeResourceClass with resourceAttributesType", () => {
128
+ test("resource class constructor includes attributes param", () => {
129
+ const lines: string[] = [];
130
+ writeResourceClass(
131
+ lines,
132
+ "Bucket",
133
+ [{ name: "BucketName", type: "string", required: false }],
134
+ [{ name: "Arn", type: "string" }],
135
+ undefined,
136
+ "CFResourceAttributes",
137
+ );
138
+ const output = lines.join("\n");
139
+ expect(output).toContain("}, attributes?: CFResourceAttributes);");
140
+ expect(output).toContain("readonly Arn: string;");
141
+ });
142
+
143
+ test("property class does not get attributes param", () => {
144
+ const lines: string[] = [];
145
+ writePropertyClass(
146
+ lines,
147
+ "BucketConfig",
148
+ [{ name: "Enabled", type: "boolean", required: false }],
149
+ );
150
+ const output = lines.join("\n");
151
+ expect(output).not.toContain("attributes?");
152
+ });
153
+ });
154
+
91
155
  describe("writeEnumType", () => {
92
156
  test("writes single-line for short enum", () => {
93
157
  const lines: string[] = [];
@@ -42,16 +42,17 @@ export function writeResourceClass(
42
42
  properties: DtsProperty[],
43
43
  attributes: DtsAttribute[],
44
44
  remap?: Map<string, string>,
45
+ resourceAttributesType?: string,
45
46
  ): void {
46
47
  lines.push("");
47
48
  lines.push(`export declare class ${tsName} {`);
48
- writeConstructor(lines, properties, remap);
49
+ writeConstructor(lines, properties, remap, resourceAttributesType);
49
50
 
50
51
  // Attributes as readonly properties (sorted)
51
52
  const attrs = [...attributes].sort((a, b) => a.name.localeCompare(b.name));
52
53
  for (const a of attrs) {
53
54
  const attrType = resolveConstructorType(a.type, remap);
54
- lines.push(` readonly ${toCamelCase(a.name)}: ${attrType};`);
55
+ lines.push(` readonly ${a.name}: ${attrType};`);
55
56
  }
56
57
 
57
58
  lines.push("}");
@@ -79,9 +80,14 @@ export function writeConstructor(
79
80
  lines: string[],
80
81
  props: DtsProperty[],
81
82
  remap: Map<string, string> | undefined,
83
+ resourceAttributesType?: string,
82
84
  ): void {
83
85
  if (props.length === 0) {
84
- lines.push(" constructor(props: Record<string, unknown>);");
86
+ if (resourceAttributesType) {
87
+ lines.push(` constructor(props: Record<string, unknown>, attributes?: ${resourceAttributesType});`);
88
+ } else {
89
+ lines.push(" constructor(props: Record<string, unknown>);");
90
+ }
85
91
  return;
86
92
  }
87
93
 
@@ -98,9 +104,13 @@ export function writeConstructor(
98
104
  if (p.description) {
99
105
  lines.push(` /** ${p.description} */`);
100
106
  }
101
- lines.push(` ${toCamelCase(p.name)}${optional}: ${tsType};`);
107
+ lines.push(` ${p.name}${optional}: ${tsType};`);
108
+ }
109
+ if (resourceAttributesType) {
110
+ lines.push(` }, attributes?: ${resourceAttributesType});`);
111
+ } else {
112
+ lines.push(" });");
102
113
  }
103
- lines.push(" });");
104
114
  }
105
115
 
106
116
  /**
@@ -155,7 +165,3 @@ export function resolveConstructorType(tsType: string, remap: Map<string, string
155
165
 
156
166
  return tsType;
157
167
  }
158
-
159
- function toCamelCase(name: string): string {
160
- return name.charAt(0).toLowerCase() + name.slice(1);
161
- }
@@ -180,26 +180,15 @@ export interface WriteConfig {
180
180
  generatedSubdir?: string;
181
181
  /** Map of filename → content to write. */
182
182
  files: Record<string, string>;
183
- /** Optional snapshot function called before overwriting. */
184
- snapshot?: (generatedDir: string) => void;
185
183
  }
186
184
 
187
185
  /**
188
- * Write generated artifacts to disk with optional auto-snapshot.
186
+ * Write generated artifacts to disk.
189
187
  */
190
188
  export function writeGeneratedArtifacts(config: WriteConfig): void {
191
189
  const generatedDir = join(config.baseDir, config.generatedSubdir ?? "src/generated");
192
190
  mkdirSync(generatedDir, { recursive: true });
193
191
 
194
- // Auto-snapshot before overwriting
195
- if (config.snapshot) {
196
- try {
197
- config.snapshot(generatedDir);
198
- } catch {
199
- // Best effort — don't fail generation if snapshot fails
200
- }
201
- }
202
-
203
192
  for (const [filename, content] of Object.entries(config.files)) {
204
193
  writeFileSync(join(generatedDir, filename), content);
205
194
  }
@@ -113,7 +113,7 @@ export async function packagePipeline(
113
113
  /**
114
114
  * Collect lint rule source files from a lexicon package.
115
115
  * Auto-discovers .ts files in the specified directories,
116
- * skipping test files, barrel files (index.ts), and non-.ts files.
116
+ * skipping test files, re-export files (index.ts), and non-.ts files.
117
117
  */
118
118
  export function collectRules(
119
119
  srcDir: string,
@@ -4,6 +4,7 @@
4
4
  import { writeFileSync, mkdirSync, rmSync } from "fs";
5
5
  import { join } from "path";
6
6
  import { tmpdir } from "os";
7
+ import { getRuntime } from "../runtime-adapter";
7
8
 
8
9
  export interface TypeCheckResult {
9
10
  ok: boolean;
@@ -39,17 +40,11 @@ export async function typecheckDTS(content: string): Promise<TypeCheckResult> {
39
40
  writeFileSync(join(dir, "tsconfig.json"), JSON.stringify(tsconfig, null, 2));
40
41
 
41
42
  // Run tsc
42
- const proc = Bun.spawn(["bunx", "tsc", "--noEmit", "--project", "tsconfig.json"], {
43
- cwd: dir,
44
- stdout: "pipe",
45
- stderr: "pipe",
46
- });
47
-
48
- const [stdout, stderr] = await Promise.all([
49
- new Response(proc.stdout).text(),
50
- new Response(proc.stderr).text(),
51
- ]);
52
- const exitCode = await proc.exited;
43
+ const rt = getRuntime();
44
+ const { stdout, stderr, exitCode } = await rt.spawn(
45
+ [rt.commands.exec, "tsc", "--noEmit", "--project", "tsconfig.json"],
46
+ { cwd: dir },
47
+ );
53
48
 
54
49
  // Parse diagnostics from stdout (tsc writes errors to stdout)
55
50
  const output = stdout + stderr;