@intentius/chant 0.0.4 → 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 (75) hide show
  1. package/README.md +10 -351
  2. package/bin/chant +20 -0
  3. package/package.json +18 -17
  4. package/src/bench.test.ts +3 -54
  5. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +8 -23
  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 +8 -23
  8. package/src/cli/commands/build.ts +1 -2
  9. package/src/cli/commands/import.test.ts +1 -1
  10. package/src/cli/commands/import.ts +2 -2
  11. package/src/cli/commands/init-lexicon.test.ts +0 -3
  12. package/src/cli/commands/init-lexicon.ts +31 -95
  13. package/src/cli/commands/init.test.ts +10 -14
  14. package/src/cli/commands/init.ts +16 -10
  15. package/src/cli/commands/lint.ts +9 -33
  16. package/src/cli/commands/list.ts +2 -2
  17. package/src/cli/commands/update.ts +5 -3
  18. package/src/cli/conflict-check.test.ts +0 -1
  19. package/src/cli/handlers/dev.ts +1 -9
  20. package/src/cli/main.ts +14 -4
  21. package/src/cli/mcp/server.test.ts +207 -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 +107 -0
  25. package/src/cli/mcp/tools/search.ts +98 -0
  26. package/src/codegen/docs-interpolation.test.ts +2 -2
  27. package/src/codegen/docs.ts +5 -4
  28. package/src/codegen/generate-registry.test.ts +2 -2
  29. package/src/codegen/generate-registry.ts +5 -6
  30. package/src/codegen/generate-typescript.test.ts +6 -6
  31. package/src/codegen/generate-typescript.ts +2 -6
  32. package/src/codegen/generate.ts +1 -12
  33. package/src/codegen/package.ts +28 -1
  34. package/src/codegen/typecheck.ts +6 -11
  35. package/src/codegen/validate.ts +16 -0
  36. package/src/config.ts +4 -0
  37. package/src/discovery/files.ts +6 -6
  38. package/src/discovery/import.ts +1 -1
  39. package/src/index.ts +1 -2
  40. package/src/lexicon-integrity.ts +5 -4
  41. package/src/lexicon.ts +2 -6
  42. package/src/lint/config.ts +8 -6
  43. package/src/lint/engine.ts +1 -5
  44. package/src/lint/rule.ts +0 -18
  45. package/src/lint/rules/evl009-composite-no-constant.test.ts +24 -8
  46. package/src/lint/rules/evl009-composite-no-constant.ts +50 -29
  47. package/src/lint/rules/index.ts +1 -22
  48. package/src/runtime-adapter.ts +158 -0
  49. package/src/serializer-walker.test.ts +0 -9
  50. package/src/serializer-walker.ts +1 -3
  51. package/src/stack-output.ts +3 -3
  52. package/src/barrel.test.ts +0 -157
  53. package/src/barrel.ts +0 -101
  54. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +0 -45
  55. package/src/codegen/case.test.ts +0 -30
  56. package/src/codegen/case.ts +0 -11
  57. package/src/codegen/rollback.test.ts +0 -92
  58. package/src/codegen/rollback.ts +0 -115
  59. package/src/lint/rules/barrel-import-style.test.ts +0 -80
  60. package/src/lint/rules/barrel-import-style.ts +0 -59
  61. package/src/lint/rules/enforce-barrel-import.test.ts +0 -169
  62. package/src/lint/rules/enforce-barrel-import.ts +0 -81
  63. package/src/lint/rules/enforce-barrel-ref.test.ts +0 -114
  64. package/src/lint/rules/enforce-barrel-ref.ts +0 -75
  65. package/src/lint/rules/evl006-barrel-usage.test.ts +0 -63
  66. package/src/lint/rules/evl006-barrel-usage.ts +0 -95
  67. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +0 -118
  68. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +0 -140
  69. package/src/lint/rules/prefer-namespace-import.test.ts +0 -102
  70. package/src/lint/rules/prefer-namespace-import.ts +0 -63
  71. package/src/lint/rules/stale-barrel-types.ts +0 -60
  72. package/src/project/scan.test.ts +0 -178
  73. package/src/project/scan.ts +0 -182
  74. package/src/project/sync.test.ts +0 -87
  75. package/src/project/sync.ts +0 -46
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, writeFileSync, readFileSync } from "fs";
1
+ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync } from "fs";
2
2
  import { join, resolve, basename } from "path";
3
3
  import { formatSuccess, formatWarning, formatError } from "../format";
4
4
  import type { TemplateIR, ResourceIR, TemplateParser } from "../../import/parser";
@@ -266,7 +266,7 @@ export async function importCommand(options: ImportOptions): Promise<ImportResul
266
266
 
267
267
  // Check output directory
268
268
  if (existsSync(outputDir) && !options.force) {
269
- const files = require("fs").readdirSync(outputDir);
269
+ const files = readdirSync(outputDir);
270
270
  if (files.length > 0) {
271
271
  warnings.push(`Output directory ${outputDir} is not empty. Use --force to overwrite.`);
272
272
  }
@@ -40,7 +40,6 @@ describe("initLexiconCommand", () => {
40
40
  "src/codegen/generate-cli.ts",
41
41
  "src/codegen/naming.ts",
42
42
  "src/codegen/package.ts",
43
- "src/codegen/rollback.ts",
44
43
  "src/codegen/docs.ts",
45
44
  "src/spec/fetch.ts",
46
45
  "src/spec/parse.ts",
@@ -65,7 +64,6 @@ describe("initLexiconCommand", () => {
65
64
  "docs/src/content/docs/index.mdx",
66
65
  "src/generated/.gitkeep",
67
66
  "examples/getting-started/.gitkeep",
68
- ".snapshots/.gitkeep",
69
67
  ];
70
68
 
71
69
  for (const file of expectedFiles) {
@@ -83,7 +81,6 @@ describe("initLexiconCommand", () => {
83
81
  expect(pluginContent).toContain("async validate(");
84
82
  expect(pluginContent).toContain("async coverage(");
85
83
  expect(pluginContent).toContain("async package(");
86
- expect(pluginContent).toContain("async rollback(");
87
84
  });
88
85
 
89
86
  test("package name uses the provided lexicon name", async () => {
@@ -83,7 +83,9 @@ export const ${names.pluginVarName}: LexiconPlugin = {
83
83
 
84
84
  async validate(options?: { verbose?: boolean }): Promise<void> {
85
85
  const { validate } = await import("./validate");
86
- await validate(options);
86
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
87
+ const result = await validate();
88
+ printValidationResult(result);
87
89
  },
88
90
 
89
91
  async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
@@ -93,32 +95,15 @@ export const ${names.pluginVarName}: LexiconPlugin = {
93
95
 
94
96
  async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
95
97
  const { packageLexicon } = await import("./codegen/package");
96
- await packageLexicon(options);
97
- },
98
-
99
- async rollback(options?: { restore?: string; verbose?: boolean }): Promise<void> {
100
- const { listSnapshots, restoreSnapshot } = await import("./codegen/rollback");
98
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
101
99
  const { join, dirname } = await import("path");
102
100
  const { fileURLToPath } = await import("url");
103
101
 
102
+ const { spec, stats } = await packageLexicon(options);
104
103
  const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
105
- const snapshotsDir = join(pkgDir, ".snapshots");
106
-
107
- if (options?.restore) {
108
- const generatedDir = join(pkgDir, "src", "generated");
109
- restoreSnapshot(String(options.restore), generatedDir);
110
- console.error(\`Restored snapshot: \${options.restore}\`);
111
- } else {
112
- const snapshots = listSnapshots(snapshotsDir);
113
- if (snapshots.length === 0) {
114
- console.error("No snapshots available.");
115
- } else {
116
- console.error(\`Available snapshots (\${snapshots.length}):\`);
117
- for (const s of snapshots) {
118
- console.error(\` \${s.timestamp} \${s.resources} resources \${s.path}\`);
119
- }
120
- }
121
- }
104
+ writeBundleSpec(spec, join(pkgDir, "dist"));
105
+
106
+ console.error(\`Packaged \${stats.resources} resources, \${stats.ruleCount} rules, \${stats.skillCount} skills\`);
122
107
  },
123
108
 
124
109
  // ── Optional extensions (uncomment and implement as needed) ───
@@ -372,55 +357,6 @@ export async function packageLexicon(options?: { verbose?: boolean; force?: bool
372
357
  `;
373
358
  }
374
359
 
375
- function generateCodegenRollbackTs(): string {
376
- return `import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, cpSync } from "fs";
377
- import { join, basename } from "path";
378
-
379
- export interface Snapshot {
380
- timestamp: string;
381
- resources: number;
382
- path: string;
383
- }
384
-
385
- /**
386
- * List available generation snapshots.
387
- */
388
- export function listSnapshots(snapshotsDir: string): Snapshot[] {
389
- if (!existsSync(snapshotsDir)) return [];
390
-
391
- return readdirSync(snapshotsDir)
392
- .filter((d) => !d.startsWith("."))
393
- .sort()
394
- .reverse()
395
- .map((dir) => {
396
- const fullPath = join(snapshotsDir, dir);
397
- const metaPath = join(fullPath, "meta.json");
398
- let resources = 0;
399
- if (existsSync(metaPath)) {
400
- try {
401
- const meta = JSON.parse(readFileSync(metaPath, "utf-8"));
402
- resources = meta.resources ?? 0;
403
- } catch {}
404
- }
405
- return { timestamp: dir, resources, path: fullPath };
406
- });
407
- }
408
-
409
- /**
410
- * Restore a snapshot to the generated directory.
411
- */
412
- export function restoreSnapshot(timestamp: string, generatedDir: string): void {
413
- const snapshotsDir = join(generatedDir, "..", "..", ".snapshots");
414
- const snapshotDir = join(snapshotsDir, timestamp);
415
- if (!existsSync(snapshotDir)) {
416
- throw new Error(\`Snapshot not found: \${timestamp}\`);
417
- }
418
- mkdirSync(generatedDir, { recursive: true });
419
- cpSync(snapshotDir, generatedDir, { recursive: true });
420
- }
421
- `;
422
- }
423
-
424
360
  function generateCodegenDocsTs(name: string): string {
425
361
  return `import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
426
362
 
@@ -612,28 +548,32 @@ export async function analyzeCoverage(options?: { verbose?: boolean }): Promise<
612
548
 
613
549
  function generateValidateTs(name: string): string {
614
550
  return `/**
615
- * Validate generated artifacts for the ${name} lexicon.
551
+ * Validate generated lexicon-${name} artifacts.
616
552
  *
617
- * TODO: Add validation checks for your generated files.
553
+ * Thin wrapper around the core validation framework
554
+ * with ${name}-specific configuration.
618
555
  */
619
- export async function validate(options?: { verbose?: boolean }): Promise<void> {
620
- const checks = [
621
- // TODO: Add checks — e.g. verify lexicon JSON exists, types compile,
622
- // registry has expected resources, etc.
623
- { name: "placeholder", ok: true, error: undefined as string | undefined },
624
- ];
625
556
 
626
- for (const check of checks) {
627
- const status = check.ok ? "OK" : "FAIL";
628
- const msg = check.error ? \` — \${check.error}\` : "";
629
- console.error(\` [\${status}] \${check.name}\${msg}\`);
630
- }
557
+ import { dirname } from "path";
558
+ import { fileURLToPath } from "url";
559
+ import { validateLexiconArtifacts, type ValidateResult } from "@intentius/chant/codegen/validate";
631
560
 
632
- const failed = checks.filter((c) => !c.ok);
633
- if (failed.length > 0) {
634
- throw new Error("Validation failed");
635
- }
636
- console.error("All validation checks passed.");
561
+ export type { ValidateCheck, ValidateResult } from "@intentius/chant/codegen/validate";
562
+
563
+ // TODO: Add names of required entities for your lexicon
564
+ const REQUIRED_NAMES: string[] = [];
565
+
566
+ /**
567
+ * Validate the generated lexicon-${name} artifacts.
568
+ */
569
+ export async function validate(opts?: { basePath?: string }): Promise<ValidateResult> {
570
+ const basePath = opts?.basePath ?? dirname(dirname(fileURLToPath(import.meta.url)));
571
+
572
+ return validateLexiconArtifacts({
573
+ lexiconJsonFilename: "lexicon-${name}.json",
574
+ requiredNames: REQUIRED_NAMES,
575
+ basePath,
576
+ });
637
577
  }
638
578
  `;
639
579
  }
@@ -721,8 +661,7 @@ package: generate validate
721
661
  }
722
662
 
723
663
  function generateGitignore(): string {
724
- return `.snapshots/
725
- dist/
664
+ return `dist/
726
665
  node_modules/
727
666
  .cache/
728
667
  `;
@@ -885,7 +824,6 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
885
824
  "docs/src/content",
886
825
  "docs/src/content/docs",
887
826
  "examples/getting-started",
888
- ".snapshots",
889
827
  ];
890
828
 
891
829
  for (const dir of dirs) {
@@ -904,7 +842,6 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
904
842
  "src/codegen/generate-cli.ts": generateCodegenGenerateCliTs(),
905
843
  "src/codegen/naming.ts": generateCodegenNamingTs(),
906
844
  "src/codegen/package.ts": generateCodegenPackageTs(name),
907
- "src/codegen/rollback.ts": generateCodegenRollbackTs(),
908
845
  "src/codegen/docs.ts": generateCodegenDocsTs(name),
909
846
  "src/spec/fetch.ts": generateSpecFetchTs(),
910
847
  "src/spec/parse.ts": generateSpecParseTs(),
@@ -933,7 +870,6 @@ export async function initLexiconCommand(options: InitLexiconOptions): Promise<I
933
870
  const gitkeeps = [
934
871
  "src/generated/.gitkeep",
935
872
  "examples/getting-started/.gitkeep",
936
- ".snapshots/.gitkeep",
937
873
  ];
938
874
 
939
875
  for (const gk of gitkeeps) {
@@ -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(
@@ -431,18 +426,29 @@ export async function initCommand(options: InitOptions): Promise<InitResult> {
431
426
  }
432
427
 
433
428
  // Install skills from the lexicon's plugin
429
+ // Write to both .chant/skills/ (chant's own location) and .claude/skills/ (Claude Code discovery)
434
430
  try {
435
431
  const plugin = await loadPlugin(options.lexicon);
436
432
  if (plugin.skills) {
437
433
  const skills = plugin.skills();
438
434
  if (skills.length > 0) {
439
- const skillsDir = join(targetDir, ".chant", "skills", options.lexicon);
440
- mkdirSync(skillsDir, { recursive: true });
435
+ // .chant/skills/ chant's own skill storage
436
+ const chantSkillsDir = join(targetDir, ".chant", "skills", options.lexicon);
437
+ mkdirSync(chantSkillsDir, { recursive: true });
441
438
  for (const skill of skills) {
442
- const skillPath = join(skillsDir, `${skill.name}.md`);
439
+ const skillPath = join(chantSkillsDir, `${skill.name}.md`);
443
440
  writeFileSync(skillPath, skill.content);
444
441
  createdFiles.push(`.chant/skills/${options.lexicon}/${skill.name}.md`);
445
442
  }
443
+
444
+ // .claude/skills/ — Claude Code skill discovery format
445
+ for (const skill of skills) {
446
+ const claudeSkillDir = join(targetDir, ".claude", "skills", skill.name);
447
+ mkdirSync(claudeSkillDir, { recursive: true });
448
+ const claudeSkillPath = join(claudeSkillDir, "SKILL.md");
449
+ writeFileSync(claudeSkillPath, skill.content);
450
+ createdFiles.push(`.claude/skills/${skill.name}/SKILL.md`);
451
+ }
446
452
  }
447
453
  }
448
454
  } catch {
@@ -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);
@@ -51,8 +51,8 @@ export async function listCommand(options: ListOptions): Promise<ListResult> {
51
51
  for (const [name, decl] of result.entities) {
52
52
  entities.push({
53
53
  name,
54
- lexicon: decl.lexicon,
55
- entityType: decl.entityType,
54
+ lexicon: decl.lexicon ?? "",
55
+ entityType: decl.entityType ?? "",
56
56
  kind: decl.kind ?? "resource",
57
57
  });
58
58
  }
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, statSync } from "fs";
1
+ import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync, statSync, readFileSync } from "fs";
2
2
  import { join, resolve } from "path";
3
+ import { createRequire } from "module";
3
4
  import { formatSuccess, formatWarning, formatError } from "../format";
4
5
  import { loadChantConfig } from "../../config";
5
6
  import { loadPlugins } from "../plugins";
@@ -63,13 +64,14 @@ function copyTypeFiles(src: string, dest: string): number {
63
64
  function resolvePackagePath(packageName: string, projectDir: string): string | undefined {
64
65
  // Try resolve from project dir
65
66
  try {
66
- const entryPoint = require.resolve(packageName, { paths: [projectDir] });
67
+ const _require = createRequire(join(projectDir, "package.json"));
68
+ const entryPoint = _require.resolve(packageName);
67
69
  // Walk up from entry point to find package root
68
70
  let dir = entryPoint;
69
71
  while (dir !== "/") {
70
72
  dir = join(dir, "..");
71
73
  if (existsSync(join(dir, "package.json"))) {
72
- const pkg = JSON.parse(require("fs").readFileSync(join(dir, "package.json"), "utf-8"));
74
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8"));
73
75
  if (pkg.name === packageName) return dir;
74
76
  }
75
77
  }
@@ -22,7 +22,6 @@ function makePlugin(
22
22
  validate: noopAsync,
23
23
  coverage: noopAsync,
24
24
  package: noopAsync,
25
- rollback: noopAsync,
26
25
  };
27
26
 
28
27
  if (opts.rules) {
@@ -21,18 +21,10 @@ export async function runDevPublish(ctx: CommandContext): Promise<number> {
21
21
  return 0;
22
22
  }
23
23
 
24
- export async function runDevRollback(ctx: CommandContext): Promise<number> {
25
- for (const plugin of ctx.plugins) {
26
- await plugin.rollback({ verbose: ctx.args.verbose });
27
- console.error(formatSuccess(`${plugin.name}: rollback complete`));
28
- }
29
- return 0;
30
- }
31
-
32
24
  export async function runDevUnknown(ctx: CommandContext): Promise<number> {
33
25
  console.error(formatError({
34
26
  message: `Unknown dev subcommand: ${ctx.args.path}`,
35
- hint: "Available: chant dev generate, chant dev publish, chant dev rollback",
27
+ hint: "Available: chant dev generate, chant dev publish",
36
28
  }));
37
29
  return 1;
38
30
  }
package/src/cli/main.ts CHANGED
@@ -4,9 +4,11 @@ import { resolve } from "node:path";
4
4
  import { formatSuccess, formatError } from "./format";
5
5
  import { loadPlugins, resolveProjectLexicons } from "./plugins";
6
6
  import { resolveCommand, type CommandDef, type ParsedArgs } from "./registry";
7
+ import { loadChantConfig } from "../config";
8
+ import { initRuntime } from "../runtime-adapter";
7
9
  import { runBuild } from "./handlers/build";
8
10
  import { runLint } from "./handlers/lint";
9
- import { runDevGenerate, runDevPublish, runDevRollback, runDevUnknown } from "./handlers/dev";
11
+ import { runDevGenerate, runDevPublish, runDevUnknown } from "./handlers/dev";
10
12
  import { runServeLsp, runServeMcp, runServeUnknown } from "./handlers/serve";
11
13
  import { runInit, runInitLexicon } from "./handlers/init";
12
14
  import { runList, runImport, runUpdate, runDoctor } from "./handlers/misc";
@@ -89,7 +91,6 @@ Commands:
89
91
  Lexicon development:
90
92
  dev generate Generate lexicon artifacts (+ validate + coverage)
91
93
  dev publish Package lexicon for distribution
92
- dev rollback List or restore generation snapshots
93
94
 
94
95
  Servers:
95
96
  serve lsp Start the LSP server (stdio)
@@ -161,13 +162,12 @@ const registry: CommandDef[] = [
161
162
  { name: "import", handler: runImport },
162
163
  { name: "init", handler: runInit },
163
164
  { name: "init lexicon", handler: runInitLexicon },
164
- { name: "update", handler: runUpdate },
165
+ { name: "update", handler: runUpdate },
165
166
  { name: "doctor", handler: runDoctor },
166
167
 
167
168
  // Dev subcommands
168
169
  { name: "dev generate", requiresPlugins: true, handler: runDevGenerate },
169
170
  { name: "dev publish", requiresPlugins: true, handler: runDevPublish },
170
- { name: "dev rollback", requiresPlugins: true, handler: runDevRollback },
171
171
 
172
172
  // Serve subcommands
173
173
  { name: "serve lsp", requiresPlugins: true, handler: runServeLsp },
@@ -189,6 +189,16 @@ async function main(): Promise<void> {
189
189
  process.exit(args.help ? 0 : 1);
190
190
  }
191
191
 
192
+ // Initialize runtime adapter early — before plugins or commands run
193
+ const projectPath0 = resolve(args.path === "." ? "." : args.path);
194
+ try {
195
+ const { config } = await loadChantConfig(projectPath0);
196
+ initRuntime(config.runtime);
197
+ } catch {
198
+ // Config may not exist yet (e.g. `chant init`); auto-detect runtime
199
+ initRuntime();
200
+ }
201
+
192
202
  const match = resolveCommand(args, registry);
193
203
  if (!match) {
194
204
  console.error(formatError({