@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
@@ -8,18 +8,17 @@
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 {
15
14
  typeName: string;
16
15
  attributes: { name: string }[];
17
16
  properties: { name: string; constraints: PropertyConstraints }[];
18
- propertyTypes: { name: string; cfnType: string }[];
17
+ propertyTypes: { name: string; specType: string }[];
19
18
  }
20
19
 
21
20
  export interface RegistryConfig<E> {
22
- /** Short name extractor (e.g. cfnShortName). */
21
+ /** Short name extractor (e.g. shortName from the spec type). */
23
22
  shortName: (typeName: string) => string;
24
23
  /** Build a resource entry from parsed data. */
25
24
  buildEntry: (
@@ -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
 
@@ -80,7 +79,7 @@ export function buildRegistry<E>(
80
79
  for (const pt of r.propertyTypes) {
81
80
  const defName = extractDefName(pt.name, shortName);
82
81
  const ptName = propertyTypeName(tsName, defName);
83
- const ptEntry = config.buildPropertyEntry(typeName, pt.cfnType);
82
+ const ptEntry = config.buildPropertyEntry(typeName, pt.specType);
84
83
  entries[ptName] = ptEntry;
85
84
 
86
85
  if (ptAliases) {
@@ -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
 
@@ -51,7 +51,7 @@ export function writeResourceClass(
51
51
  const attrs = [...attributes].sort((a, b) => a.name.localeCompare(b.name));
52
52
  for (const a of attrs) {
53
53
  const attrType = resolveConstructorType(a.type, remap);
54
- lines.push(` readonly ${toCamelCase(a.name)}: ${attrType};`);
54
+ lines.push(` readonly ${a.name}: ${attrType};`);
55
55
  }
56
56
 
57
57
  lines.push("}");
@@ -98,7 +98,7 @@ export function writeConstructor(
98
98
  if (p.description) {
99
99
  lines.push(` /** ${p.description} */`);
100
100
  }
101
- lines.push(` ${toCamelCase(p.name)}${optional}: ${tsType};`);
101
+ lines.push(` ${p.name}${optional}: ${tsType};`);
102
102
  }
103
103
  lines.push(" });");
104
104
  }
@@ -155,7 +155,3 @@ export function resolveConstructorType(tsType: string, remap: Map<string, string
155
155
 
156
156
  return tsType;
157
157
  }
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
  }
@@ -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
  */
@@ -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;
@@ -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
+ }
package/src/config.ts CHANGED
@@ -7,6 +7,7 @@ import type { LintConfig } from "./lint/config";
7
7
  * Zod schema for ChantConfig validation.
8
8
  */
9
9
  export const ChantConfigSchema = z.object({
10
+ runtime: z.enum(["bun", "node"]).optional(),
10
11
  lexicons: z.array(z.string().min(1)).optional(),
11
12
  lint: z.record(z.string(), z.unknown()).optional(),
12
13
  }).passthrough();
@@ -17,6 +18,9 @@ export const ChantConfigSchema = z.object({
17
18
  * Loaded from `chant.config.ts` (preferred) or `chant.config.json`.
18
19
  */
19
20
  export interface ChantConfig {
21
+ /** JS runtime to use for spawned commands: "bun" (default) or "node" */
22
+ runtime?: "bun" | "node";
23
+
20
24
  /** Lexicon package names to load (e.g. ["aws"]) */
21
25
  lexicons?: string[];
22
26
 
@@ -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,17 +42,16 @@ 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";
49
48
  export * from "./codegen/package";
50
49
  export * from "./codegen/typecheck";
51
- export * from "./codegen/rollback";
52
50
  export * from "./codegen/coverage";
53
51
  export * from "./codegen/validate";
54
52
  export * from "./codegen/docs";
55
53
  export * from "./runtime";
54
+ export * from "./runtime-adapter";
56
55
  export * from "./stack-output";
57
56
  export * from "./child-project";
58
57
  export * from "./lsp/types";
@@ -2,18 +2,19 @@
2
2
  * Content hashing and integrity verification for lexicon artifacts.
3
3
  */
4
4
  import type { BundleSpec } from "./lexicon";
5
+ import { getRuntime } from "./runtime-adapter";
5
6
 
6
7
  export interface ArtifactIntegrity {
7
- algorithm: "xxhash64";
8
+ algorithm: string;
8
9
  artifacts: Record<string, string>;
9
10
  composite: string;
10
11
  }
11
12
 
12
13
  /**
13
- * Hash a single artifact's content using xxhash64.
14
+ * Hash a single artifact's content using the runtime's hash algorithm.
14
15
  */
15
16
  export function hashArtifact(content: string): string {
16
- return Bun.hash(content).toString(16);
17
+ return getRuntime().hash(content);
17
18
  }
18
19
 
19
20
  /**
@@ -39,7 +40,7 @@ export function computeIntegrity(spec: BundleSpec): ArtifactIntegrity {
39
40
  const compositeInput = sorted.map(([k, v]) => `${k}:${v}`).join("\n");
40
41
  const composite = hashArtifact(compositeInput);
41
42
 
42
- return { algorithm: "xxhash64", artifacts, composite };
43
+ return { algorithm: getRuntime().hashAlgorithm, artifacts, composite };
43
44
  }
44
45
 
45
46
  /**
package/src/lexicon.ts CHANGED
@@ -101,7 +101,7 @@ export interface IntrinsicDef {
101
101
  * Plugin interface for lexicon packages.
102
102
  *
103
103
  * Required lifecycle methods enforce consistency: every lexicon must support
104
- * generate, validate, coverage, package, and rollback operations.
104
+ * generate, validate, coverage, and package operations.
105
105
  */
106
106
  export interface LexiconPlugin {
107
107
  // ── Required ──────────────────────────────────────────────
@@ -123,9 +123,6 @@ export interface LexiconPlugin {
123
123
  /** Package lexicon into distributable tarball */
124
124
  package(options?: { verbose?: boolean; force?: boolean }): Promise<void>;
125
125
 
126
- /** List or restore generation snapshots */
127
- rollback(options?: { restore?: string; verbose?: boolean }): Promise<void>;
128
-
129
126
  // ── Optional extensions ───────────────────────────────────
130
127
  /** Return lint rules provided by this lexicon */
131
128
  lintRules?(): LintRule[];
@@ -206,7 +203,6 @@ export function isLexiconPlugin(value: unknown): value is LexiconPlugin {
206
203
  typeof obj.generate === "function" &&
207
204
  typeof obj.validate === "function" &&
208
205
  typeof obj.coverage === "function" &&
209
- typeof obj.package === "function" &&
210
- typeof obj.rollback === "function"
206
+ typeof obj.package === "function"
211
207
  );
212
208
  }
@@ -1,13 +1,15 @@
1
1
  import { readFileSync, existsSync } from "fs";
2
2
  import { join, dirname, resolve } from "path";
3
+ import { createRequire } from "module";
3
4
  import { z } from "zod";
4
5
  import type { Severity, RuleConfig } from "./rule";
6
+ import { moduleDir, getRuntime } from "../runtime-adapter";
5
7
  import strictPreset from "./presets/strict.json";
6
8
 
7
9
  /** Mapping of built-in preset names to their file paths */
8
10
  const BUILTIN_PRESETS: Record<string, string> = {
9
- "@intentius/chant/lint/presets/strict": resolve(import.meta.dir, "presets/strict.json"),
10
- "@intentius/chant/lint/presets/relaxed": resolve(import.meta.dir, "presets/relaxed.json"),
11
+ "@intentius/chant/lint/presets/strict": resolve(moduleDir(import.meta.url), "presets/strict.json"),
12
+ "@intentius/chant/lint/presets/relaxed": resolve(moduleDir(import.meta.url), "presets/relaxed.json"),
11
13
  };
12
14
 
13
15
  // ── Zod schemas for lint config validation ─────────────────────────
@@ -307,11 +309,12 @@ function loadConfigFile(configPath: string, visited: Set<string> = new Set()): L
307
309
  * @returns Loaded and merged configuration, or default config if not found
308
310
  */
309
311
  export function loadConfig(dir: string): LintConfig {
310
- // Try chant.config.ts first — Bun supports synchronous require() for .ts
312
+ // Try chant.config.ts first — Bun has native require() for .ts, Node uses tsx's loader
311
313
  const tsConfigPath = join(dir, "chant.config.ts");
312
314
  if (existsSync(tsConfigPath)) {
313
315
  try {
314
- const mod = require(tsConfigPath);
316
+ const _require = createRequire(join(dir, "package.json"));
317
+ const mod = _require(tsConfigPath);
315
318
  const config = mod.default ?? mod.config ?? mod;
316
319
  if (typeof config === "object" && config !== null) {
317
320
  // ChantConfig format: extract lint property
@@ -362,8 +365,7 @@ export function resolveRulesForFile(config: LintConfig, filePath: string): Recor
362
365
 
363
366
  for (const override of config.overrides) {
364
367
  const matches = override.files.some((pattern) => {
365
- const glob = new Bun.Glob(pattern);
366
- return glob.match(filePath);
368
+ return getRuntime().globMatch(pattern, filePath);
367
369
  });
368
370
 
369
371
  if (matches) {
@@ -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
  /**
@@ -31,7 +31,7 @@ describe("EVL009: composite-no-constant", () => {
31
31
  expect(diags).toHaveLength(1);
32
32
  expect(diags[0].ruleId).toBe("EVL009");
33
33
  expect(diags[0].message).toContain("assumeRolePolicyDocument");
34
- expect(diags[0].message).toContain("_.$.name");
34
+ expect(diags[0].message).toContain("import directly");
35
35
  });
36
36
 
37
37
  test("flags inline array with objects that doesn't reference props", () => {
@@ -61,11 +61,25 @@ describe("EVL009: composite-no-constant", () => {
61
61
  expect(diags[0].message).toContain("policies");
62
62
  });
63
63
 
64
- test("allows barrel refs (_.$.name)", () => {
64
+ test("allows imported refs (direct import)", () => {
65
65
  const ctx = createContext(`
66
- const MyComp = _.Composite((props) => {
66
+ import { lambdaTrustPolicy } from "./defaults";
67
+ const MyComp = Composite((props) => {
68
+ const role = new Role({
69
+ assumeRolePolicyDocument: lambdaTrustPolicy,
70
+ });
71
+ return { role };
72
+ }, "MyComp");
73
+ `);
74
+ expect(evl009CompositeNoConstantRule.check(ctx)).toHaveLength(0);
75
+ });
76
+
77
+ test("allows namespace import refs", () => {
78
+ const ctx = createContext(`
79
+ import * as defaults from "./defaults";
80
+ const MyComp = Composite((props) => {
67
81
  const role = new Role({
68
- assumeRolePolicyDocument: _.$.lambdaTrustPolicy,
82
+ assumeRolePolicyDocument: defaults.lambdaTrustPolicy,
69
83
  });
70
84
  return { role };
71
85
  }, "MyComp");
@@ -99,8 +113,9 @@ describe("EVL009: composite-no-constant", () => {
99
113
 
100
114
  test("allows sibling member reference", () => {
101
115
  const ctx = createContext(`
116
+ import { trustPolicy } from "./defaults";
102
117
  const MyComp = Composite((props) => {
103
- const role = new Role({ assumeRolePolicyDocument: _.$.trustPolicy });
118
+ const role = new Role({ assumeRolePolicyDocument: trustPolicy });
104
119
  const func = new Function({
105
120
  config: { roleArn: role.arn },
106
121
  });
@@ -146,16 +161,17 @@ describe("EVL009: composite-no-constant", () => {
146
161
  expect(diags).toHaveLength(2);
147
162
  });
148
163
 
149
- test("allows array wrapping barrel ref", () => {
164
+ test("allows array wrapping imported ref", () => {
150
165
  const ctx = createContext(`
166
+ import { lambdaBasicExecutionArn } from "./defaults";
151
167
  const MyComp = Composite((props) => {
152
168
  const role = new Role({
153
- managedPolicyArns: [_.$.lambdaBasicExecutionArn],
169
+ managedPolicyArns: [lambdaBasicExecutionArn],
154
170
  });
155
171
  return { role };
156
172
  }, "MyComp");
157
173
  `);
158
- // Array with barrel ref inside — not flagged (contains barrel ref)
174
+ // Array with imported ref inside — not flagged
159
175
  // Also it's an array of identifiers, no objects inside
160
176
  expect(evl009CompositeNoConstantRule.check(ctx)).toHaveLength(0);
161
177
  });