@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,437 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { build, partitionByLexicon, detectCrossLexiconRefs } from "./build";
3
+ import { output } from "./lexicon-output";
4
+ import { AttrRef } from "./attrref";
5
+ import type { Serializer } from "./serializer";
6
+ import type { Declarable } from "./declarable";
7
+ import { DECLARABLE_MARKER } from "./declarable";
8
+ import { mkdir, writeFile, rm } from "node:fs/promises";
9
+ import { join } from "node:path";
10
+ import { tmpdir } from "node:os";
11
+
12
+ describe("build", () => {
13
+ let testDir: string;
14
+
15
+ beforeEach(async () => {
16
+ testDir = join(tmpdir(), `chant-build-test-${Date.now()}-${Math.random()}`);
17
+ await mkdir(testDir, { recursive: true });
18
+ });
19
+
20
+ afterEach(async () => {
21
+ await rm(testDir, { recursive: true, force: true });
22
+ });
23
+
24
+ test("builds empty directory successfully", async () => {
25
+ const mockSerializer: Serializer = {
26
+ name: "test",
27
+ rulePrefix: "TEST",
28
+ serialize: (_entities) => "serialized output",
29
+ };
30
+
31
+ const result = await build(testDir, [mockSerializer]);
32
+
33
+ expect(result.outputs.size).toBe(0);
34
+ expect(result.entities.size).toBe(0);
35
+ expect(result.warnings).toEqual([]);
36
+ expect(result.errors).toEqual([]);
37
+ });
38
+
39
+ test("discovers and builds entities with single lexicon", async () => {
40
+ // Create a test infrastructure file with a simple entity
41
+ const infraFile = join(testDir, "test.infra.ts");
42
+ await writeFile(
43
+ infraFile,
44
+ `
45
+ export const testEntity = {
46
+ lexicon: "test",
47
+ entityType: "TestEntity",
48
+ [Symbol.for("chant.declarable")]: true,
49
+ };
50
+ `
51
+ );
52
+
53
+ const mockSerializer: Serializer = {
54
+ name: "test",
55
+ rulePrefix: "TEST",
56
+ serialize: (_entities) => "serialized output",
57
+ };
58
+
59
+ const result = await build(testDir, [mockSerializer]);
60
+
61
+ expect(result.outputs.size).toBe(1);
62
+ expect(result.outputs.get("test")).toBe("serialized output");
63
+ expect(result.entities.size).toBe(1);
64
+ expect(result.entities.has("testEntity")).toBe(true);
65
+ expect(result.errors.length).toBe(0);
66
+ });
67
+
68
+ test("handles discovery errors", async () => {
69
+ // Create a test file with syntax error
70
+ const infraFile = join(testDir, "broken.infra.ts");
71
+ await writeFile(infraFile, "this is not valid typescript {{{");
72
+
73
+ const mockSerializer: Serializer = {
74
+ name: "test",
75
+ rulePrefix: "TEST",
76
+ serialize: (_entities) => "serialized output",
77
+ };
78
+
79
+ const result = await build(testDir, [mockSerializer]);
80
+
81
+ expect(result.entities.size).toBe(0);
82
+ expect(result.errors.length).toBeGreaterThan(0);
83
+ });
84
+
85
+ test("handles circular dependencies", async () => {
86
+ // Create test file with circular entity dependencies (not import cycles)
87
+ await writeFile(
88
+ join(testDir, "entities.ts"),
89
+ `
90
+ export const entity1 = {
91
+ lexicon: "test",
92
+ entityType: "TestEntity",
93
+ [Symbol.for("chant.declarable")]: true,
94
+ };
95
+
96
+ export const entity2 = {
97
+ lexicon: "test",
98
+ entityType: "TestEntity",
99
+ [Symbol.for("chant.declarable")]: true,
100
+ ref1: entity1,
101
+ };
102
+
103
+ // Create circular reference after declaration
104
+ entity1.ref2 = entity2;
105
+ `
106
+ );
107
+
108
+ const mockSerializer: Serializer = {
109
+ name: "test",
110
+ rulePrefix: "TEST",
111
+ serialize: (_entities) => "serialized output",
112
+ };
113
+
114
+ const result = await build(testDir, [mockSerializer]);
115
+
116
+ expect(result.outputs.get("test")).toBe("serialized output");
117
+
118
+ // Should have a BuildError for circular dependency detected by topological sort
119
+ const hasCircularError = result.errors.some(
120
+ (err) => err.name === "BuildError" && err.message.includes("Circular dependency")
121
+ );
122
+ expect(hasCircularError).toBe(true);
123
+ });
124
+
125
+ test("calls serializer.serialize()", async () => {
126
+ let serializeCalled = false;
127
+
128
+ const infraFile = join(testDir, "test.infra.ts");
129
+ await writeFile(
130
+ infraFile,
131
+ `
132
+ export const testEntity = {
133
+ lexicon: "test",
134
+ entityType: "TestEntity",
135
+ [Symbol.for("chant.declarable")]: true,
136
+ };
137
+ `
138
+ );
139
+
140
+ const mockSerializer: Serializer = {
141
+ name: "test",
142
+ rulePrefix: "TEST",
143
+ serialize: (_entities) => {
144
+ serializeCalled = true;
145
+ return "custom serialization";
146
+ },
147
+ };
148
+
149
+ const result = await build(testDir, [mockSerializer]);
150
+
151
+ expect(serializeCalled).toBe(true);
152
+ expect(result.outputs.get("test")).toBe("custom serialization");
153
+ });
154
+
155
+ test("two-lexicon project produces two outputs", async () => {
156
+ const infraFile = join(testDir, "multi.infra.ts");
157
+ await writeFile(
158
+ infraFile,
159
+ `
160
+ export const alphaEntity = {
161
+ lexicon: "alpha",
162
+ entityType: "Bucket",
163
+ [Symbol.for("chant.declarable")]: true,
164
+ };
165
+
166
+ export const betaEntity = {
167
+ lexicon: "beta",
168
+ entityType: "Storage",
169
+ [Symbol.for("chant.declarable")]: true,
170
+ };
171
+ `
172
+ );
173
+
174
+ const alphaSerializer: Serializer = {
175
+ name: "alpha",
176
+ rulePrefix: "ALPHA",
177
+ serialize: (entities) => JSON.stringify({ alpha: Array.from(entities.keys()) }),
178
+ };
179
+
180
+ const betaSerializer: Serializer = {
181
+ name: "beta",
182
+ rulePrefix: "BETA",
183
+ serialize: (entities) => JSON.stringify({ beta: Array.from(entities.keys()) }),
184
+ };
185
+
186
+ const result = await build(testDir, [alphaSerializer, betaSerializer]);
187
+
188
+ expect(result.outputs.size).toBe(2);
189
+ expect(result.outputs.has("alpha")).toBe(true);
190
+ expect(result.outputs.has("beta")).toBe(true);
191
+
192
+ const alphaOutput = JSON.parse(result.outputs.get("alpha")!);
193
+ expect(alphaOutput.alpha).toContain("alphaEntity");
194
+
195
+ const betaOutput = JSON.parse(result.outputs.get("beta")!);
196
+ expect(betaOutput.beta).toContain("betaEntity");
197
+ });
198
+
199
+ test("warns when no serializer found for a lexicon", async () => {
200
+ const infraFile = join(testDir, "test.infra.ts");
201
+ await writeFile(
202
+ infraFile,
203
+ `
204
+ export const testEntity = {
205
+ lexicon: "unknown",
206
+ entityType: "TestEntity",
207
+ [Symbol.for("chant.declarable")]: true,
208
+ };
209
+ `
210
+ );
211
+
212
+ const result = await build(testDir, []);
213
+
214
+ expect(result.warnings.length).toBeGreaterThan(0);
215
+ expect(result.warnings[0]).toContain('No serializer found for lexicon "unknown"');
216
+ });
217
+ });
218
+
219
+ describe("partitionByLexicon", () => {
220
+ test("partitions entities by lexicon", () => {
221
+ const entities = new Map<string, Declarable>([
222
+ [
223
+ "bucket",
224
+ { lexicon: "alpha", entityType: "Bucket", [DECLARABLE_MARKER]: true } as Declarable,
225
+ ],
226
+ [
227
+ "storage",
228
+ { lexicon: "beta", entityType: "Storage", [DECLARABLE_MARKER]: true } as Declarable,
229
+ ],
230
+ [
231
+ "handler",
232
+ { lexicon: "alpha", entityType: "Function", [DECLARABLE_MARKER]: true } as Declarable,
233
+ ],
234
+ ]);
235
+
236
+ const partitions = partitionByLexicon(entities);
237
+
238
+ expect(partitions.size).toBe(2);
239
+ expect(partitions.get("alpha")!.size).toBe(2);
240
+ expect(partitions.get("beta")!.size).toBe(1);
241
+ expect(partitions.get("alpha")!.has("bucket")).toBe(true);
242
+ expect(partitions.get("alpha")!.has("handler")).toBe(true);
243
+ expect(partitions.get("beta")!.has("storage")).toBe(true);
244
+ });
245
+
246
+ test("single lexicon produces one partition", () => {
247
+ const entities = new Map<string, Declarable>([
248
+ [
249
+ "bucket",
250
+ { lexicon: "alpha", entityType: "Bucket", [DECLARABLE_MARKER]: true } as Declarable,
251
+ ],
252
+ [
253
+ "handler",
254
+ { lexicon: "alpha", entityType: "Function", [DECLARABLE_MARKER]: true } as Declarable,
255
+ ],
256
+ ]);
257
+
258
+ const partitions = partitionByLexicon(entities);
259
+
260
+ expect(partitions.size).toBe(1);
261
+ expect(partitions.get("alpha")!.size).toBe(2);
262
+ });
263
+
264
+ test("empty entities produces empty partitions", () => {
265
+ const entities = new Map<string, Declarable>();
266
+ const partitions = partitionByLexicon(entities);
267
+ expect(partitions.size).toBe(0);
268
+ });
269
+
270
+ test("property-kind entities partition by their own lexicon", () => {
271
+ const entities = new Map<string, Declarable>([
272
+ [
273
+ "bucket",
274
+ { lexicon: "alpha", entityType: "Bucket", [DECLARABLE_MARKER]: true } as Declarable,
275
+ ],
276
+ [
277
+ "bucketPolicy",
278
+ {
279
+ lexicon: "alpha",
280
+ entityType: "BucketPolicy",
281
+ kind: "property",
282
+ [DECLARABLE_MARKER]: true,
283
+ } as Declarable,
284
+ ],
285
+ ]);
286
+
287
+ const partitions = partitionByLexicon(entities);
288
+
289
+ expect(partitions.size).toBe(1);
290
+ expect(partitions.get("alpha")!.size).toBe(2);
291
+ expect(partitions.get("alpha")!.has("bucketPolicy")).toBe(true);
292
+ });
293
+ });
294
+
295
+ describe("detectCrossLexiconRefs", () => {
296
+ test("cross-lexicon AttrRef is auto-detected without explicit output()", () => {
297
+ const alphaBucket = {
298
+ lexicon: "alpha",
299
+ entityType: "Alpha::Storage::Bucket",
300
+ [DECLARABLE_MARKER]: true,
301
+ } as Declarable;
302
+
303
+ const bucketEndpoint = new AttrRef(alphaBucket, "Endpoint");
304
+
305
+ const ghAction = {
306
+ lexicon: "github",
307
+ entityType: "Action",
308
+ [DECLARABLE_MARKER]: true,
309
+ props: { url: bucketEndpoint },
310
+ } as unknown as Declarable;
311
+
312
+ const entities = new Map<string, Declarable>([
313
+ ["dataBucket", alphaBucket],
314
+ ["deployAction", ghAction],
315
+ ]);
316
+
317
+ const detected = detectCrossLexiconRefs(entities);
318
+
319
+ expect(detected).toHaveLength(1);
320
+ expect(detected[0].sourceLexicon).toBe("alpha");
321
+ expect(detected[0].sourceEntity).toBe("dataBucket");
322
+ expect(detected[0].sourceAttribute).toBe("Endpoint");
323
+ expect(detected[0].outputName).toBe("dataBucket_Endpoint");
324
+ });
325
+
326
+ test("explicit output() overrides auto-detected name", () => {
327
+ const alphaBucket = {
328
+ lexicon: "alpha",
329
+ entityType: "Alpha::Storage::Bucket",
330
+ [DECLARABLE_MARKER]: true,
331
+ } as Declarable;
332
+
333
+ const bucketArn = new AttrRef(alphaBucket, "Arn");
334
+ const explicitOutput = output(bucketArn, "MyCustomArnName");
335
+
336
+ const ghAction = {
337
+ lexicon: "github",
338
+ entityType: "Action",
339
+ [DECLARABLE_MARKER]: true,
340
+ props: { arn: bucketArn, out: explicitOutput },
341
+ } as unknown as Declarable;
342
+
343
+ const entities = new Map<string, Declarable>([
344
+ ["dataBucket", alphaBucket],
345
+ ["deployAction", ghAction],
346
+ ]);
347
+
348
+ // Auto-detect finds the cross-lexicon ref
349
+ const autoDetected = detectCrossLexiconRefs(entities);
350
+ expect(autoDetected).toHaveLength(1);
351
+ expect(autoDetected[0].outputName).toBe("dataBucket_Arn");
352
+
353
+ // But when collecting explicit outputs, the explicit one is found
354
+ const { collectLexiconOutputs } = require("./build");
355
+ const explicitOutputs = collectLexiconOutputs(entities);
356
+ expect(explicitOutputs).toHaveLength(1);
357
+ expect(explicitOutputs[0].outputName).toBe("MyCustomArnName");
358
+
359
+ // Merge logic: explicit wins (same parent object + attribute)
360
+ const explicitRefs = explicitOutputs.map((o: { _sourceParent: WeakRef<object>; sourceAttribute: string }) => ({
361
+ parent: o._sourceParent.deref(),
362
+ attribute: o.sourceAttribute,
363
+ }));
364
+ const merged = [
365
+ ...explicitOutputs,
366
+ ...autoDetected.filter((auto) => {
367
+ const autoParent = auto._sourceParent.deref();
368
+ return !explicitRefs.some(
369
+ (e: { parent: object | undefined; attribute: string }) =>
370
+ e.parent === autoParent && e.attribute === auto.sourceAttribute
371
+ );
372
+ }),
373
+ ];
374
+
375
+ expect(merged).toHaveLength(1);
376
+ expect(merged[0].outputName).toBe("MyCustomArnName");
377
+ });
378
+
379
+ test("same-lexicon AttrRef is NOT auto-detected", () => {
380
+ const alphaBucket = {
381
+ lexicon: "alpha",
382
+ entityType: "Alpha::Storage::Bucket",
383
+ [DECLARABLE_MARKER]: true,
384
+ } as Declarable;
385
+
386
+ const bucketArn = new AttrRef(alphaBucket, "Arn");
387
+
388
+ const alphaFunction = {
389
+ lexicon: "alpha",
390
+ entityType: "Alpha::Compute::Function",
391
+ [DECLARABLE_MARKER]: true,
392
+ props: { bucketArn },
393
+ } as unknown as Declarable;
394
+
395
+ const entities = new Map<string, Declarable>([
396
+ ["dataBucket", alphaBucket],
397
+ ["handler", alphaFunction],
398
+ ]);
399
+
400
+ const detected = detectCrossLexiconRefs(entities);
401
+ expect(detected).toHaveLength(0);
402
+ });
403
+
404
+ test("deduplicates when same cross-lexicon ref appears in multiple entities", () => {
405
+ const alphaBucket = {
406
+ lexicon: "alpha",
407
+ entityType: "Alpha::Storage::Bucket",
408
+ [DECLARABLE_MARKER]: true,
409
+ } as Declarable;
410
+
411
+ const bucketEndpoint = new AttrRef(alphaBucket, "Endpoint");
412
+
413
+ const ghAction1 = {
414
+ lexicon: "github",
415
+ entityType: "Action",
416
+ [DECLARABLE_MARKER]: true,
417
+ props: { url: bucketEndpoint },
418
+ } as unknown as Declarable;
419
+
420
+ const ghAction2 = {
421
+ lexicon: "github",
422
+ entityType: "Action",
423
+ [DECLARABLE_MARKER]: true,
424
+ props: { endpoint: bucketEndpoint },
425
+ } as unknown as Declarable;
426
+
427
+ const entities = new Map<string, Declarable>([
428
+ ["dataBucket", alphaBucket],
429
+ ["action1", ghAction1],
430
+ ["action2", ghAction2],
431
+ ]);
432
+
433
+ const detected = detectCrossLexiconRefs(entities);
434
+ expect(detected).toHaveLength(1);
435
+ expect(detected[0].outputName).toBe("dataBucket_Endpoint");
436
+ });
437
+ });