@intentius/chant 0.0.1

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 (271) hide show
  1. package/README.md +365 -0
  2. package/package.json +22 -0
  3. package/src/attrref.test.ts +148 -0
  4. package/src/attrref.ts +50 -0
  5. package/src/barrel.test.ts +157 -0
  6. package/src/barrel.ts +101 -0
  7. package/src/bench.test.ts +227 -0
  8. package/src/build.test.ts +437 -0
  9. package/src/build.ts +425 -0
  10. package/src/builder.test.ts +312 -0
  11. package/src/builder.ts +56 -0
  12. package/src/child-project.ts +44 -0
  13. package/src/cli/commands/__fixtures__/init-lexicon-output/README.md +26 -0
  14. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/astro.config.mjs +14 -0
  15. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/package.json +16 -0
  16. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content/docs/index.mdx +8 -0
  17. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +7 -0
  18. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/tsconfig.json +10 -0
  19. package/src/cli/commands/__fixtures__/init-lexicon-output/examples/getting-started/.gitkeep +0 -0
  20. package/src/cli/commands/__fixtures__/init-lexicon-output/justfile +26 -0
  21. package/src/cli/commands/__fixtures__/init-lexicon-output/package.json +29 -0
  22. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/docs.ts +25 -0
  23. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +8 -0
  24. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +74 -0
  25. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/naming.ts +33 -0
  26. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/package.ts +25 -0
  27. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/rollback.ts +45 -0
  28. package/src/cli/commands/__fixtures__/init-lexicon-output/src/coverage.ts +11 -0
  29. package/src/cli/commands/__fixtures__/init-lexicon-output/src/generated/.gitkeep +0 -0
  30. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/generator.ts +10 -0
  31. package/src/cli/commands/__fixtures__/init-lexicon-output/src/import/parser.ts +10 -0
  32. package/src/cli/commands/__fixtures__/init-lexicon-output/src/index.ts +9 -0
  33. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/index.ts +1 -0
  34. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lint/rules/sample.ts +18 -0
  35. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/completions.ts +14 -0
  36. package/src/cli/commands/__fixtures__/init-lexicon-output/src/lsp/hover.ts +14 -0
  37. package/src/cli/commands/__fixtures__/init-lexicon-output/src/plugin.ts +110 -0
  38. package/src/cli/commands/__fixtures__/init-lexicon-output/src/serializer.ts +24 -0
  39. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/fetch.ts +21 -0
  40. package/src/cli/commands/__fixtures__/init-lexicon-output/src/spec/parse.ts +25 -0
  41. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate-cli.ts +4 -0
  42. package/src/cli/commands/__fixtures__/init-lexicon-output/src/validate.ts +24 -0
  43. package/src/cli/commands/__fixtures__/init-lexicon-output/tsconfig.json +10 -0
  44. package/src/cli/commands/__fixtures__/sample-rule.ts +11 -0
  45. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +222 -0
  46. package/src/cli/commands/build.test.ts +149 -0
  47. package/src/cli/commands/build.ts +344 -0
  48. package/src/cli/commands/diff.test.ts +148 -0
  49. package/src/cli/commands/diff.ts +221 -0
  50. package/src/cli/commands/doctor.test.ts +239 -0
  51. package/src/cli/commands/doctor.ts +224 -0
  52. package/src/cli/commands/import.test.ts +379 -0
  53. package/src/cli/commands/import.ts +335 -0
  54. package/src/cli/commands/init-lexicon.test.ts +297 -0
  55. package/src/cli/commands/init-lexicon.ts +993 -0
  56. package/src/cli/commands/init.test.ts +317 -0
  57. package/src/cli/commands/init.ts +505 -0
  58. package/src/cli/commands/licenses.ts +165 -0
  59. package/src/cli/commands/lint.test.ts +332 -0
  60. package/src/cli/commands/lint.ts +408 -0
  61. package/src/cli/commands/list.test.ts +100 -0
  62. package/src/cli/commands/list.ts +108 -0
  63. package/src/cli/commands/update.test.ts +38 -0
  64. package/src/cli/commands/update.ts +207 -0
  65. package/src/cli/conflict-check.test.ts +255 -0
  66. package/src/cli/conflict-check.ts +89 -0
  67. package/src/cli/debug.ts +8 -0
  68. package/src/cli/format.test.ts +140 -0
  69. package/src/cli/format.ts +133 -0
  70. package/src/cli/handlers/build.ts +58 -0
  71. package/src/cli/handlers/dev.ts +38 -0
  72. package/src/cli/handlers/init.ts +46 -0
  73. package/src/cli/handlers/lint.ts +36 -0
  74. package/src/cli/handlers/misc.ts +57 -0
  75. package/src/cli/handlers/serve.ts +26 -0
  76. package/src/cli/index.ts +3 -0
  77. package/src/cli/lsp/capabilities.ts +46 -0
  78. package/src/cli/lsp/diagnostics.ts +52 -0
  79. package/src/cli/lsp/server.test.ts +618 -0
  80. package/src/cli/lsp/server.ts +393 -0
  81. package/src/cli/main.test.ts +257 -0
  82. package/src/cli/main.ts +224 -0
  83. package/src/cli/mcp/resources/context.ts +59 -0
  84. package/src/cli/mcp/server.test.ts +747 -0
  85. package/src/cli/mcp/server.ts +402 -0
  86. package/src/cli/mcp/tools/build.ts +117 -0
  87. package/src/cli/mcp/tools/import.ts +48 -0
  88. package/src/cli/mcp/tools/lint.ts +45 -0
  89. package/src/cli/plugins.test.ts +31 -0
  90. package/src/cli/plugins.ts +94 -0
  91. package/src/cli/registry.ts +73 -0
  92. package/src/cli/reporters/stylish.test.ts +282 -0
  93. package/src/cli/reporters/stylish.ts +186 -0
  94. package/src/cli/watch.test.ts +81 -0
  95. package/src/cli/watch.ts +101 -0
  96. package/src/codegen/case.test.ts +30 -0
  97. package/src/codegen/case.ts +11 -0
  98. package/src/codegen/coverage.ts +167 -0
  99. package/src/codegen/docs.ts +634 -0
  100. package/src/codegen/fetch.test.ts +119 -0
  101. package/src/codegen/fetch.ts +261 -0
  102. package/src/codegen/generate-registry.test.ts +118 -0
  103. package/src/codegen/generate-registry.ts +107 -0
  104. package/src/codegen/generate-runtime-index.test.ts +81 -0
  105. package/src/codegen/generate-runtime-index.ts +99 -0
  106. package/src/codegen/generate-typescript.test.ts +146 -0
  107. package/src/codegen/generate-typescript.ts +161 -0
  108. package/src/codegen/generate.ts +206 -0
  109. package/src/codegen/json-patch.test.ts +113 -0
  110. package/src/codegen/json-patch.ts +151 -0
  111. package/src/codegen/json-schema.test.ts +196 -0
  112. package/src/codegen/json-schema.ts +209 -0
  113. package/src/codegen/naming.ts +201 -0
  114. package/src/codegen/package.ts +161 -0
  115. package/src/codegen/rollback.test.ts +92 -0
  116. package/src/codegen/rollback.ts +115 -0
  117. package/src/codegen/topo-sort.test.ts +69 -0
  118. package/src/codegen/topo-sort.ts +46 -0
  119. package/src/codegen/typecheck.test.ts +37 -0
  120. package/src/codegen/typecheck.ts +74 -0
  121. package/src/codegen/validate.test.ts +86 -0
  122. package/src/codegen/validate.ts +143 -0
  123. package/src/composite.test.ts +426 -0
  124. package/src/composite.ts +243 -0
  125. package/src/config.test.ts +91 -0
  126. package/src/config.ts +87 -0
  127. package/src/declarable.test.ts +160 -0
  128. package/src/declarable.ts +47 -0
  129. package/src/detectLexicon.test.ts +236 -0
  130. package/src/detectLexicon.ts +37 -0
  131. package/src/discovery/cache.test.ts +78 -0
  132. package/src/discovery/cache.ts +86 -0
  133. package/src/discovery/collect.test.ts +269 -0
  134. package/src/discovery/collect.ts +51 -0
  135. package/src/discovery/cycles.test.ts +238 -0
  136. package/src/discovery/cycles.ts +107 -0
  137. package/src/discovery/files.test.ts +154 -0
  138. package/src/discovery/files.ts +61 -0
  139. package/src/discovery/graph.test.ts +476 -0
  140. package/src/discovery/graph.ts +150 -0
  141. package/src/discovery/import.test.ts +199 -0
  142. package/src/discovery/import.ts +20 -0
  143. package/src/discovery/index.test.ts +272 -0
  144. package/src/discovery/index.ts +132 -0
  145. package/src/discovery/resolve.test.ts +267 -0
  146. package/src/discovery/resolve.ts +54 -0
  147. package/src/errors.test.ts +138 -0
  148. package/src/errors.ts +86 -0
  149. package/src/import/base-parser.test.ts +67 -0
  150. package/src/import/base-parser.ts +48 -0
  151. package/src/import/generator.ts +21 -0
  152. package/src/import/ir-utils.test.ts +103 -0
  153. package/src/import/ir-utils.ts +87 -0
  154. package/src/import/parser.ts +41 -0
  155. package/src/index.ts +60 -0
  156. package/src/intrinsic-interpolation.test.ts +91 -0
  157. package/src/intrinsic-interpolation.ts +89 -0
  158. package/src/intrinsic.test.ts +69 -0
  159. package/src/intrinsic.ts +43 -0
  160. package/src/lexicon-integrity.test.ts +94 -0
  161. package/src/lexicon-integrity.ts +69 -0
  162. package/src/lexicon-manifest.test.ts +101 -0
  163. package/src/lexicon-manifest.ts +71 -0
  164. package/src/lexicon-output.test.ts +182 -0
  165. package/src/lexicon-output.ts +82 -0
  166. package/src/lexicon-schema.test.ts +239 -0
  167. package/src/lexicon-schema.ts +144 -0
  168. package/src/lexicon.ts +212 -0
  169. package/src/lint/config-overrides.test.ts +254 -0
  170. package/src/lint/config.test.ts +644 -0
  171. package/src/lint/config.ts +375 -0
  172. package/src/lint/declarative.test.ts +256 -0
  173. package/src/lint/declarative.ts +187 -0
  174. package/src/lint/engine.test.ts +465 -0
  175. package/src/lint/engine.ts +172 -0
  176. package/src/lint/named-checks.test.ts +37 -0
  177. package/src/lint/named-checks.ts +33 -0
  178. package/src/lint/parser.test.ts +129 -0
  179. package/src/lint/parser.ts +42 -0
  180. package/src/lint/post-synth.test.ts +113 -0
  181. package/src/lint/post-synth.ts +76 -0
  182. package/src/lint/presets/relaxed.json +19 -0
  183. package/src/lint/presets/strict.json +19 -0
  184. package/src/lint/rule-loader.test.ts +67 -0
  185. package/src/lint/rule-loader.ts +67 -0
  186. package/src/lint/rule-options.test.ts +141 -0
  187. package/src/lint/rule.test.ts +196 -0
  188. package/src/lint/rule.ts +98 -0
  189. package/src/lint/rules/barrel-import-style.test.ts +80 -0
  190. package/src/lint/rules/barrel-import-style.ts +59 -0
  191. package/src/lint/rules/composite-scope.ts +55 -0
  192. package/src/lint/rules/cor017-composite-name-match.test.ts +107 -0
  193. package/src/lint/rules/cor017-composite-name-match.ts +108 -0
  194. package/src/lint/rules/cor018-composite-prefer-lexicon-type.test.ts +172 -0
  195. package/src/lint/rules/cor018-composite-prefer-lexicon-type.ts +167 -0
  196. package/src/lint/rules/declarable-naming-convention.test.ts +69 -0
  197. package/src/lint/rules/declarable-naming-convention.ts +70 -0
  198. package/src/lint/rules/enforce-barrel-import.test.ts +169 -0
  199. package/src/lint/rules/enforce-barrel-import.ts +81 -0
  200. package/src/lint/rules/enforce-barrel-ref.test.ts +114 -0
  201. package/src/lint/rules/enforce-barrel-ref.ts +75 -0
  202. package/src/lint/rules/evl001-non-literal-expression.test.ts +158 -0
  203. package/src/lint/rules/evl001-non-literal-expression.ts +149 -0
  204. package/src/lint/rules/evl002-control-flow-resource.test.ts +110 -0
  205. package/src/lint/rules/evl002-control-flow-resource.ts +61 -0
  206. package/src/lint/rules/evl003-dynamic-property-access.test.ts +63 -0
  207. package/src/lint/rules/evl003-dynamic-property-access.ts +41 -0
  208. package/src/lint/rules/evl004-spread-non-const.test.ts +130 -0
  209. package/src/lint/rules/evl004-spread-non-const.ts +111 -0
  210. package/src/lint/rules/evl005-resource-block-body.test.ts +59 -0
  211. package/src/lint/rules/evl005-resource-block-body.ts +49 -0
  212. package/src/lint/rules/evl006-barrel-usage.test.ts +63 -0
  213. package/src/lint/rules/evl006-barrel-usage.ts +95 -0
  214. package/src/lint/rules/evl007-invalid-siblings.test.ts +87 -0
  215. package/src/lint/rules/evl007-invalid-siblings.ts +139 -0
  216. package/src/lint/rules/evl008-unresolvable-barrel-ref.test.ts +118 -0
  217. package/src/lint/rules/evl008-unresolvable-barrel-ref.ts +140 -0
  218. package/src/lint/rules/evl009-composite-no-constant.test.ts +162 -0
  219. package/src/lint/rules/evl009-composite-no-constant.ts +171 -0
  220. package/src/lint/rules/evl010-composite-no-transform.test.ts +121 -0
  221. package/src/lint/rules/evl010-composite-no-transform.ts +69 -0
  222. package/src/lint/rules/export-required.test.ts +213 -0
  223. package/src/lint/rules/export-required.ts +158 -0
  224. package/src/lint/rules/file-declarable-limit.test.ts +148 -0
  225. package/src/lint/rules/file-declarable-limit.ts +96 -0
  226. package/src/lint/rules/flat-declarations.test.ts +210 -0
  227. package/src/lint/rules/flat-declarations.ts +70 -0
  228. package/src/lint/rules/index.ts +99 -0
  229. package/src/lint/rules/no-cyclic-declarable-ref.test.ts +135 -0
  230. package/src/lint/rules/no-cyclic-declarable-ref.ts +178 -0
  231. package/src/lint/rules/no-redundant-type-import.test.ts +129 -0
  232. package/src/lint/rules/no-redundant-type-import.ts +85 -0
  233. package/src/lint/rules/no-redundant-value-cast.test.ts +51 -0
  234. package/src/lint/rules/no-redundant-value-cast.ts +46 -0
  235. package/src/lint/rules/no-string-ref.test.ts +100 -0
  236. package/src/lint/rules/no-string-ref.ts +66 -0
  237. package/src/lint/rules/no-unused-declarable-import.test.ts +74 -0
  238. package/src/lint/rules/no-unused-declarable-import.ts +103 -0
  239. package/src/lint/rules/no-unused-declarable.test.ts +134 -0
  240. package/src/lint/rules/no-unused-declarable.ts +118 -0
  241. package/src/lint/rules/prefer-namespace-import.test.ts +102 -0
  242. package/src/lint/rules/prefer-namespace-import.ts +63 -0
  243. package/src/lint/rules/single-concern-file.test.ts +156 -0
  244. package/src/lint/rules/single-concern-file.ts +98 -0
  245. package/src/lint/rules/stale-barrel-types.ts +60 -0
  246. package/src/lint/selectors.test.ts +113 -0
  247. package/src/lint/selectors.ts +188 -0
  248. package/src/lsp/lexicon-providers.ts +191 -0
  249. package/src/lsp/types.ts +79 -0
  250. package/src/mcp/types.ts +22 -0
  251. package/src/project/scan.test.ts +178 -0
  252. package/src/project/scan.ts +182 -0
  253. package/src/project/sync.test.ts +87 -0
  254. package/src/project/sync.ts +46 -0
  255. package/src/project-validation.test.ts +64 -0
  256. package/src/project-validation.ts +79 -0
  257. package/src/pseudo-parameter.test.ts +39 -0
  258. package/src/pseudo-parameter.ts +47 -0
  259. package/src/runtime.ts +68 -0
  260. package/src/serializer-walker.test.ts +124 -0
  261. package/src/serializer-walker.ts +83 -0
  262. package/src/serializer.ts +42 -0
  263. package/src/sort.test.ts +290 -0
  264. package/src/sort.ts +58 -0
  265. package/src/stack-output.ts +82 -0
  266. package/src/types.test.ts +307 -0
  267. package/src/types.ts +46 -0
  268. package/src/utils.test.ts +195 -0
  269. package/src/utils.ts +46 -0
  270. package/src/validation.test.ts +308 -0
  271. package/src/validation.ts +50 -0
@@ -0,0 +1,408 @@
1
+ import { resolve, join } from "path";
2
+ import { readFileSync, writeFileSync, readdirSync, statSync } from "fs";
3
+ import { runLint } from "../../lint/engine";
4
+ import type { LintRule, LintDiagnostic, LintFix, LintRunOptions } from "../../lint/rule";
5
+ import { loadPlugins, resolveProjectLexicons } from "../plugins";
6
+ import { formatStylish, formatJson, formatSarif } from "../reporters/stylish";
7
+ import { loadLocalRules } from "../../lint/rule-loader";
8
+ import { loadCoreRules } from "../../lint/rules/index";
9
+ import { rule } from "../../lint/declarative";
10
+ import { watchDirectory, formatTimestamp, formatChangedFiles } from "../watch";
11
+ import { formatError, formatInfo } from "../format";
12
+
13
+ // Import config loader
14
+ import { loadConfig, resolveRulesForFile, parseRuleConfig } from "../../lint/config";
15
+ import type { RuleConfig } from "../../lint/rule";
16
+
17
+ /**
18
+ * Type guard to check if a value conforms to the LintRule interface.
19
+ */
20
+ export function isLintRule(value: unknown): value is LintRule {
21
+ return (
22
+ typeof value === "object" &&
23
+ value !== null &&
24
+ "id" in value &&
25
+ typeof (value as Record<string, unknown>).id === "string" &&
26
+ "severity" in value &&
27
+ "category" in value &&
28
+ "check" in value &&
29
+ typeof (value as Record<string, unknown>).check === "function"
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Load custom lint rules from plugin files.
35
+ * Each plugin file is dynamically imported and all exports conforming to LintRule are collected.
36
+ */
37
+ export async function loadPluginRules(
38
+ plugins: string[],
39
+ configDir: string,
40
+ ): Promise<Map<string, LintRule>> {
41
+ const pluginRules = new Map<string, LintRule>();
42
+ for (const pluginPath of plugins) {
43
+ const resolved = resolve(configDir, pluginPath);
44
+ let mod: Record<string, unknown>;
45
+ try {
46
+ mod = await import(resolved);
47
+ } catch (err) {
48
+ throw new Error(
49
+ `Failed to load plugin "${pluginPath}": ${err instanceof Error ? err.message : String(err)}`,
50
+ );
51
+ }
52
+ for (const value of Object.values(mod)) {
53
+ if (isLintRule(value)) {
54
+ pluginRules.set(value.id, value);
55
+ }
56
+ }
57
+ }
58
+ return pluginRules;
59
+ }
60
+
61
+ /**
62
+ * Load all lint rules: core COR/EVL rules, then lexicon plugin rules.
63
+ */
64
+ async function loadAllPluginRules(projectPath: string): Promise<Map<string, LintRule>> {
65
+ const rules = new Map<string, LintRule>();
66
+
67
+ // Load core COR/EVL rules directly
68
+ for (const r of loadCoreRules()) {
69
+ rules.set(r.id, r);
70
+ }
71
+
72
+ // Resolve project lexicons (e.g. ["aws"]) from config or detection
73
+ let lexiconNames: string[] = [];
74
+ try {
75
+ lexiconNames = await resolveProjectLexicons(projectPath);
76
+ } catch {
77
+ // No lexicons detected — core rules only
78
+ }
79
+
80
+ // Load only project lexicon plugins (no "chant" injection)
81
+ const plugins = await loadPlugins(lexiconNames);
82
+
83
+ for (const plugin of plugins) {
84
+ if (plugin.lintRules) {
85
+ for (const r of plugin.lintRules()) {
86
+ rules.set(r.id, r);
87
+ }
88
+ }
89
+ // Compile declarative rules from plugins
90
+ if (plugin.declarativeRules) {
91
+ for (const spec of plugin.declarativeRules()) {
92
+ const compiled = rule(spec);
93
+ rules.set(compiled.id, compiled);
94
+ }
95
+ }
96
+ }
97
+
98
+ // Load project-local rules from .chant/rules/
99
+ const localRules = await loadLocalRules(projectPath);
100
+ for (const r of localRules) {
101
+ rules.set(r.id, r);
102
+ }
103
+
104
+ return rules;
105
+ }
106
+
107
+ /**
108
+ * Lint command options
109
+ */
110
+ export interface LintOptions {
111
+ /** Path to lint */
112
+ path: string;
113
+ /** Apply auto-fixes */
114
+ fix?: boolean;
115
+ /** Output format */
116
+ format: "stylish" | "json" | "sarif";
117
+ /** Rules to use (defaults to all) */
118
+ rules?: LintRule[];
119
+ }
120
+
121
+ /**
122
+ * Lint command result
123
+ */
124
+ export interface LintResult {
125
+ /** Whether lint passed (no errors) */
126
+ success: boolean;
127
+ /** Number of errors */
128
+ errorCount: number;
129
+ /** Number of warnings */
130
+ warningCount: number;
131
+ /** All diagnostics */
132
+ diagnostics: LintDiagnostic[];
133
+ /** Formatted output */
134
+ output: string;
135
+ }
136
+
137
+ /**
138
+ * Get all TypeScript files recursively
139
+ */
140
+ function getTypeScriptFiles(dir: string): string[] {
141
+ const files: string[] = [];
142
+
143
+ function scan(currentDir: string): void {
144
+ const entries = readdirSync(currentDir);
145
+
146
+ for (const entry of entries) {
147
+ const fullPath = join(currentDir, entry);
148
+ const stat = statSync(fullPath);
149
+
150
+ if (stat.isDirectory()) {
151
+ if (entry !== "node_modules" && !entry.startsWith(".")) {
152
+ scan(fullPath);
153
+ }
154
+ } else if (entry.endsWith(".ts") && !entry.endsWith(".test.ts") && !entry.endsWith(".spec.ts")) {
155
+ files.push(fullPath);
156
+ }
157
+ }
158
+ }
159
+
160
+ scan(dir);
161
+ return files;
162
+ }
163
+
164
+ /**
165
+ * Get default rules and options, optionally applying per-file overrides
166
+ */
167
+ function getDefaultRules(
168
+ infraPath: string,
169
+ filePath?: string,
170
+ allRules: Map<string, LintRule> = new Map(),
171
+ ): { rules: LintRule[]; ruleOptions: Map<string, Record<string, unknown>> } {
172
+ const config = loadConfig(infraPath);
173
+ const effectiveRules = filePath ? resolveRulesForFile(config, filePath) : config.rules;
174
+ const rules: LintRule[] = [];
175
+ const ruleOptions = new Map<string, Record<string, unknown>>();
176
+
177
+ for (const [ruleId, rule] of allRules) {
178
+ const configValue: RuleConfig | undefined = effectiveRules?.[ruleId];
179
+
180
+ if (configValue === undefined) {
181
+ // Rule not mentioned in config — include with default severity
182
+ rules.push(rule);
183
+ continue;
184
+ }
185
+
186
+ const parsed = parseRuleConfig(configValue);
187
+
188
+ // Skip rules that are explicitly turned off
189
+ if (parsed.severity === "off") continue;
190
+
191
+ // Override severity from config
192
+ rules.push({
193
+ ...rule,
194
+ severity: parsed.severity as "error" | "warning" | "info",
195
+ });
196
+
197
+ // Store options if present
198
+ if (parsed.options) {
199
+ ruleOptions.set(ruleId, parsed.options);
200
+ }
201
+ }
202
+
203
+ return { rules, ruleOptions };
204
+ }
205
+
206
+ /**
207
+ * Apply fixes to a file
208
+ */
209
+ function applyFixes(filePath: string, fixes: LintFix[]): void {
210
+ if (fixes.length === 0) return;
211
+
212
+ let content = readFileSync(filePath, "utf-8");
213
+
214
+ // Sort fixes by position descending so we can apply from end to start
215
+ const sortedFixes = [...fixes].sort((a, b) => b.range[0] - a.range[0]);
216
+
217
+ for (const fix of sortedFixes) {
218
+ content = content.slice(0, fix.range[0]) + fix.replacement + content.slice(fix.range[1]);
219
+ }
220
+
221
+ writeFileSync(filePath, content);
222
+ }
223
+
224
+ /**
225
+ * Execute the lint command
226
+ */
227
+ export async function lintCommand(options: LintOptions): Promise<LintResult> {
228
+ const infraPath = resolve(options.path);
229
+ const config = loadConfig(infraPath);
230
+ const hasOverrides = config.overrides && config.overrides.length > 0;
231
+
232
+ // Load all rules from lexicon plugins (core "chant" + lexicon-specific)
233
+ let allRules = await loadAllPluginRules(infraPath);
234
+
235
+ // Merge in any config-level plugin rules (custom .ts rule files)
236
+ if (config.plugins && config.plugins.length > 0) {
237
+ const pluginRules = await loadPluginRules(config.plugins, infraPath);
238
+ allRules = new Map([...allRules, ...pluginRules]);
239
+ }
240
+
241
+ // Get all TypeScript files
242
+ const files = getTypeScriptFiles(infraPath);
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
+ // Run lint — use per-file rules when overrides are present
260
+ let diagnostics: LintDiagnostic[];
261
+ if (options.rules) {
262
+ diagnostics = await runLint(files, options.rules, undefined, runOptions);
263
+ } else if (hasOverrides) {
264
+ diagnostics = [];
265
+ for (const file of files) {
266
+ const relativePath = file.slice(infraPath.length + 1);
267
+ const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
268
+ const fileDiagnostics = await runLint([file], fileRules, ruleOptions, runOptions);
269
+ diagnostics.push(...fileDiagnostics);
270
+ }
271
+ } else {
272
+ const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
273
+ diagnostics = await runLint(files, rules, ruleOptions, runOptions);
274
+ }
275
+
276
+ // Apply fixes if requested
277
+ 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)
288
+ const fixesByFile = new Map<string, LintFix[]>();
289
+
290
+ for (const diag of diagnostics) {
291
+ if (diag.fix && diag.fix.kind !== "write-file") {
292
+ const existing = fixesByFile.get(diag.file) ?? [];
293
+ existing.push(diag.fix);
294
+ fixesByFile.set(diag.file, existing);
295
+ }
296
+ }
297
+
298
+ // Apply fixes to each file
299
+ for (const [file, fixes] of fixesByFile) {
300
+ applyFixes(file, fixes);
301
+ }
302
+
303
+ // Re-lint after fixes to get updated diagnostics
304
+ let postFixDiagnostics: LintDiagnostic[];
305
+ if (options.rules) {
306
+ postFixDiagnostics = await runLint(files, options.rules, undefined, runOptions);
307
+ } else if (hasOverrides) {
308
+ postFixDiagnostics = [];
309
+ for (const file of files) {
310
+ const relativePath = file.slice(infraPath.length + 1);
311
+ const { rules: fileRules, ruleOptions } = getDefaultRules(infraPath, relativePath, allRules);
312
+ const fileDiagnostics = await runLint([file], fileRules, ruleOptions, runOptions);
313
+ postFixDiagnostics.push(...fileDiagnostics);
314
+ }
315
+ } else {
316
+ const { rules, ruleOptions } = getDefaultRules(infraPath, undefined, allRules);
317
+ postFixDiagnostics = await runLint(files, rules, ruleOptions, runOptions);
318
+ }
319
+ diagnostics.length = 0;
320
+ diagnostics.push(...postFixDiagnostics);
321
+ }
322
+
323
+ // Count errors and warnings
324
+ let errorCount = 0;
325
+ let warningCount = 0;
326
+
327
+ for (const diag of diagnostics) {
328
+ if (diag.severity === "error") {
329
+ errorCount++;
330
+ } else if (diag.severity === "warning") {
331
+ warningCount++;
332
+ }
333
+ }
334
+
335
+ // Format output
336
+ let output: string;
337
+ switch (options.format) {
338
+ case "json":
339
+ output = formatJson(diagnostics);
340
+ break;
341
+ case "sarif":
342
+ output = formatSarif(diagnostics);
343
+ break;
344
+ case "stylish":
345
+ default:
346
+ output = formatStylish(diagnostics);
347
+ break;
348
+ }
349
+
350
+ return {
351
+ success: errorCount === 0,
352
+ errorCount,
353
+ warningCount,
354
+ diagnostics,
355
+ output,
356
+ };
357
+ }
358
+
359
+ /**
360
+ * Print lint result to console
361
+ */
362
+ export function printLintResult(result: LintResult): void {
363
+ if (result.output) {
364
+ console.log(result.output);
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Run lint in watch mode. Runs an initial lint, then watches for changes
370
+ * and triggers re-lints. Returns a cleanup function.
371
+ */
372
+ export function lintCommandWatch(
373
+ options: LintOptions,
374
+ onReLint?: (result: LintResult) => void,
375
+ ): () => void {
376
+ const infraPath = resolve(options.path);
377
+
378
+ console.error(formatInfo(`[${formatTimestamp()}] Watching for changes...`));
379
+
380
+ // Run initial lint
381
+ lintCommand(options).then((result) => {
382
+ printLintResult(result);
383
+ onReLint?.(result);
384
+ console.error(formatInfo(`[${formatTimestamp()}] Waiting for changes...`));
385
+ });
386
+
387
+ // Watch for changes and trigger re-lints
388
+ const cleanup = watchDirectory(infraPath, async (changedFiles) => {
389
+ console.error("");
390
+ console.error(
391
+ formatInfo(
392
+ `[${formatTimestamp()}] Changes detected: ${formatChangedFiles(changedFiles, infraPath)}`,
393
+ ),
394
+ );
395
+
396
+ try {
397
+ const result = await lintCommand(options);
398
+ printLintResult(result);
399
+ onReLint?.(result);
400
+ } catch (err) {
401
+ console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
402
+ }
403
+
404
+ console.error(formatInfo(`[${formatTimestamp()}] Waiting for changes...`));
405
+ });
406
+
407
+ return cleanup;
408
+ }
@@ -0,0 +1,100 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { listCommand, type ListOptions } from "./list";
3
+ import { mkdir, rm, writeFile } from "node:fs/promises";
4
+ import { join } from "node:path";
5
+ import { tmpdir } from "node:os";
6
+
7
+ describe("listCommand", () => {
8
+ let testDir: string;
9
+
10
+ beforeEach(async () => {
11
+ testDir = join(tmpdir(), `chant-list-test-${Date.now()}-${Math.random()}`);
12
+ await mkdir(testDir, { recursive: true });
13
+ });
14
+
15
+ afterEach(async () => {
16
+ await rm(testDir, { recursive: true, force: true });
17
+ });
18
+
19
+ test("lists empty directory", async () => {
20
+ const options: ListOptions = { path: testDir, format: "text" };
21
+ const result = await listCommand(options);
22
+
23
+ expect(result.success).toBe(true);
24
+ expect(result.entities).toHaveLength(0);
25
+ expect(result.output).toContain("No entities found");
26
+ });
27
+
28
+ test("lists entities in text format", async () => {
29
+ const infraFile = join(testDir, "test.infra.ts");
30
+ await writeFile(
31
+ infraFile,
32
+ `
33
+ export const myBucket = {
34
+ lexicon: "aws",
35
+ entityType: "AWS::S3::Bucket",
36
+ [Symbol.for("chant.declarable")]: true,
37
+ };
38
+ `
39
+ );
40
+
41
+ const options: ListOptions = { path: testDir, format: "text" };
42
+ const result = await listCommand(options);
43
+
44
+ expect(result.success).toBe(true);
45
+ expect(result.entities).toHaveLength(1);
46
+ expect(result.entities[0].name).toBe("myBucket");
47
+ expect(result.entities[0].lexicon).toBe("aws");
48
+ expect(result.entities[0].entityType).toBe("AWS::S3::Bucket");
49
+ expect(result.output).toContain("myBucket");
50
+ expect(result.output).toContain("NAME");
51
+ });
52
+
53
+ test("lists entities in json format", async () => {
54
+ const infraFile = join(testDir, "test.infra.ts");
55
+ await writeFile(
56
+ infraFile,
57
+ `
58
+ export const myFunc = {
59
+ lexicon: "aws",
60
+ entityType: "AWS::Lambda::Function",
61
+ [Symbol.for("chant.declarable")]: true,
62
+ };
63
+ `
64
+ );
65
+
66
+ const options: ListOptions = { path: testDir, format: "json" };
67
+ const result = await listCommand(options);
68
+
69
+ expect(result.success).toBe(true);
70
+ const parsed = JSON.parse(result.output);
71
+ expect(parsed).toBeArrayOfSize(1);
72
+ expect(parsed[0].name).toBe("myFunc");
73
+ });
74
+
75
+ test("entities are sorted by name", async () => {
76
+ const infraFile = join(testDir, "test.infra.ts");
77
+ await writeFile(
78
+ infraFile,
79
+ `
80
+ export const zeta = {
81
+ lexicon: "aws",
82
+ entityType: "AWS::S3::Bucket",
83
+ [Symbol.for("chant.declarable")]: true,
84
+ };
85
+ export const alpha = {
86
+ lexicon: "aws",
87
+ entityType: "AWS::Lambda::Function",
88
+ [Symbol.for("chant.declarable")]: true,
89
+ };
90
+ `
91
+ );
92
+
93
+ const options: ListOptions = { path: testDir, format: "json" };
94
+ const result = await listCommand(options);
95
+
96
+ expect(result.success).toBe(true);
97
+ expect(result.entities[0].name).toBe("alpha");
98
+ expect(result.entities[1].name).toBe("zeta");
99
+ });
100
+ });
@@ -0,0 +1,108 @@
1
+ import { resolve } from "path";
2
+ import { discover } from "../../discovery/index";
3
+ import { formatSuccess, formatBold } from "../format";
4
+
5
+ /**
6
+ * List command options
7
+ */
8
+ export interface ListOptions {
9
+ /** Path to infrastructure directory */
10
+ path: string;
11
+ /** Output format */
12
+ format: "text" | "json";
13
+ }
14
+
15
+ /**
16
+ * A single entity in the list output
17
+ */
18
+ export interface ListEntity {
19
+ name: string;
20
+ lexicon: string;
21
+ entityType: string;
22
+ kind: string;
23
+ }
24
+
25
+ /**
26
+ * List command result
27
+ */
28
+ export interface ListResult {
29
+ /** Whether the list succeeded */
30
+ success: boolean;
31
+ /** Discovered entities */
32
+ entities: ListEntity[];
33
+ /** Formatted output */
34
+ output: string;
35
+ }
36
+
37
+ /**
38
+ * Execute the list command
39
+ */
40
+ export async function listCommand(options: ListOptions): Promise<ListResult> {
41
+ const infraPath = resolve(options.path);
42
+ const result = await discover(infraPath);
43
+
44
+ if (result.errors.length > 0) {
45
+ const messages = result.errors.map((e) => e.message).join("\n");
46
+ return { success: false, entities: [], output: messages };
47
+ }
48
+
49
+ // Collect entities sorted by name
50
+ const entities: ListEntity[] = [];
51
+ for (const [name, decl] of result.entities) {
52
+ entities.push({
53
+ name,
54
+ lexicon: decl.lexicon,
55
+ entityType: decl.entityType,
56
+ kind: decl.kind ?? "resource",
57
+ });
58
+ }
59
+ entities.sort((a, b) => a.name.localeCompare(b.name));
60
+
61
+ let output: string;
62
+ if (options.format === "json") {
63
+ output = JSON.stringify(entities, null, 2);
64
+ } else {
65
+ output = formatTextTable(entities);
66
+ }
67
+
68
+ return { success: true, entities, output };
69
+ }
70
+
71
+ /**
72
+ * Format entities as a text table
73
+ */
74
+ function formatTextTable(entities: ListEntity[]): string {
75
+ if (entities.length === 0) return "No entities found.";
76
+
77
+ // Calculate column widths
78
+ const headers = { name: "NAME", lexicon: "LEXICON", entityType: "TYPE", kind: "KIND" };
79
+ const nameWidth = Math.max(headers.name.length, ...entities.map((e) => e.name.length));
80
+ const lexiconWidth = Math.max(headers.lexicon.length, ...entities.map((e) => e.lexicon.length));
81
+ const typeWidth = Math.max(headers.entityType.length, ...entities.map((e) => e.entityType.length));
82
+ const kindWidth = Math.max(headers.kind.length, ...entities.map((e) => e.kind.length));
83
+
84
+ const lines: string[] = [];
85
+ lines.push(
86
+ `${headers.name.padEnd(nameWidth)} ${headers.lexicon.padEnd(lexiconWidth)} ${headers.entityType.padEnd(typeWidth)} ${headers.kind.padEnd(kindWidth)}`
87
+ );
88
+
89
+ for (const entity of entities) {
90
+ lines.push(
91
+ `${entity.name.padEnd(nameWidth)} ${entity.lexicon.padEnd(lexiconWidth)} ${entity.entityType.padEnd(typeWidth)} ${entity.kind.padEnd(kindWidth)}`
92
+ );
93
+ }
94
+
95
+ return lines.join("\n");
96
+ }
97
+
98
+ /**
99
+ * Print list result to console
100
+ */
101
+ export function printListResult(result: ListResult): void {
102
+ if (result.output) {
103
+ console.log(result.output);
104
+ }
105
+ if (result.success) {
106
+ console.error(formatSuccess(`Found ${formatBold(String(result.entities.length))} entities`));
107
+ }
108
+ }
@@ -0,0 +1,38 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { updateCommand } from "./update";
3
+ import { withTestDir } from "@intentius/chant-test-utils";
4
+ import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
5
+ import { join } from "path";
6
+
7
+ describe("updateCommand", () => {
8
+ test("fails when no lexicons configured", async () => {
9
+ await withTestDir(async (testDir) => {
10
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
11
+
12
+ const result = await updateCommand({ path: testDir });
13
+ expect(result.success).toBe(false);
14
+ expect(result.error).toContain("No lexicons configured");
15
+ });
16
+ });
17
+
18
+ test("warns when packages not found", async () => {
19
+ await withTestDir(async (testDir) => {
20
+ writeFileSync(
21
+ join(testDir, "chant.config.ts"),
22
+ `export default { lexicons: ["nonexistent"] };`,
23
+ );
24
+
25
+ const result = await updateCommand({ path: testDir });
26
+ expect(result.success).toBe(true);
27
+ expect(result.warnings.some((w) => w.includes("not found"))).toBe(true);
28
+ });
29
+ });
30
+
31
+ test("succeeds with no config file (returns error about no lexicons)", async () => {
32
+ await withTestDir(async (testDir) => {
33
+ const result = await updateCommand({ path: testDir });
34
+ expect(result.success).toBe(false);
35
+ expect(result.error).toContain("No lexicons");
36
+ });
37
+ });
38
+ });