@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
package/src/build.ts ADDED
@@ -0,0 +1,425 @@
1
+ import type { Declarable } from "./declarable";
2
+ import type { Serializer, SerializerResult } from "./serializer";
3
+ import type { DiscoveryError, BuildError } from "./errors";
4
+ import { BuildError as BuildErrorClass } from "./errors";
5
+ import { LexiconOutput, isLexiconOutput } from "./lexicon-output";
6
+ import { AttrRef } from "./attrref";
7
+ import { isChildProject, type ChildProjectInstance } from "./child-project";
8
+ import { discover } from "./discovery/index";
9
+ import { topologicalSort } from "./sort";
10
+ import { resolve } from "node:path";
11
+
12
+ /**
13
+ * Build manifest describing cross-lexicon outputs and deployment order
14
+ */
15
+ export interface BuildManifest {
16
+ lexicons: string[];
17
+ outputs: Record<
18
+ string,
19
+ { source: string; entity: string; attribute: string }
20
+ >;
21
+ deployOrder: string[];
22
+ }
23
+
24
+ /**
25
+ * Result of the build process
26
+ */
27
+ export interface BuildResult {
28
+ /** Map of lexicon name to serialized output (string or multi-file result) */
29
+ outputs: Map<string, string | SerializerResult>;
30
+ /** Map of entity name to Declarable entity */
31
+ entities: Map<string, Declarable>;
32
+ /** Array of warnings encountered during the build */
33
+ warnings: string[];
34
+ /** Array of errors encountered during discovery and build */
35
+ errors: Array<DiscoveryError | BuildError>;
36
+ /** Build manifest with cross-lexicon dependency info */
37
+ manifest: BuildManifest;
38
+ /** Number of source files processed */
39
+ sourceFileCount: number;
40
+ }
41
+
42
+ /**
43
+ * Partitions entities by their lexicon field.
44
+ * Property-kind Declarables are included in the same partition as their parent
45
+ * (they get inlined during serialization).
46
+ *
47
+ * @param entities - Map of entity name to Declarable
48
+ * @returns Map of lexicon name to Map of entity name to Declarable
49
+ */
50
+ export function partitionByLexicon(
51
+ entities: Map<string, Declarable>
52
+ ): Map<string, Map<string, Declarable>> {
53
+ const partitions = new Map<string, Map<string, Declarable>>();
54
+
55
+ for (const [name, entity] of entities) {
56
+ const lexicon = entity.lexicon;
57
+ if (!partitions.has(lexicon)) {
58
+ partitions.set(lexicon, new Map());
59
+ }
60
+ partitions.get(lexicon)!.set(name, entity);
61
+ }
62
+
63
+ return partitions;
64
+ }
65
+
66
+ /**
67
+ * Collect LexiconOutput instances from all entity property trees.
68
+ * Walks entity properties recursively to find LexiconOutput values.
69
+ */
70
+ export function collectLexiconOutputs(
71
+ entities: Map<string, Declarable>
72
+ ): LexiconOutput[] {
73
+ const outputs: LexiconOutput[] = [];
74
+ const visited = new Set<unknown>();
75
+
76
+ function walk(value: unknown): void {
77
+ if (value === null || value === undefined || typeof value !== "object") {
78
+ return;
79
+ }
80
+ if (visited.has(value)) return;
81
+ visited.add(value);
82
+
83
+ if (isLexiconOutput(value)) {
84
+ outputs.push(value);
85
+ return;
86
+ }
87
+
88
+ if (Array.isArray(value)) {
89
+ for (const item of value) {
90
+ walk(item);
91
+ }
92
+ return;
93
+ }
94
+
95
+ for (const val of Object.values(value as Record<string, unknown>)) {
96
+ walk(val);
97
+ }
98
+ }
99
+
100
+ for (const [name, entity] of entities) {
101
+ if (isLexiconOutput(entity as unknown)) {
102
+ const lexiconOutput = entity as unknown as LexiconOutput;
103
+ lexiconOutput._setSourceEntity(name);
104
+ outputs.push(lexiconOutput);
105
+ continue;
106
+ }
107
+
108
+ if ("props" in entity && typeof entity.props === "object" && entity.props !== null) {
109
+ // Set source entity name for any LexiconOutputs found in props
110
+ const prevLength = outputs.length;
111
+ walk(entity.props);
112
+ for (let i = prevLength; i < outputs.length; i++) {
113
+ if (!outputs[i].sourceEntity) {
114
+ outputs[i]._setSourceEntity(name);
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ return outputs;
121
+ }
122
+
123
+ /**
124
+ * Detect cross-lexicon AttrRefs by walking each entity's property tree.
125
+ * For each AttrRef whose parent entity belongs to a different lexicon than
126
+ * the consuming entity, auto-create a LexiconOutput.
127
+ *
128
+ * @param entities - Map of entity name to Declarable
129
+ * @returns Array of auto-detected LexiconOutput instances
130
+ */
131
+ export function detectCrossLexiconRefs(
132
+ entities: Map<string, Declarable>
133
+ ): LexiconOutput[] {
134
+ const outputs: LexiconOutput[] = [];
135
+ // Track by "sourceEntityName_attribute" to avoid duplicates
136
+ const seen = new Set<string>();
137
+
138
+ // Build a reverse lookup: object identity -> entity name
139
+ const objectToName = new Map<object, string>();
140
+ for (const [name, entity] of entities) {
141
+ objectToName.set(entity as object, name);
142
+ }
143
+
144
+ function walk(
145
+ value: unknown,
146
+ consumingLexicon: string,
147
+ visited: Set<unknown>
148
+ ): void {
149
+ if (value === null || value === undefined || typeof value !== "object") {
150
+ return;
151
+ }
152
+ if (visited.has(value)) return;
153
+ visited.add(value);
154
+
155
+ if (value instanceof AttrRef) {
156
+ const parent = value.parent.deref();
157
+ if (!parent) return;
158
+
159
+ const parentLexicon = (parent as Record<string, unknown>).lexicon;
160
+ if (typeof parentLexicon !== "string") return;
161
+
162
+ if (parentLexicon !== consumingLexicon) {
163
+ // Find the parent's entity name
164
+ const parentName = objectToName.get(parent);
165
+ if (!parentName) return;
166
+
167
+ const key = `${parentName}_${value.attribute}`;
168
+ if (!seen.has(key)) {
169
+ seen.add(key);
170
+ outputs.push(LexiconOutput.auto(value, parentName));
171
+ }
172
+ }
173
+ return;
174
+ }
175
+
176
+ // Skip LexiconOutput instances — these are explicit outputs
177
+ if (isLexiconOutput(value)) return;
178
+
179
+ if (Array.isArray(value)) {
180
+ for (const item of value) {
181
+ walk(item, consumingLexicon, visited);
182
+ }
183
+ return;
184
+ }
185
+
186
+ for (const val of Object.values(value as Record<string, unknown>)) {
187
+ walk(val, consumingLexicon, visited);
188
+ }
189
+ }
190
+
191
+ for (const [, entity] of entities) {
192
+ const visited = new Set<unknown>();
193
+ const consumingLexicon = entity.lexicon;
194
+
195
+ // Walk entity-level properties (AttrRefs could be direct properties)
196
+ for (const val of Object.values(entity as unknown as Record<string, unknown>)) {
197
+ walk(val, consumingLexicon, visited);
198
+ }
199
+
200
+ // Walk props if present
201
+ if (
202
+ "props" in entity &&
203
+ typeof entity.props === "object" &&
204
+ entity.props !== null
205
+ ) {
206
+ walk(entity.props, consumingLexicon, visited);
207
+ }
208
+ }
209
+
210
+ return outputs;
211
+ }
212
+
213
+ /**
214
+ * Compute deploy order: source lexicons before consuming lexicons.
215
+ */
216
+ function computeDeployOrder(
217
+ lexiconNames: string[],
218
+ lexiconOutputs: LexiconOutput[]
219
+ ): string[] {
220
+ // Build a dependency graph: consuming lexicons depend on source lexicons
221
+ const deps = new Map<string, Set<string>>();
222
+ for (const name of lexiconNames) {
223
+ deps.set(name, new Set());
224
+ }
225
+
226
+ for (const output of lexiconOutputs) {
227
+ // All lexicons other than the source lexicon implicitly depend on it
228
+ for (const name of lexiconNames) {
229
+ if (name !== output.sourceLexicon) {
230
+ deps.get(name)?.add(output.sourceLexicon);
231
+ }
232
+ }
233
+ }
234
+
235
+ // Simple topological sort for deploy order
236
+ const sorted: string[] = [];
237
+ const visited = new Set<string>();
238
+ const visiting = new Set<string>();
239
+
240
+ function visit(name: string): void {
241
+ if (visited.has(name)) return;
242
+ if (visiting.has(name)) return; // cycle, just skip
243
+ visiting.add(name);
244
+ for (const dep of deps.get(name) ?? []) {
245
+ visit(dep);
246
+ }
247
+ visiting.delete(name);
248
+ visited.add(name);
249
+ sorted.push(name);
250
+ }
251
+
252
+ for (const name of lexiconNames) {
253
+ visit(name);
254
+ }
255
+
256
+ return sorted;
257
+ }
258
+
259
+ /**
260
+ * Generate the build manifest
261
+ */
262
+ function generateManifest(
263
+ lexiconNames: string[],
264
+ lexiconOutputs: LexiconOutput[]
265
+ ): BuildManifest {
266
+ const outputsRecord: Record<
267
+ string,
268
+ { source: string; entity: string; attribute: string }
269
+ > = {};
270
+
271
+ for (const output of lexiconOutputs) {
272
+ outputsRecord[output.outputName] = {
273
+ source: output.sourceLexicon,
274
+ entity: output.sourceEntity,
275
+ attribute: output.sourceAttribute,
276
+ };
277
+ }
278
+
279
+ return {
280
+ lexicons: lexiconNames,
281
+ outputs: outputsRecord,
282
+ deployOrder: computeDeployOrder(lexiconNames, lexiconOutputs),
283
+ };
284
+ }
285
+
286
+ /**
287
+ * Builds a lexicon specification by discovering entities, sorting them
288
+ * topologically, and serializing them using the lexicon serializers.
289
+ *
290
+ * @param path - The directory path containing the specification files
291
+ * @param serializers - The serializers to use for serialization
292
+ * @returns BuildResult with outputs, entities, warnings, and errors
293
+ */
294
+ export async function build(
295
+ path: string,
296
+ serializers: Serializer[],
297
+ parentBuildStack?: Set<string>,
298
+ ): Promise<BuildResult> {
299
+ const warnings: string[] = [];
300
+ const errors: Array<DiscoveryError | BuildError> = [];
301
+
302
+ // Step 1: Discover entities and dependencies
303
+ const discoveryResult = await discover(path);
304
+
305
+ // Collect discovery errors
306
+ errors.push(...discoveryResult.errors);
307
+
308
+ // Step 2: Convert Map<string, Set<string>> to Record<string, string[]> for topologicalSort
309
+ const dependenciesRecord: Record<string, string[]> = {};
310
+ for (const [entityName, deps] of discoveryResult.dependencies) {
311
+ dependenciesRecord[entityName] = Array.from(deps);
312
+ }
313
+
314
+ // Step 3: Perform topological sort
315
+ try {
316
+ topologicalSort(dependenciesRecord);
317
+ } catch (error) {
318
+ // BuildError from cycle detection
319
+ if (error instanceof Error && error.name === "BuildError") {
320
+ errors.push(error as BuildError);
321
+ } else {
322
+ // Unexpected error
323
+ errors.push(
324
+ new BuildErrorClass(
325
+ "",
326
+ error instanceof Error ? error.message : String(error)
327
+ )
328
+ );
329
+ }
330
+ }
331
+
332
+ // Step 4: Recursively build child projects
333
+ const resolvedPath = resolve(path);
334
+ const buildStack = parentBuildStack
335
+ ? new Set(parentBuildStack)
336
+ : new Set<string>();
337
+ buildStack.add(resolvedPath);
338
+
339
+ for (const [name, entity] of discoveryResult.entities) {
340
+ if (isChildProject(entity)) {
341
+ const childPath = resolve(entity.projectPath);
342
+ if (buildStack.has(childPath)) {
343
+ errors.push(
344
+ new BuildErrorClass(
345
+ childPath,
346
+ `Circular nested stack: ${[...buildStack].join(" → ")} → ${childPath}`,
347
+ ),
348
+ );
349
+ continue;
350
+ }
351
+ const childResult = await build(childPath, serializers, buildStack);
352
+ entity.buildResult = childResult;
353
+ if (childResult.errors.length > 0) {
354
+ for (const err of childResult.errors) {
355
+ errors.push(err);
356
+ }
357
+ }
358
+ }
359
+ }
360
+
361
+ // Step 5: Partition entities by lexicon
362
+ const partitions = partitionByLexicon(discoveryResult.entities);
363
+
364
+ // Build a serializer lookup by name
365
+ const serializersByName = new Map<string, Serializer>();
366
+ for (const serializer of serializers) {
367
+ serializersByName.set(serializer.name, serializer);
368
+ }
369
+
370
+ // Step 6: Collect explicit LexiconOutputs from all entities
371
+ const explicitOutputs = collectLexiconOutputs(discoveryResult.entities);
372
+
373
+ // Step 6b: Auto-detect cross-lexicon AttrRefs
374
+ const autoOutputs = detectCrossLexiconRefs(discoveryResult.entities);
375
+
376
+ // Merge: explicit outputs take precedence over auto-detected ones.
377
+ // Match by parent object identity + attribute to detect collisions.
378
+ const explicitRefs = explicitOutputs.map((o) => ({
379
+ parent: o._sourceParent.deref(),
380
+ attribute: o.sourceAttribute,
381
+ }));
382
+ const lexiconOutputs = [
383
+ ...explicitOutputs,
384
+ ...autoOutputs.filter((auto) => {
385
+ const autoParent = auto._sourceParent.deref();
386
+ return !explicitRefs.some(
387
+ (e) => e.parent === autoParent && e.attribute === auto.sourceAttribute
388
+ );
389
+ }),
390
+ ];
391
+
392
+ // Group outputs by source lexicon
393
+ const outputsByLexicon = new Map<string, LexiconOutput[]>();
394
+ for (const output of lexiconOutputs) {
395
+ if (!outputsByLexicon.has(output.sourceLexicon)) {
396
+ outputsByLexicon.set(output.sourceLexicon, []);
397
+ }
398
+ outputsByLexicon.get(output.sourceLexicon)!.push(output);
399
+ }
400
+
401
+ // Step 7: Serialize each lexicon's entities
402
+ const outputs = new Map<string, string | SerializerResult>();
403
+ for (const [lexiconName, lexiconEntities] of partitions) {
404
+ const serializer = serializersByName.get(lexiconName);
405
+ if (serializer) {
406
+ const lexiconLexiconOutputs = outputsByLexicon.get(lexiconName) ?? [];
407
+ outputs.set(lexiconName, serializer.serialize(lexiconEntities, lexiconLexiconOutputs));
408
+ } else {
409
+ warnings.push(`No serializer found for lexicon "${lexiconName}"`);
410
+ }
411
+ }
412
+
413
+ // Step 8: Generate manifest
414
+ const lexiconNames = Array.from(partitions.keys());
415
+ const manifest = generateManifest(lexiconNames, lexiconOutputs);
416
+
417
+ return {
418
+ outputs,
419
+ entities: discoveryResult.entities,
420
+ warnings,
421
+ errors,
422
+ manifest,
423
+ sourceFileCount: discoveryResult.sourceFiles.length,
424
+ };
425
+ }