@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,644 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { loadConfig, DEFAULT_CONFIG } from "./config";
3
+ import { writeFileSync, mkdirSync, rmSync } from "fs";
4
+ import { join } from "path";
5
+
6
+ const TEST_DIR = join(import.meta.dir, "__test_config__");
7
+
8
+ beforeEach(() => {
9
+ // Create test directory
10
+ mkdirSync(TEST_DIR, { recursive: true });
11
+ });
12
+
13
+ afterEach(() => {
14
+ // Clean up test directory
15
+ rmSync(TEST_DIR, { recursive: true, force: true });
16
+ });
17
+
18
+ describe("loadConfig", () => {
19
+ test("returns default config when no config file exists", () => {
20
+ const config = loadConfig(TEST_DIR);
21
+
22
+ expect(config).toEqual(DEFAULT_CONFIG);
23
+ });
24
+
25
+ test("loads basic config file", () => {
26
+ const configPath = join(TEST_DIR, "chant.config.json");
27
+ writeFileSync(
28
+ configPath,
29
+ JSON.stringify({
30
+ rules: {
31
+ "test-rule": "error",
32
+ },
33
+ })
34
+ );
35
+
36
+ const config = loadConfig(TEST_DIR);
37
+
38
+ expect(config.rules).toEqual({
39
+ "test-rule": "error",
40
+ });
41
+ });
42
+
43
+ test("supports disabling rules with 'off'", () => {
44
+ const configPath = join(TEST_DIR, "chant.config.json");
45
+ writeFileSync(
46
+ configPath,
47
+ JSON.stringify({
48
+ rules: {
49
+ "test-rule": "off",
50
+ },
51
+ })
52
+ );
53
+
54
+ const config = loadConfig(TEST_DIR);
55
+
56
+ expect(config.rules?.["test-rule"]).toBe("off");
57
+ });
58
+
59
+ test("supports all severity levels", () => {
60
+ const configPath = join(TEST_DIR, "chant.config.json");
61
+ writeFileSync(
62
+ configPath,
63
+ JSON.stringify({
64
+ rules: {
65
+ "rule-1": "error",
66
+ "rule-2": "warning",
67
+ "rule-3": "info",
68
+ "rule-4": "off",
69
+ },
70
+ })
71
+ );
72
+
73
+ const config = loadConfig(TEST_DIR);
74
+
75
+ expect(config.rules?.["rule-1"]).toBe("error");
76
+ expect(config.rules?.["rule-2"]).toBe("warning");
77
+ expect(config.rules?.["rule-3"]).toBe("info");
78
+ expect(config.rules?.["rule-4"]).toBe("off");
79
+ });
80
+
81
+ test("throws error for invalid severity", () => {
82
+ const configPath = join(TEST_DIR, "chant.config.json");
83
+ writeFileSync(
84
+ configPath,
85
+ JSON.stringify({
86
+ rules: {
87
+ "test-rule": "invalid",
88
+ },
89
+ })
90
+ );
91
+
92
+ expect(() => loadConfig(TEST_DIR)).toThrow(
93
+ /rule "test-rule" has invalid severity "invalid"/
94
+ );
95
+ });
96
+
97
+ test("throws error for invalid config structure", () => {
98
+ const configPath = join(TEST_DIR, "chant.config.json");
99
+ writeFileSync(configPath, "[]"); // Array instead of object
100
+
101
+ expect(() => loadConfig(TEST_DIR)).toThrow(/must be an object/);
102
+ });
103
+
104
+ test("throws error for invalid rules type", () => {
105
+ const configPath = join(TEST_DIR, "chant.config.json");
106
+ writeFileSync(
107
+ configPath,
108
+ JSON.stringify({
109
+ rules: [],
110
+ })
111
+ );
112
+
113
+ expect(() => loadConfig(TEST_DIR)).toThrow(/rules must be an object/);
114
+ });
115
+
116
+ test("throws error for invalid JSON", () => {
117
+ const configPath = join(TEST_DIR, "chant.config.json");
118
+ writeFileSync(configPath, "{invalid json}");
119
+
120
+ expect(() => loadConfig(TEST_DIR)).toThrow(/Failed to parse config file/);
121
+ });
122
+
123
+ test("extends single config file", () => {
124
+ const baseConfigPath = join(TEST_DIR, "base.json");
125
+ writeFileSync(
126
+ baseConfigPath,
127
+ JSON.stringify({
128
+ rules: {
129
+ "rule-1": "error",
130
+ "rule-2": "warning",
131
+ },
132
+ })
133
+ );
134
+
135
+ const configPath = join(TEST_DIR, "chant.config.json");
136
+ writeFileSync(
137
+ configPath,
138
+ JSON.stringify({
139
+ extends: ["./base.json"],
140
+ rules: {
141
+ "rule-3": "info",
142
+ },
143
+ })
144
+ );
145
+
146
+ const config = loadConfig(TEST_DIR);
147
+
148
+ expect(config.rules).toEqual({
149
+ "rule-1": "error",
150
+ "rule-2": "warning",
151
+ "rule-3": "info",
152
+ });
153
+ });
154
+
155
+ test("extends multiple config files with override priority", () => {
156
+ const base1Path = join(TEST_DIR, "base1.json");
157
+ writeFileSync(
158
+ base1Path,
159
+ JSON.stringify({
160
+ rules: {
161
+ "rule-1": "error",
162
+ "rule-2": "warning",
163
+ },
164
+ })
165
+ );
166
+
167
+ const base2Path = join(TEST_DIR, "base2.json");
168
+ writeFileSync(
169
+ base2Path,
170
+ JSON.stringify({
171
+ rules: {
172
+ "rule-2": "info", // Override base1
173
+ "rule-3": "error",
174
+ },
175
+ })
176
+ );
177
+
178
+ const configPath = join(TEST_DIR, "chant.config.json");
179
+ writeFileSync(
180
+ configPath,
181
+ JSON.stringify({
182
+ extends: ["./base1.json", "./base2.json"],
183
+ rules: {
184
+ "rule-3": "warning", // Override base2
185
+ },
186
+ })
187
+ );
188
+
189
+ const config = loadConfig(TEST_DIR);
190
+
191
+ expect(config.rules).toEqual({
192
+ "rule-1": "error",
193
+ "rule-2": "info", // From base2
194
+ "rule-3": "warning", // From main config
195
+ });
196
+ });
197
+
198
+ test("throws error when extended config not found", () => {
199
+ const configPath = join(TEST_DIR, "chant.config.json");
200
+ writeFileSync(
201
+ configPath,
202
+ JSON.stringify({
203
+ extends: ["./non-existent.json"],
204
+ })
205
+ );
206
+
207
+ expect(() => loadConfig(TEST_DIR)).toThrow(/Extended config file not found/);
208
+ });
209
+
210
+ test("throws error for invalid extends type", () => {
211
+ const configPath = join(TEST_DIR, "chant.config.json");
212
+ writeFileSync(
213
+ configPath,
214
+ JSON.stringify({
215
+ extends: "single-string",
216
+ })
217
+ );
218
+
219
+ expect(() => loadConfig(TEST_DIR)).toThrow(/extends must be an array/);
220
+ });
221
+
222
+ test("throws error for non-string in extends array", () => {
223
+ const configPath = join(TEST_DIR, "chant.config.json");
224
+ writeFileSync(
225
+ configPath,
226
+ JSON.stringify({
227
+ extends: [123],
228
+ })
229
+ );
230
+
231
+ expect(() => loadConfig(TEST_DIR)).toThrow(/extends must be an array of strings/);
232
+ });
233
+
234
+ test("supports nested extends", () => {
235
+ const base1Path = join(TEST_DIR, "base1.json");
236
+ writeFileSync(
237
+ base1Path,
238
+ JSON.stringify({
239
+ rules: {
240
+ "rule-1": "error",
241
+ },
242
+ })
243
+ );
244
+
245
+ const base2Path = join(TEST_DIR, "base2.json");
246
+ writeFileSync(
247
+ base2Path,
248
+ JSON.stringify({
249
+ extends: ["./base1.json"],
250
+ rules: {
251
+ "rule-2": "warning",
252
+ },
253
+ })
254
+ );
255
+
256
+ const configPath = join(TEST_DIR, "chant.config.json");
257
+ writeFileSync(
258
+ configPath,
259
+ JSON.stringify({
260
+ extends: ["./base2.json"],
261
+ rules: {
262
+ "rule-3": "info",
263
+ },
264
+ })
265
+ );
266
+
267
+ const config = loadConfig(TEST_DIR);
268
+
269
+ expect(config.rules).toEqual({
270
+ "rule-1": "error",
271
+ "rule-2": "warning",
272
+ "rule-3": "info",
273
+ });
274
+ });
275
+
276
+ test("detects circular extends", () => {
277
+ const config1Path = join(TEST_DIR, "config1.json");
278
+ const config2Path = join(TEST_DIR, "config2.json");
279
+
280
+ writeFileSync(
281
+ config1Path,
282
+ JSON.stringify({
283
+ extends: ["./config2.json"],
284
+ })
285
+ );
286
+
287
+ writeFileSync(
288
+ config2Path,
289
+ JSON.stringify({
290
+ extends: ["./config1.json"],
291
+ })
292
+ );
293
+
294
+ const configPath = join(TEST_DIR, "chant.config.json");
295
+ writeFileSync(
296
+ configPath,
297
+ JSON.stringify({
298
+ extends: ["./config1.json"],
299
+ })
300
+ );
301
+
302
+ expect(() => loadConfig(TEST_DIR)).toThrow(/Circular extends detected/);
303
+ });
304
+
305
+ test("handles empty config file", () => {
306
+ const configPath = join(TEST_DIR, "chant.config.json");
307
+ writeFileSync(configPath, "{}");
308
+
309
+ const config = loadConfig(TEST_DIR);
310
+
311
+ expect(config.rules).toEqual({});
312
+ });
313
+
314
+ test("handles config with empty rules object", () => {
315
+ const configPath = join(TEST_DIR, "chant.config.json");
316
+ writeFileSync(
317
+ configPath,
318
+ JSON.stringify({
319
+ rules: {},
320
+ })
321
+ );
322
+
323
+ const config = loadConfig(TEST_DIR);
324
+
325
+ expect(config.rules).toEqual({});
326
+ });
327
+
328
+ test("handles config with empty extends array", () => {
329
+ const configPath = join(TEST_DIR, "chant.config.json");
330
+ writeFileSync(
331
+ configPath,
332
+ JSON.stringify({
333
+ extends: [],
334
+ rules: {
335
+ "test-rule": "error",
336
+ },
337
+ })
338
+ );
339
+
340
+ const config = loadConfig(TEST_DIR);
341
+
342
+ expect(config.rules).toEqual({
343
+ "test-rule": "error",
344
+ });
345
+ });
346
+
347
+ test("allows rule severity changes from extended config", () => {
348
+ const basePath = join(TEST_DIR, "base.json");
349
+ writeFileSync(
350
+ basePath,
351
+ JSON.stringify({
352
+ rules: {
353
+ "test-rule": "error",
354
+ },
355
+ })
356
+ );
357
+
358
+ const configPath = join(TEST_DIR, "chant.config.json");
359
+ writeFileSync(
360
+ configPath,
361
+ JSON.stringify({
362
+ extends: ["./base.json"],
363
+ rules: {
364
+ "test-rule": "warning",
365
+ },
366
+ })
367
+ );
368
+
369
+ const config = loadConfig(TEST_DIR);
370
+
371
+ expect(config.rules?.["test-rule"]).toBe("warning");
372
+ });
373
+
374
+ test("allows disabling rules from extended config", () => {
375
+ const basePath = join(TEST_DIR, "base.json");
376
+ writeFileSync(
377
+ basePath,
378
+ JSON.stringify({
379
+ rules: {
380
+ "test-rule": "error",
381
+ },
382
+ })
383
+ );
384
+
385
+ const configPath = join(TEST_DIR, "chant.config.json");
386
+ writeFileSync(
387
+ configPath,
388
+ JSON.stringify({
389
+ extends: ["./base.json"],
390
+ rules: {
391
+ "test-rule": "off",
392
+ },
393
+ })
394
+ );
395
+
396
+ const config = loadConfig(TEST_DIR);
397
+
398
+ expect(config.rules?.["test-rule"]).toBe("off");
399
+ });
400
+
401
+ test("default config uses strict preset severities", () => {
402
+ expect(DEFAULT_CONFIG.rules).toEqual({
403
+ COR001: "error",
404
+ COR002: "error",
405
+ COR003: "warning",
406
+ COR004: "warning",
407
+ COR005: "warning",
408
+ COR006: "error",
409
+ COR007: "warning",
410
+ COR008: "error",
411
+ COR009: "warning",
412
+ COR010: "warning",
413
+ COR011: "error",
414
+ COR012: "warning",
415
+ COR013: "info",
416
+ COR014: "warning",
417
+ COR015: "warning",
418
+ });
419
+ });
420
+
421
+ test("no config file returns strict preset defaults", () => {
422
+ const config = loadConfig(TEST_DIR);
423
+
424
+ expect(config.rules?.["COR001"]).toBe("error");
425
+ expect(config.rules?.["COR008"]).toBe("error");
426
+ expect(config.rules?.["COR006"]).toBe("error");
427
+ expect(config.rules?.["COR009"]).toBe("warning");
428
+ expect(config.rules?.["COR005"]).toBe("warning");
429
+ expect(config.rules?.["COR002"]).toBe("error");
430
+ expect(config.rules?.["COR010"]).toBe("warning");
431
+ expect(config.rules?.["COR013"]).toBe("info");
432
+ });
433
+
434
+ test("extends relaxed preset via package path", () => {
435
+ const configPath = join(TEST_DIR, "chant.config.json");
436
+ writeFileSync(
437
+ configPath,
438
+ JSON.stringify({
439
+ extends: ["@intentius/chant/lint/presets/relaxed"],
440
+ })
441
+ );
442
+
443
+ const config = loadConfig(TEST_DIR);
444
+
445
+ expect(config.rules).toEqual({
446
+ COR001: "warning",
447
+ COR002: "off",
448
+ COR003: "off",
449
+ COR004: "off",
450
+ COR005: "off",
451
+ COR006: "off",
452
+ COR007: "off",
453
+ COR008: "warning",
454
+ COR009: "off",
455
+ COR010: "warning",
456
+ COR011: "warning",
457
+ COR012: "off",
458
+ COR013: "off",
459
+ COR014: "off",
460
+ COR015: "off",
461
+ });
462
+ });
463
+
464
+ test("extends strict preset via package path", () => {
465
+ const configPath = join(TEST_DIR, "chant.config.json");
466
+ writeFileSync(
467
+ configPath,
468
+ JSON.stringify({
469
+ extends: ["@intentius/chant/lint/presets/strict"],
470
+ })
471
+ );
472
+
473
+ const config = loadConfig(TEST_DIR);
474
+
475
+ expect(config.rules).toEqual({
476
+ COR001: "error",
477
+ COR002: "error",
478
+ COR003: "warning",
479
+ COR004: "warning",
480
+ COR005: "warning",
481
+ COR006: "error",
482
+ COR007: "warning",
483
+ COR008: "error",
484
+ COR009: "warning",
485
+ COR010: "warning",
486
+ COR011: "error",
487
+ COR012: "warning",
488
+ COR013: "info",
489
+ COR014: "warning",
490
+ COR015: "warning",
491
+ });
492
+ });
493
+
494
+ test("user rules override preset severities", () => {
495
+ const configPath = join(TEST_DIR, "chant.config.json");
496
+ writeFileSync(
497
+ configPath,
498
+ JSON.stringify({
499
+ extends: ["@intentius/chant/lint/presets/strict"],
500
+ rules: {
501
+ COR001: "off",
502
+ COR006: "warning",
503
+ },
504
+ })
505
+ );
506
+
507
+ const config = loadConfig(TEST_DIR);
508
+
509
+ expect(config.rules?.["COR001"]).toBe("off");
510
+ expect(config.rules?.["COR008"]).toBe("error");
511
+ expect(config.rules?.["COR006"]).toBe("warning");
512
+ });
513
+
514
+ test("throws error for unknown @intentius/chant preset", () => {
515
+ const configPath = join(TEST_DIR, "chant.config.json");
516
+ writeFileSync(
517
+ configPath,
518
+ JSON.stringify({
519
+ extends: ["@intentius/chant/lint/presets/nonexistent"],
520
+ })
521
+ );
522
+
523
+ expect(() => loadConfig(TEST_DIR)).toThrow(/Unknown preset/);
524
+ });
525
+
526
+ test("config with no plugins field works (backward compat)", () => {
527
+ const configPath = join(TEST_DIR, "chant.config.json");
528
+ writeFileSync(
529
+ configPath,
530
+ JSON.stringify({
531
+ rules: {
532
+ "test-rule": "error",
533
+ },
534
+ })
535
+ );
536
+
537
+ const config = loadConfig(TEST_DIR);
538
+
539
+ expect(config.rules).toEqual({ "test-rule": "error" });
540
+ expect(config.plugins).toBeUndefined();
541
+ });
542
+
543
+ test("config with valid plugins array is loaded", () => {
544
+ const configPath = join(TEST_DIR, "chant.config.json");
545
+ writeFileSync(
546
+ configPath,
547
+ JSON.stringify({
548
+ rules: { "test-rule": "error" },
549
+ plugins: ["./my-plugin.ts", "./another-plugin.ts"],
550
+ })
551
+ );
552
+
553
+ const config = loadConfig(TEST_DIR);
554
+
555
+ expect(config.plugins).toEqual(["./my-plugin.ts", "./another-plugin.ts"]);
556
+ });
557
+
558
+ test("throws error for non-array plugins", () => {
559
+ const configPath = join(TEST_DIR, "chant.config.json");
560
+ writeFileSync(
561
+ configPath,
562
+ JSON.stringify({
563
+ plugins: "not-an-array",
564
+ })
565
+ );
566
+
567
+ expect(() => loadConfig(TEST_DIR)).toThrow(/plugins must be an array/);
568
+ });
569
+
570
+ test("throws error for non-string in plugins array", () => {
571
+ const configPath = join(TEST_DIR, "chant.config.json");
572
+ writeFileSync(
573
+ configPath,
574
+ JSON.stringify({
575
+ plugins: ["valid-plugin", 123],
576
+ })
577
+ );
578
+
579
+ expect(() => loadConfig(TEST_DIR)).toThrow(/plugins must be an array of strings/);
580
+ });
581
+
582
+ test("plugins from extended config are not inherited", () => {
583
+ const basePath = join(TEST_DIR, "base.json");
584
+ writeFileSync(
585
+ basePath,
586
+ JSON.stringify({
587
+ rules: { "rule-1": "error" },
588
+ plugins: ["./base-plugin.ts"],
589
+ })
590
+ );
591
+
592
+ const configPath = join(TEST_DIR, "chant.config.json");
593
+ writeFileSync(
594
+ configPath,
595
+ JSON.stringify({
596
+ extends: ["./base.json"],
597
+ rules: { "rule-2": "warning" },
598
+ })
599
+ );
600
+
601
+ const config = loadConfig(TEST_DIR);
602
+
603
+ expect(config.rules).toEqual({ "rule-1": "error", "rule-2": "warning" });
604
+ expect(config.plugins).toBeUndefined();
605
+ });
606
+
607
+ test("loads lint config from chant.config.ts", () => {
608
+ const tsPath = join(TEST_DIR, "chant.config.ts");
609
+ writeFileSync(
610
+ tsPath,
611
+ `export default { lexicons: ["aws"], lint: { rules: { COR001: "off" } } };`,
612
+ );
613
+
614
+ const config = loadConfig(TEST_DIR);
615
+ expect(config.rules?.COR001).toBe("off");
616
+ });
617
+
618
+ test("prefers chant.config.ts over chant.config.json", () => {
619
+ writeFileSync(
620
+ join(TEST_DIR, "chant.config.ts"),
621
+ `export default { lint: { rules: { COR001: "off" } } };`,
622
+ );
623
+ writeFileSync(
624
+ join(TEST_DIR, "chant.config.json"),
625
+ JSON.stringify({ rules: { COR001: "error" } }),
626
+ );
627
+
628
+ const config = loadConfig(TEST_DIR);
629
+ expect(config.rules?.COR001).toBe("off");
630
+ });
631
+
632
+ test("chant.config.ts with only lexicons returns defaults for lint", () => {
633
+ // Note: Bun caches require() by path, so we use a subdirectory
634
+ const subDir = join(TEST_DIR, "no-lint-sub");
635
+ mkdirSync(subDir, { recursive: true });
636
+ writeFileSync(
637
+ join(subDir, "chant.config.ts"),
638
+ `export default { lexicons: ["aws"] };`,
639
+ );
640
+
641
+ const config = loadConfig(subDir);
642
+ expect(config).toEqual(DEFAULT_CONFIG);
643
+ });
644
+ });