@intentius/chant 0.0.3 → 0.0.5

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 (48) hide show
  1. package/README.md +10 -351
  2. package/package.json +1 -1
  3. package/src/bench.test.ts +3 -54
  4. package/src/build.ts +14 -1
  5. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +12 -2
  6. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +22 -18
  7. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +12 -2
  8. package/src/cli/commands/import.test.ts +1 -1
  9. package/src/cli/commands/init-lexicon.ts +34 -20
  10. package/src/cli/commands/init.test.ts +10 -14
  11. package/src/cli/commands/init.ts +2 -7
  12. package/src/cli/commands/lint.ts +9 -33
  13. package/src/cli/main.ts +1 -1
  14. package/src/codegen/docs-interpolation.test.ts +77 -0
  15. package/src/codegen/docs.ts +80 -5
  16. package/src/codegen/generate-registry.test.ts +1 -1
  17. package/src/codegen/generate-registry.ts +3 -3
  18. package/src/codegen/package.ts +28 -1
  19. package/src/codegen/validate.ts +16 -0
  20. package/src/discovery/collect.ts +7 -0
  21. package/src/discovery/files.ts +6 -6
  22. package/src/discovery/import.ts +1 -1
  23. package/src/index.ts +0 -1
  24. package/src/lint/engine.ts +1 -5
  25. package/src/lint/rule.ts +0 -18
  26. package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
  27. package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
  28. package/src/lint/rules/index.ts +1 -22
  29. package/src/stack-output.ts +3 -3
  30. package/src/barrel.test.ts +0 -157
  31. package/src/barrel.ts +0 -101
  32. package/src/lint/rules/barrel-import-style.test.ts +0 -80
  33. package/src/lint/rules/barrel-import-style.ts +0 -59
  34. package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
  35. package/src/lint/rules/enforce-barrel-import.ts +0 -81
  36. package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
  37. package/src/lint/rules/enforce-barrel-ref.ts +0 -75
  38. package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
  39. package/src/lint/rules/evl006-barrel-usage.ts +0 -95
  40. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
  41. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
  42. package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
  43. package/src/lint/rules/prefer-namespace-import.ts +0 -63
  44. package/src/lint/rules/stale-barrel-types.ts +0 -60
  45. package/src/project/scan.test.ts +0 -178
  46. package/src/project/scan.ts +0 -182
  47. package/src/project/sync.test.ts +0 -87
  48. package/src/project/sync.ts +0 -46
@@ -22,14 +22,13 @@ describe("initCommand", () => {
22
22
  expect(result.createdFiles).toContain("tsconfig.json");
23
23
  expect(result.createdFiles).toContain("chant.config.ts");
24
24
  expect(result.createdFiles).toContain(".gitignore");
25
- expect(result.createdFiles).toContain("src/_.ts");
26
25
  expect(result.createdFiles).toContain("src/config.ts");
27
26
  expect(result.createdFiles).toContain("src/data-bucket.ts");
28
27
  expect(result.createdFiles).toContain("src/logs-bucket.ts");
29
28
  });
30
29
  });
31
30
 
32
- test("aws source files use namespace imports", async () => {
31
+ test("aws source files use direct imports", async () => {
33
32
  await withTestDir(async (testDir) => {
34
33
  const options: InitOptions = {
35
34
  path: testDir,
@@ -41,15 +40,15 @@ describe("initCommand", () => {
41
40
  await initCommand(options);
42
41
 
43
42
  const configContent = readFileSync(join(testDir, "src", "config.ts"), "utf-8");
44
- expect(configContent).toContain('import * as aws from "@intentius/chant-lexicon-aws"');
43
+ expect(configContent).toContain('from "@intentius/chant-lexicon-aws"');
45
44
 
46
45
  const dataBucketContent = readFileSync(join(testDir, "src", "data-bucket.ts"), "utf-8");
47
- expect(dataBucketContent).toContain('import * as aws from "@intentius/chant-lexicon-aws"');
48
- expect(dataBucketContent).toContain('import * as _ from "./_"');
46
+ expect(dataBucketContent).toContain('from "@intentius/chant-lexicon-aws"');
47
+ expect(dataBucketContent).toContain('from "./config"');
49
48
 
50
49
  const logsBucketContent = readFileSync(join(testDir, "src", "logs-bucket.ts"), "utf-8");
51
- expect(logsBucketContent).toContain('import * as aws from "@intentius/chant-lexicon-aws"');
52
- expect(logsBucketContent).toContain('import * as _ from "./_"');
50
+ expect(logsBucketContent).toContain('from "@intentius/chant-lexicon-aws"');
51
+ expect(logsBucketContent).toContain('from "./config"');
53
52
  });
54
53
  });
55
54
 
@@ -177,7 +176,7 @@ describe("initCommand", () => {
177
176
  });
178
177
  });
179
178
 
180
- test("generates barrel file", async () => {
179
+ test("does not generate barrel file", async () => {
181
180
  await withTestDir(async (testDir) => {
182
181
  const options: InitOptions = {
183
182
  path: testDir,
@@ -188,13 +187,11 @@ describe("initCommand", () => {
188
187
 
189
188
  await initCommand(options);
190
189
 
190
+ // No _.ts barrel — direct imports are used instead
191
191
  const barrelPath = join(testDir, "src", "_.ts");
192
- expect(existsSync(barrelPath)).toBe(true);
192
+ expect(existsSync(barrelPath)).toBe(false);
193
193
 
194
- const barrelContent = readFileSync(barrelPath, "utf-8");
195
- expect(barrelContent).toContain('export * from "./config"');
196
-
197
- // No index.ts — barrel re-exports cause duplicate entity errors during build
194
+ // No index.ts either
198
195
  const indexPath = join(testDir, "src", "index.ts");
199
196
  expect(existsSync(indexPath)).toBe(false);
200
197
  });
@@ -218,7 +215,6 @@ describe("initCommand", () => {
218
215
  expect(coreContent).toContain("Value<T>");
219
216
  expect(coreContent).toContain("Serializer");
220
217
  expect(coreContent).toContain("ChantConfig");
221
- expect(coreContent).toContain("barrel");
222
218
 
223
219
  const corePkg = join(testDir, ".chant", "types", "core", "package.json");
224
220
  expect(existsSync(corePkg)).toBe(true);
@@ -214,8 +214,6 @@ export interface ChantConfig {
214
214
  };
215
215
  }
216
216
 
217
- /** Barrel proxy — lazy-loads all sibling exports */
218
- export declare function barrel(dir: string): Record<string, unknown>;
219
217
  `;
220
218
  }
221
219
 
@@ -345,7 +343,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
345
343
  warnings,
346
344
  );
347
345
 
348
- // Generate source files from plugin (or fallback to a minimal barrel)
346
+ // Generate source files from plugin (if available)
349
347
  let sourceFiles: Record<string, string> = {};
350
348
  try {
351
349
  const plugin = await loadPlugin(options.lexicon);
@@ -353,10 +351,7 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
353
351
  sourceFiles = plugin.initTemplates();
354
352
  }
355
353
  } catch {
356
- // Plugin not yet installed — write a minimal barrel stub
357
- sourceFiles = {
358
- "_.ts": "// Barrel — re-export shared config here\n",
359
- };
354
+ // Plugin not yet installed — no source files to scaffold
360
355
  }
361
356
  for (const [filename, content] of Object.entries(sourceFiles)) {
362
357
  writeIfNotExists(
@@ -1,7 +1,7 @@
1
1
  import { resolve, join } from "path";
2
2
  import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
3
3
  import { runLint } from "../../lint/engine";
4
- import type { LintRule, LintDiagnostic, LintFix, LintRunOptions } from "../../lint/rule";
4
+ import type { LintRule, LintDiagnostic, LintFix } from "../../lint/rule";
5
5
  import { loadPlugins, resolveProjectLexicons } from "../plugins";
6
6
  import { formatStylish, formatJson, formatSarif } from "../reporters/stylish";
7
7
  import { loadLocalRules } from "../../lint/rule-loader";
@@ -241,54 +241,30 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
241
241
  // Get all TypeScript files
242
242
  const files = getTypeScriptFiles(infraPath);
243
243
 
244
- // Build barrel exports context for EVL008
245
- let runOptions: LintRunOptions | undefined;
246
- try {
247
- const { scanProject } = require("../../project/scan");
248
- const scan = scanProject(infraPath);
249
- const barrelExports = new Set<string>(scan.exports.map((e: { name: string }) => e.name));
250
- const projectExports = new Map<string, { file: string; className: string }>();
251
- for (const exp of scan.exports) {
252
- projectExports.set(exp.name, { file: exp.file, className: exp.className });
253
- }
254
- runOptions = { barrelExports, projectExports, projectScan: scan };
255
- } catch {
256
- // No barrel file found — EVL008/COR016 will be no-ops
257
- }
258
-
259
244
  // Run lint — use per-file rules when overrides are present
260
245
  let diagnostics: LintDiagnostic[];
261
246
  if (options.rules) {
262
- diagnostics = await runLint(files, options.rules, undefined, runOptions);
247
+ diagnostics = await runLint(files, options.rules, undefined);
263
248
  } else if (hasOverrides) {
264
249
  diagnostics = [];
265
250
  for (const file of files) {
266
251
  const relativePath = file.slice(infraPath.length + 1);
267
252
  const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
268
- const fileDiagnostics = await runLint([file], fileRules, ruleOptions, runOptions);
253
+ const fileDiagnostics = await runLint([file], fileRules, ruleOptions);
269
254
  diagnostics.push(...fileDiagnostics);
270
255
  }
271
256
  } else {
272
257
  const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
273
- diagnostics = await runLint(files, rules, ruleOptions, runOptions);
258
+ diagnostics = await runLint(files, rules, ruleOptions);
274
259
  }
275
260
 
276
261
  // Apply fixes if requested
277
262
  if (options.fix) {
278
- // Handle cross-file write fixes (COR016) separately
279
- for (const diag of diagnostics) {
280
- if (diag.fix?.kind === "write-file" && diag.fix.params) {
281
- const path = diag.fix.params.path as string;
282
- const content = diag.fix.params.content as string;
283
- writeFileSync(path, content, "utf-8");
284
- }
285
- }
286
-
287
- // Group remaining fixes by file (exclude write-file fixes)
263
+ // Group fixes by file
288
264
  const fixesByFile = new Map<string, LintFix[]>();
289
265
 
290
266
  for (const diag of diagnostics) {
291
- if (diag.fix && diag.fix.kind !== "write-file") {
267
+ if (diag.fix) {
292
268
  const existing = fixesByFile.get(diag.file) ?? [];
293
269
  existing.push(diag.fix);
294
270
  fixesByFile.set(diag.file, existing);
@@ -303,18 +279,18 @@ export async function lintCommand(options: LintOptions): Promise<LintResult> {
303
279
  // Re-lint after fixes to get updated diagnostics
304
280
  let postFixDiagnostics: LintDiagnostic[];
305
281
  if (options.rules) {
306
- postFixDiagnostics = await runLint(files, options.rules, undefined, runOptions);
282
+ postFixDiagnostics = await runLint(files, options.rules, undefined);
307
283
  } else if (hasOverrides) {
308
284
  postFixDiagnostics = [];
309
285
  for (const file of files) {
310
286
  const relativePath = file.slice(infraPath.length + 1);
311
287
  const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
312
- const fileDiagnostics = await runLint([file], fileRules, ruleOptions, runOptions);
288
+ const fileDiagnostics = await runLint([file], fileRules, ruleOptions);
313
289
  postFixDiagnostics.push(...fileDiagnostics);
314
290
  }
315
291
  } else {
316
292
  const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
317
- postFixDiagnostics = await runLint(files, rules, ruleOptions, runOptions);
293
+ postFixDiagnostics = await runLint(files, rules, ruleOptions);
318
294
  }
319
295
  diagnostics.length = 0;
320
296
  diagnostics.push(...postFixDiagnostics);
package/src/cli/main.ts CHANGED
@@ -161,7 +161,7 @@ const registry: CommandDef[] = [
161
161
  { name: "import", handler: runImport },
162
162
  { name: "init", handler: runInit },
163
163
  { name: "init lexicon", handler: runInitLexicon },
164
- { name: "update", handler: runUpdate },
164
+ { name: "update", handler: runUpdate },
165
165
  { name: "doctor", handler: runDoctor },
166
166
 
167
167
  // Dev subcommands
@@ -0,0 +1,77 @@
1
+ import { describe, test, expect, beforeAll, afterAll } from "bun:test";
2
+ import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from "fs";
3
+ import { join } from "path";
4
+ import { tmpdir } from "os";
5
+ import { expandFileMarkers } from "./docs";
6
+
7
+ describe("expandFileMarkers", () => {
8
+ let dir: string;
9
+
10
+ beforeAll(() => {
11
+ dir = mkdtempSync(join(tmpdir(), "docs-interp-"));
12
+ mkdirSync(join(dir, "sub"), { recursive: true });
13
+ writeFileSync(
14
+ join(dir, "example.ts"),
15
+ 'import { Bucket } from "@intentius/chant-lexicon-aws";\n\nexport const bucket = new Bucket({\n bucketName: "test",\n});\n',
16
+ );
17
+ writeFileSync(
18
+ join(dir, "sub", "nested.ts"),
19
+ "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\n",
20
+ );
21
+ });
22
+
23
+ afterAll(() => {
24
+ rmSync(dir, { recursive: true, force: true });
25
+ });
26
+
27
+ test("expands full file marker", () => {
28
+ const result = expandFileMarkers("Before\n\n{{file:example.ts}}\n\nAfter", dir);
29
+ expect(result).toContain('```typescript title="example.ts"');
30
+ expect(result).toContain('import { Bucket } from "@intentius/chant-lexicon-aws";');
31
+ expect(result).toContain("```");
32
+ expect(result).toStartWith("Before\n\n");
33
+ expect(result).toEndWith("\n\nAfter");
34
+ });
35
+
36
+ test("expands line range", () => {
37
+ const result = expandFileMarkers("{{file:sub/nested.ts:3-5}}", dir);
38
+ expect(result).toContain('```typescript title="nested.ts"');
39
+ expect(result).toContain("line3\nline4\nline5");
40
+ expect(result).not.toContain("line2");
41
+ expect(result).not.toContain("line6");
42
+ });
43
+
44
+ test("supports title override", () => {
45
+ const result = expandFileMarkers("{{file:example.ts|title=my-file.ts}}", dir);
46
+ expect(result).toContain('```typescript title="my-file.ts"');
47
+ });
48
+
49
+ test("supports line range with title override", () => {
50
+ const result = expandFileMarkers(
51
+ "{{file:sub/nested.ts:2-4|title=snippet.ts}}",
52
+ dir,
53
+ );
54
+ expect(result).toContain('```typescript title="snippet.ts"');
55
+ expect(result).toContain("line2\nline3\nline4");
56
+ });
57
+
58
+ test("throws on missing file", () => {
59
+ expect(() => expandFileMarkers("{{file:nope.ts}}", dir)).toThrow(
60
+ /file not found/,
61
+ );
62
+ });
63
+
64
+ test("leaves content without markers unchanged", () => {
65
+ const input = "No markers here\n\n```typescript\ncode\n```\n";
66
+ expect(expandFileMarkers(input, dir)).toBe(input);
67
+ });
68
+
69
+ test("expands multiple markers", () => {
70
+ const result = expandFileMarkers(
71
+ "{{file:example.ts}}\n\n{{file:sub/nested.ts:1-2}}",
72
+ dir,
73
+ );
74
+ expect(result).toContain('title="example.ts"');
75
+ expect(result).toContain('title="nested.ts"');
76
+ });
77
+ });
@@ -39,6 +39,8 @@ export interface DocsConfig {
39
39
  srcDir?: string;
40
40
  /** Base path for the generated Astro site (e.g. '/lexicons/aws/') */
41
41
  basePath?: string;
42
+ /** Root directory for resolving {{file:...}} markers in extra page content */
43
+ examplesDir?: string;
42
44
  }
43
45
 
44
46
  export interface DocsResult {
@@ -84,6 +86,66 @@ interface RuleMeta {
84
86
  type: "lint" | "post-synth";
85
87
  }
86
88
 
89
+ // ── File marker interpolation ──────────────────────────────────────
90
+
91
+ /**
92
+ * Expand `{{file:path.ts}}` markers in content with fenced code blocks.
93
+ *
94
+ * Supported forms:
95
+ * - `{{file:path.ts}}` — full file
96
+ * - `{{file:path.ts:5-12}}` — lines 5–12 (1-based, inclusive)
97
+ * - `{{file:path.ts|title=custom.ts}}` — override the code block title
98
+ * - `{{file:path.ts:5-12|title=custom.ts}}` — both
99
+ */
100
+ export function expandFileMarkers(content: string, examplesDir: string): string {
101
+ return content.replace(
102
+ /\{\{file:([^}]+)\}\}/g,
103
+ (_match, spec: string) => {
104
+ // Parse options after |
105
+ let filePart = spec;
106
+ let title: string | undefined;
107
+ const pipeIdx = spec.indexOf("|");
108
+ if (pipeIdx !== -1) {
109
+ filePart = spec.substring(0, pipeIdx);
110
+ const opts = spec.substring(pipeIdx + 1);
111
+ const titleMatch = opts.match(/title=([^\s|]+)/);
112
+ if (titleMatch) title = titleMatch[1];
113
+ }
114
+
115
+ // Parse line range after :digits-digits at end of filePart
116
+ let lineStart: number | undefined;
117
+ let lineEnd: number | undefined;
118
+ const rangeMatch = filePart.match(/^(.+):(\d+)-(\d+)$/);
119
+ if (rangeMatch) {
120
+ filePart = rangeMatch[1];
121
+ lineStart = parseInt(rangeMatch[2], 10);
122
+ lineEnd = parseInt(rangeMatch[3], 10);
123
+ }
124
+
125
+ const filePath = join(examplesDir, filePart);
126
+ let fileContent: string;
127
+ try {
128
+ fileContent = readFileSync(filePath, "utf-8");
129
+ } catch {
130
+ throw new Error(`File marker {{file:${spec}}} — file not found: ${filePath}`);
131
+ }
132
+
133
+ // Extract line range if specified
134
+ if (lineStart !== undefined && lineEnd !== undefined) {
135
+ const lines = fileContent.split("\n");
136
+ fileContent = lines.slice(lineStart - 1, lineEnd).join("\n");
137
+ }
138
+
139
+ // Determine language from extension
140
+ const ext = filePart.substring(filePart.lastIndexOf(".") + 1);
141
+ const lang = ext === "ts" || ext === "tsx" ? "typescript" : ext;
142
+ const displayTitle = title ?? filePart.substring(filePart.lastIndexOf("/") + 1);
143
+
144
+ return `\`\`\`${lang} title="${displayTitle}"\n${fileContent.trimEnd()}\n\`\`\``;
145
+ },
146
+ );
147
+ }
148
+
87
149
  // ── Pipeline ───────────────────────────────────────────────────────
88
150
 
89
151
  /**
@@ -128,15 +190,20 @@ export function docsPipeline(config: DocsConfig): DocsResult {
128
190
  // Generate pages
129
191
  const pages = new Map<string, string>();
130
192
 
131
- pages.set(
132
- "index.mdx",
133
- generateOverview(config, manifest, resources, properties, serviceGroups, rules),
134
- );
193
+ let overviewContent = generateOverview(config, manifest, resources, properties, serviceGroups, rules);
194
+ if (config.examplesDir) {
195
+ overviewContent = expandFileMarkers(overviewContent, config.examplesDir);
196
+ }
197
+ pages.set("index.mdx", overviewContent);
135
198
  const suppress = new Set(config.suppressPages ?? []);
136
199
 
137
200
  // Extra pages from lexicon config
138
201
  if (config.extraPages) {
139
202
  for (const page of config.extraPages) {
203
+ let content = page.content;
204
+ if (config.examplesDir) {
205
+ content = expandFileMarkers(content, config.examplesDir);
206
+ }
140
207
  pages.set(
141
208
  `${page.slug}.mdx`,
142
209
  [
@@ -145,7 +212,7 @@ export function docsPipeline(config: DocsConfig): DocsResult {
145
212
  page.description ? `description: "${page.description}"` : "",
146
213
  "---",
147
214
  "",
148
- page.content,
215
+ content,
149
216
  "",
150
217
  ]
151
218
  .filter(Boolean)
@@ -300,7 +367,15 @@ function buildSidebar(
300
367
  config: DocsConfig,
301
368
  result: DocsResult,
302
369
  ): Array<Record<string, unknown>> {
370
+ // Starlight prepends basePath to every sidebar `link`, so a site-root-relative
371
+ // path like "/chant/" becomes "/chant/lexicons/aws/chant/" — a 404. Instead
372
+ // we use relative traversal: "../../" is prepended to become
373
+ // "/chant/lexicons/aws/../../" which the browser resolves to "/chant/".
374
+ const segments = (config.basePath ?? "/").replace(/^\/|\/$/g, "").split("/");
375
+ const backLink = segments.length > 1 ? "../".repeat(segments.length - 1) : "/";
376
+
303
377
  const items: Array<Record<string, unknown>> = [
378
+ { label: "← chant docs", link: backLink },
304
379
  { label: "Overview", slug: "index" },
305
380
  ];
306
381
 
@@ -69,7 +69,7 @@ describe("buildRegistry", () => {
69
69
  typeName: "Test::S3::Bucket",
70
70
  attributes: [],
71
71
  properties: [],
72
- propertyTypes: [{ name: "Bucket_Versioning", cfnType: "Versioning" }],
72
+ propertyTypes: [{ name: "Bucket_Versioning", specType: "Versioning" }],
73
73
  },
74
74
  ];
75
75
  const naming = makeNaming(resources);
@@ -15,11 +15,11 @@ export interface RegistryResource {
15
15
  typeName: string;
16
16
  attributes: { name: string }[];
17
17
  properties: { name: string; constraints: PropertyConstraints }[];
18
- propertyTypes: { name: string; cfnType: string }[];
18
+ propertyTypes: { name: string; specType: string }[];
19
19
  }
20
20
 
21
21
  export interface RegistryConfig<E> {
22
- /** Short name extractor (e.g. cfnShortName). */
22
+ /** Short name extractor (e.g. shortName from the spec type). */
23
23
  shortName: (typeName: string) => string;
24
24
  /** Build a resource entry from parsed data. */
25
25
  buildEntry: (
@@ -80,7 +80,7 @@ export function buildRegistry<E>(
80
80
  for (const pt of r.propertyTypes) {
81
81
  const defName = extractDefName(pt.name, shortName);
82
82
  const ptName = propertyTypeName(tsName, defName);
83
- const ptEntry = config.buildPropertyEntry(typeName, pt.cfnType);
83
+ const ptEntry = config.buildPropertyEntry(typeName, pt.specType);
84
84
  entries[ptName] = ptEntry;
85
85
 
86
86
  if (ptAliases) {
@@ -5,7 +5,7 @@
5
5
  * assemble BundleSpec → compute integrity → attach metadata.
6
6
  */
7
7
 
8
- import { readFileSync, readdirSync } from "fs";
8
+ import { readFileSync, readdirSync, writeFileSync, mkdirSync } from "fs";
9
9
  import { join } from "path";
10
10
  import type { BundleSpec, LexiconManifest } from "../lexicon";
11
11
  import { computeIntegrity } from "../lexicon-integrity";
@@ -147,6 +147,33 @@ export function collectRules(
147
147
  return rules;
148
148
  }
149
149
 
150
+ /**
151
+ * Write a BundleSpec to the given dist directory.
152
+ *
153
+ * Creates the directory structure and writes all artifacts:
154
+ * manifest.json, meta.json, types/index.d.ts, rules/*, skills/*, integrity.json.
155
+ */
156
+ export function writeBundleSpec(spec: BundleSpec, distDir: string): void {
157
+ mkdirSync(join(distDir, "types"), { recursive: true });
158
+ mkdirSync(join(distDir, "rules"), { recursive: true });
159
+ mkdirSync(join(distDir, "skills"), { recursive: true });
160
+
161
+ writeFileSync(join(distDir, "manifest.json"), JSON.stringify(spec.manifest, null, 2));
162
+ writeFileSync(join(distDir, "meta.json"), spec.registry);
163
+ writeFileSync(join(distDir, "types", "index.d.ts"), spec.typesDTS);
164
+
165
+ for (const [name, content] of spec.rules) {
166
+ writeFileSync(join(distDir, "rules", name), content);
167
+ }
168
+ for (const [name, content] of spec.skills) {
169
+ writeFileSync(join(distDir, "skills", name), content);
170
+ }
171
+
172
+ if (spec.integrity) {
173
+ writeFileSync(join(distDir, "integrity.json"), JSON.stringify(spec.integrity, null, 2));
174
+ }
175
+ }
176
+
150
177
  /**
151
178
  * Collect skills from a plugin's skill definitions.
152
179
  */
@@ -141,3 +141,19 @@ export async function validateLexiconArtifacts(config: LexiconValidationConfig):
141
141
  checks,
142
142
  };
143
143
  }
144
+
145
+ /**
146
+ * Print validation results to stderr and throw on failure.
147
+ */
148
+ export function printValidationResult(result: ValidateResult): void {
149
+ for (const check of result.checks) {
150
+ const status = check.ok ? "OK" : "FAIL";
151
+ const msg = check.error ? ` — ${check.error}` : "";
152
+ console.error(` [${status}] ${check.name}${msg}`);
153
+ }
154
+
155
+ if (!result.success) {
156
+ throw new Error("Validation failed");
157
+ }
158
+ console.error("All validation checks passed.");
159
+ }
@@ -1,11 +1,14 @@
1
1
  import { isDeclarable, type Declarable } from "../declarable";
2
2
  import { isCompositeInstance, expandComposite } from "../composite";
3
+ import { isLexiconOutput } from "../lexicon-output";
3
4
  import { DiscoveryError } from "../errors";
4
5
 
5
6
  /**
6
7
  * Collects all declarable entities from imported modules.
7
8
  * CompositeInstance exports are expanded into individual entities
8
9
  * with `{exportName}_{memberName}` naming.
10
+ * LexiconOutput exports are also collected so that build() can
11
+ * extract them and pass them to the serializer.
9
12
  *
10
13
  * @param modules - Array of module records with their exports
11
14
  * @returns Map of export name to Declarable entity
@@ -43,6 +46,10 @@ export function collectEntities(
43
46
  }
44
47
  entities.set(expandedName, entity);
45
48
  }
49
+ } else if (isLexiconOutput(value)) {
50
+ // LexiconOutput is not a Declarable but build() expects to find them
51
+ // in the entities map so it can collect and pass them to serializers
52
+ entities.set(name, value as unknown as Declarable);
46
53
  }
47
54
  }
48
55
  }
@@ -30,12 +30,12 @@ export async function findInfraFiles(path: string): Promise<string[]> {
30
30
  }
31
31
 
32
32
  if (entry.isDirectory()) {
33
- // Child project boundary — a directory with its own barrel file is a
34
- // separate scope, but only if we've already found the project's own
35
- // source root (the first barrel directory). The project's own src/
36
- // with _.ts is the source root, not a child project.
37
- const barrelPath = join(fullPath, "_.ts");
38
- if (existsSync(barrelPath)) {
33
+ // Child project boundary — a directory with its own chant.config.ts
34
+ // is a separate scope, but only if we've already found the project's
35
+ // own source root. The first config directory is the source root, not
36
+ // a child project.
37
+ const configPath = join(fullPath, "chant.config.ts");
38
+ if (existsSync(configPath)) {
39
39
  if (sourceRoot === null) {
40
40
  sourceRoot = fullPath;
41
41
  } else {
@@ -11,7 +11,7 @@ export async function importModule(
11
11
  path: string
12
12
  ): Promise<Record<string, unknown>> {
13
13
  try {
14
- return require(path);
14
+ return await import(path);
15
15
  } catch (error) {
16
16
  const message =
17
17
  error instanceof Error ? error.message : "Unknown import error";
package/src/index.ts CHANGED
@@ -42,7 +42,6 @@ export * from "./lexicon-schema";
42
42
  export * from "./config";
43
43
  export * from "./validation";
44
44
  export * from "./project-validation";
45
- export { barrel } from "./barrel";
46
45
  export * from "./codegen/naming";
47
46
  export * from "./codegen/fetch";
48
47
  export * from "./codegen/generate";
@@ -1,4 +1,4 @@
1
- import type { LintRule, LintDiagnostic, LintContext, LintRunOptions } from "./rule";
1
+ import type { LintRule, LintDiagnostic, LintContext } from "./rule";
2
2
  import { parseFile } from "./parser";
3
3
  import { readFileSync } from "fs";
4
4
 
@@ -120,7 +120,6 @@ export async function runLint(
120
120
  files: string[],
121
121
  rules: LintRule[],
122
122
  ruleOptions?: Map<string, Record<string, unknown>>,
123
- runOptions?: LintRunOptions,
124
123
  ): Promise<LintDiagnostic[]> {
125
124
  const allDiagnostics: LintDiagnostic[] = [];
126
125
  const allRuleIds = new Set(rules.map((r) => r.id));
@@ -140,9 +139,6 @@ export async function runLint(
140
139
  entities: [],
141
140
  filePath,
142
141
  lexicon: undefined,
143
- barrelExports: runOptions?.barrelExports,
144
- projectExports: runOptions?.projectExports,
145
- projectScan: runOptions?.projectScan,
146
142
  };
147
143
 
148
144
  // Execute each rule
package/src/lint/rule.ts CHANGED
@@ -56,24 +56,6 @@ export interface LintContext {
56
56
  filePath: string;
57
57
  /** Optional lexicon context (undefined for core rules) */
58
58
  lexicon?: string;
59
- /** Export names from the barrel file (for EVL008) */
60
- barrelExports?: Set<string>;
61
- /** All project exports keyed by name (for EVL008) */
62
- projectExports?: Map<string, { file: string; className: string }>;
63
- /** Project scan result (for COR016) */
64
- projectScan?: import("../project/scan").ProjectScan;
65
- }
66
-
67
- /**
68
- * Options for extending the lint context with project-level information
69
- */
70
- export interface LintRunOptions {
71
- /** Export names from the barrel file */
72
- barrelExports?: Set<string>;
73
- /** All project exports keyed by name */
74
- projectExports?: Map<string, { file: string; className: string }>;
75
- /** Project scan result (for COR016) */
76
- projectScan?: import("../project/scan").ProjectScan;
77
59
  }
78
60
 
79
61
  /**