@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,221 @@
1
+ import { resolve } from "path";
2
+ import { readFileSync, existsSync } from "fs";
3
+ import { build } from "../../build";
4
+ import type { Serializer } from "../../serializer";
5
+ import { formatSuccess, formatError, formatBold } from "../format";
6
+
7
+ /**
8
+ * Diff command options
9
+ */
10
+ export interface DiffOptions {
11
+ /** Path to infrastructure directory */
12
+ path: string;
13
+ /** Existing output file to diff against */
14
+ output?: string;
15
+ /** Serializers to use for serialization */
16
+ serializers: Serializer[];
17
+ }
18
+
19
+ /**
20
+ * Diff command result
21
+ */
22
+ export interface DiffResult {
23
+ /** Whether the diff succeeded (no build errors) */
24
+ success: boolean;
25
+ /** Whether there are changes between current and previous */
26
+ hasChanges: boolean;
27
+ /** Unified diff output */
28
+ diff: string;
29
+ }
30
+
31
+ /**
32
+ * Execute the diff command
33
+ */
34
+ export async function diffCommand(options: DiffOptions): Promise<DiffResult> {
35
+ const infraPath = resolve(options.path);
36
+
37
+ // Build current output
38
+ const result = await build(infraPath, options.serializers);
39
+
40
+ if (result.errors.length > 0) {
41
+ const messages = result.errors.map((e) => e.message).join("\n");
42
+ return { success: false, hasChanges: false, diff: messages };
43
+ }
44
+
45
+ // Combine lexicon outputs (sorted for determinism)
46
+ const combined: Record<string, unknown> = {};
47
+ const sortedLexiconNames = [...result.outputs.keys()].sort();
48
+ for (const lexiconName of sortedLexiconNames) {
49
+ combined[lexiconName] = JSON.parse(result.outputs.get(lexiconName)!);
50
+ }
51
+ const currentOutput = JSON.stringify(combined, sortedJsonReplacer, 2);
52
+
53
+ // Read previous output
54
+ let previousOutput = "";
55
+ if (options.output && existsSync(options.output)) {
56
+ previousOutput = readFileSync(resolve(options.output), "utf-8");
57
+ }
58
+
59
+ // Produce unified diff
60
+ const diff = unifiedDiff(previousOutput, currentOutput, options.output ?? "(none)");
61
+
62
+ return {
63
+ success: true,
64
+ hasChanges: diff.length > 0,
65
+ diff,
66
+ };
67
+ }
68
+
69
+ /**
70
+ * JSON.stringify replacer that sorts object keys for deterministic output
71
+ */
72
+ function sortedJsonReplacer(_key: string, value: unknown): unknown {
73
+ if (value && typeof value === "object" && !Array.isArray(value)) {
74
+ return Object.fromEntries(
75
+ Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b))
76
+ );
77
+ }
78
+ return value;
79
+ }
80
+
81
+ /**
82
+ * Simple line-by-line unified diff
83
+ */
84
+ function unifiedDiff(previous: string, current: string, filename: string): string {
85
+ const prevLines = previous ? previous.split("\n") : [];
86
+ const currLines = current.split("\n");
87
+
88
+ // Quick equality check
89
+ if (previous === current) return "";
90
+
91
+ const lines: string[] = [];
92
+ lines.push(`--- a/${filename}`);
93
+ lines.push(`+++ b/${filename}`);
94
+
95
+ // Simple diff: show removed then added lines using LCS-based approach
96
+ const { added, removed } = diffLines(prevLines, currLines);
97
+
98
+ if (removed.size === 0 && added.size === 0) return "";
99
+
100
+ // Build hunks
101
+ const allChangedLines = new Set<number>();
102
+ for (const i of removed) allChangedLines.add(i);
103
+
104
+ // Map current line indices to approximate previous positions
105
+ let hunkLines: string[] = [];
106
+ const contextSize = 3;
107
+
108
+ // Simple approach: output all removals then all additions with context
109
+ if (prevLines.length === 0) {
110
+ // Entirely new file
111
+ lines.push(`@@ -0,0 +1,${currLines.length} @@`);
112
+ for (const line of currLines) {
113
+ lines.push(`+${line}`);
114
+ }
115
+ } else {
116
+ // Use LCS result to produce interleaved diff
117
+ const { ops } = lcsOps(prevLines, currLines);
118
+ lines.push(`@@ -1,${prevLines.length} +1,${currLines.length} @@`);
119
+ for (const op of ops) {
120
+ lines.push(op);
121
+ }
122
+ }
123
+
124
+ return lines.join("\n");
125
+ }
126
+
127
+ /**
128
+ * Compute which lines were added and removed between two line arrays
129
+ */
130
+ function diffLines(
131
+ prev: string[],
132
+ curr: string[]
133
+ ): { added: Set<number>; removed: Set<number> } {
134
+ const prevSet = new Map<string, number[]>();
135
+ for (let i = 0; i < prev.length; i++) {
136
+ const existing = prevSet.get(prev[i]) ?? [];
137
+ existing.push(i);
138
+ prevSet.set(prev[i], existing);
139
+ }
140
+
141
+ const added = new Set<number>();
142
+ const removed = new Set<number>(prev.map((_, i) => i));
143
+
144
+ for (let i = 0; i < curr.length; i++) {
145
+ const indices = prevSet.get(curr[i]);
146
+ if (indices && indices.length > 0) {
147
+ removed.delete(indices.shift()!);
148
+ } else {
149
+ added.add(i);
150
+ }
151
+ }
152
+
153
+ return { added, removed };
154
+ }
155
+
156
+ /**
157
+ * Produce diff operations using a simple LCS approach
158
+ */
159
+ function lcsOps(prev: string[], curr: string[]): { ops: string[] } {
160
+ // For small diffs, use O(n*m) LCS; for large ones, fall back to simple
161
+ const maxSize = 10000;
162
+ if (prev.length * curr.length > maxSize) {
163
+ // Fall back to simple remove-all/add-all
164
+ const ops: string[] = [];
165
+ for (const line of prev) ops.push(`-${line}`);
166
+ for (const line of curr) ops.push(`+${line}`);
167
+ return { ops };
168
+ }
169
+
170
+ // Standard LCS DP
171
+ const m = prev.length;
172
+ const n = curr.length;
173
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
174
+
175
+ for (let i = 1; i <= m; i++) {
176
+ for (let j = 1; j <= n; j++) {
177
+ if (prev[i - 1] === curr[j - 1]) {
178
+ dp[i][j] = dp[i - 1][j - 1] + 1;
179
+ } else {
180
+ dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
181
+ }
182
+ }
183
+ }
184
+
185
+ // Backtrack to produce ops
186
+ const ops: string[] = [];
187
+ let i = m;
188
+ let j = n;
189
+ const result: string[] = [];
190
+
191
+ while (i > 0 || j > 0) {
192
+ if (i > 0 && j > 0 && prev[i - 1] === curr[j - 1]) {
193
+ result.push(` ${prev[i - 1]}`);
194
+ i--;
195
+ j--;
196
+ } else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
197
+ result.push(`+${curr[j - 1]}`);
198
+ j--;
199
+ } else {
200
+ result.push(`-${prev[i - 1]}`);
201
+ i--;
202
+ }
203
+ }
204
+
205
+ return { ops: result.reverse() };
206
+ }
207
+
208
+ /**
209
+ * Print diff result to console
210
+ */
211
+ export function printDiffResult(result: DiffResult): void {
212
+ if (!result.success) {
213
+ console.error(formatError({ message: result.diff }));
214
+ return;
215
+ }
216
+ if (result.diff) {
217
+ console.log(result.diff);
218
+ } else {
219
+ console.error(formatSuccess("No changes detected"));
220
+ }
221
+ }
@@ -0,0 +1,239 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import { doctorCommand } from "./doctor";
3
+ import { withTestDir } from "@intentius/chant-test-utils";
4
+ import { writeFileSync, mkdirSync } from "fs";
5
+ import { join } from "path";
6
+
7
+ describe("doctorCommand", () => {
8
+ test("config-exists fails when no config file present", async () => {
9
+ await withTestDir(async (testDir) => {
10
+ const report = await doctorCommand(testDir);
11
+ const check = report.checks.find((c) => c.name === "config-exists");
12
+ expect(check).toBeDefined();
13
+ expect(check!.status).toBe("fail");
14
+ expect(check!.message).toContain("No chant.config.json");
15
+ });
16
+ });
17
+
18
+ test("config-exists passes with chant.config.json", async () => {
19
+ await withTestDir(async (testDir) => {
20
+ writeFileSync(
21
+ join(testDir, "chant.config.json"),
22
+ JSON.stringify({ lexicons: ["aws"] }),
23
+ );
24
+ const report = await doctorCommand(testDir);
25
+ const check = report.checks.find((c) => c.name === "config-exists");
26
+ expect(check).toBeDefined();
27
+ expect(check!.status).toBe("pass");
28
+ });
29
+ });
30
+
31
+ test("config-exists passes with chant.config.ts", async () => {
32
+ await withTestDir(async (testDir) => {
33
+ writeFileSync(
34
+ join(testDir, "chant.config.ts"),
35
+ `export default { lexicons: ["aws"] };`,
36
+ );
37
+ const report = await doctorCommand(testDir);
38
+ const check = report.checks.find((c) => c.name === "config-exists");
39
+ expect(check).toBeDefined();
40
+ expect(check!.status).toBe("pass");
41
+ });
42
+ });
43
+
44
+ test("config-exists fails on invalid JSON", async () => {
45
+ await withTestDir(async (testDir) => {
46
+ writeFileSync(join(testDir, "chant.config.json"), "not valid json{{{");
47
+ const report = await doctorCommand(testDir);
48
+ const check = report.checks.find((c) => c.name === "config-exists");
49
+ expect(check).toBeDefined();
50
+ expect(check!.status).toBe("fail");
51
+ expect(check!.message).toContain("Config parse error");
52
+ });
53
+ });
54
+
55
+ test("core-types fails when .chant/types/core/ missing", async () => {
56
+ await withTestDir(async (testDir) => {
57
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
58
+ const report = await doctorCommand(testDir);
59
+ const check = report.checks.find((c) => c.name === "core-types");
60
+ expect(check).toBeDefined();
61
+ expect(check!.status).toBe("fail");
62
+ expect(check!.message).toContain("not found");
63
+ });
64
+ });
65
+
66
+ test("core-types passes when .chant/types/core/ has files", async () => {
67
+ await withTestDir(async (testDir) => {
68
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
69
+ const coreDir = join(testDir, ".chant", "types", "core");
70
+ mkdirSync(coreDir, { recursive: true });
71
+ writeFileSync(join(coreDir, "index.d.ts"), "export {};");
72
+ const report = await doctorCommand(testDir);
73
+ const check = report.checks.find((c) => c.name === "core-types");
74
+ expect(check).toBeDefined();
75
+ expect(check!.status).toBe("pass");
76
+ });
77
+ });
78
+
79
+ test("core-types fails when .chant/types/core/ is empty", async () => {
80
+ await withTestDir(async (testDir) => {
81
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
82
+ const coreDir = join(testDir, ".chant", "types", "core");
83
+ mkdirSync(coreDir, { recursive: true });
84
+ const report = await doctorCommand(testDir);
85
+ const check = report.checks.find((c) => c.name === "core-types");
86
+ expect(check).toBeDefined();
87
+ expect(check!.status).toBe("fail");
88
+ expect(check!.message).toContain("empty");
89
+ });
90
+ });
91
+
92
+ test("detects stale/orphaned lexicon directories", async () => {
93
+ await withTestDir(async (testDir) => {
94
+ writeFileSync(
95
+ join(testDir, "chant.config.json"),
96
+ JSON.stringify({ lexicons: ["aws"] }),
97
+ );
98
+ const typesDir = join(testDir, ".chant", "types");
99
+ mkdirSync(join(typesDir, "core"), { recursive: true });
100
+ mkdirSync(join(typesDir, "lexicon-aws"), { recursive: true });
101
+ mkdirSync(join(typesDir, "lexicon-gcp"), { recursive: true });
102
+ writeFileSync(join(typesDir, "core", "index.d.ts"), "export {};");
103
+ writeFileSync(join(typesDir, "lexicon-aws", "index.d.ts"), "export {};");
104
+ writeFileSync(join(typesDir, "lexicon-gcp", "index.d.ts"), "export {};");
105
+
106
+ const report = await doctorCommand(testDir);
107
+ const staleCheck = report.checks.find((c) => c.name === "stale-lexicon-gcp");
108
+ expect(staleCheck).toBeDefined();
109
+ expect(staleCheck!.status).toBe("warn");
110
+ expect(staleCheck!.message).toContain("Orphaned");
111
+ expect(staleCheck!.message).toContain("gcp");
112
+ });
113
+ });
114
+
115
+ test("does not flag configured lexicon directories as stale", async () => {
116
+ await withTestDir(async (testDir) => {
117
+ writeFileSync(
118
+ join(testDir, "chant.config.json"),
119
+ JSON.stringify({ lexicons: ["aws"] }),
120
+ );
121
+ const typesDir = join(testDir, ".chant", "types");
122
+ mkdirSync(join(typesDir, "core"), { recursive: true });
123
+ mkdirSync(join(typesDir, "lexicon-aws"), { recursive: true });
124
+ writeFileSync(join(typesDir, "core", "index.d.ts"), "export {};");
125
+ writeFileSync(join(typesDir, "lexicon-aws", "index.d.ts"), "export {};");
126
+
127
+ const report = await doctorCommand(testDir);
128
+ const staleChecks = report.checks.filter((c) => c.name.startsWith("stale-"));
129
+ expect(staleChecks.length).toBe(0);
130
+ });
131
+ });
132
+
133
+ test("src-directory fails when missing", async () => {
134
+ await withTestDir(async (testDir) => {
135
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
136
+ const report = await doctorCommand(testDir);
137
+ const check = report.checks.find((c) => c.name === "src-directory");
138
+ expect(check).toBeDefined();
139
+ expect(check!.status).toBe("fail");
140
+ expect(check!.message).toContain("not found");
141
+ });
142
+ });
143
+
144
+ test("src-directory passes with .ts files", async () => {
145
+ await withTestDir(async (testDir) => {
146
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
147
+ mkdirSync(join(testDir, "src"), { recursive: true });
148
+ writeFileSync(join(testDir, "src", "main.ts"), "export {};");
149
+ const report = await doctorCommand(testDir);
150
+ const check = report.checks.find((c) => c.name === "src-directory");
151
+ expect(check).toBeDefined();
152
+ expect(check!.status).toBe("pass");
153
+ });
154
+ });
155
+
156
+ test("report success is false when any check fails", async () => {
157
+ await withTestDir(async (testDir) => {
158
+ // No config, no src, no .chant — everything fails
159
+ const report = await doctorCommand(testDir);
160
+ expect(report.success).toBe(false);
161
+ });
162
+ });
163
+
164
+ test("report success is true when only warnings (no fails)", async () => {
165
+ await withTestDir(async (testDir) => {
166
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
167
+ mkdirSync(join(testDir, "src"), { recursive: true });
168
+ writeFileSync(join(testDir, "src", "main.ts"), "export {};");
169
+ const coreDir = join(testDir, ".chant", "types", "core");
170
+ mkdirSync(coreDir, { recursive: true });
171
+ writeFileSync(join(coreDir, "index.d.ts"), "export {};");
172
+ // mcp-config will be a warn, but not a fail
173
+ const report = await doctorCommand(testDir);
174
+ expect(report.success).toBe(true);
175
+ });
176
+ });
177
+
178
+ test("lexicon-docs passes when docs/ exists in a lexicon project", async () => {
179
+ await withTestDir(async (testDir) => {
180
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
181
+ mkdirSync(join(testDir, "src"), { recursive: true });
182
+ writeFileSync(join(testDir, "src", "plugin.ts"), "export {};");
183
+ mkdirSync(join(testDir, "docs"), { recursive: true });
184
+ const report = await doctorCommand(testDir);
185
+ const check = report.checks.find((c) => c.name === "lexicon-docs");
186
+ expect(check).toBeDefined();
187
+ expect(check!.status).toBe("pass");
188
+ });
189
+ });
190
+
191
+ test("lexicon-docs warns when docs/ is missing in a lexicon project", async () => {
192
+ await withTestDir(async (testDir) => {
193
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
194
+ mkdirSync(join(testDir, "src"), { recursive: true });
195
+ writeFileSync(join(testDir, "src", "plugin.ts"), "export {};");
196
+ const report = await doctorCommand(testDir);
197
+ const check = report.checks.find((c) => c.name === "lexicon-docs");
198
+ expect(check).toBeDefined();
199
+ expect(check!.status).toBe("warn");
200
+ expect(check!.message).toContain("docs/");
201
+ });
202
+ });
203
+
204
+ test("lexicon-docs check is skipped for non-lexicon projects", async () => {
205
+ await withTestDir(async (testDir) => {
206
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
207
+ mkdirSync(join(testDir, "src"), { recursive: true });
208
+ writeFileSync(join(testDir, "src", "main.ts"), "export {};");
209
+ const report = await doctorCommand(testDir);
210
+ const check = report.checks.find((c) => c.name === "lexicon-docs");
211
+ expect(check).toBeUndefined();
212
+ });
213
+ });
214
+
215
+ test("mcp-config warns when .mcp.json missing", async () => {
216
+ await withTestDir(async (testDir) => {
217
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
218
+ const report = await doctorCommand(testDir);
219
+ const check = report.checks.find((c) => c.name === "mcp-config");
220
+ expect(check).toBeDefined();
221
+ expect(check!.status).toBe("warn");
222
+ expect(check!.message).toContain("not found");
223
+ });
224
+ });
225
+
226
+ test("mcp-config passes when .mcp.json has chant entry", async () => {
227
+ await withTestDir(async (testDir) => {
228
+ writeFileSync(join(testDir, "chant.config.json"), "{}");
229
+ writeFileSync(
230
+ join(testDir, ".mcp.json"),
231
+ JSON.stringify({ mcpServers: { chant: { command: "chant" } } }),
232
+ );
233
+ const report = await doctorCommand(testDir);
234
+ const check = report.checks.find((c) => c.name === "mcp-config");
235
+ expect(check).toBeDefined();
236
+ expect(check!.status).toBe("pass");
237
+ });
238
+ });
239
+ });
@@ -0,0 +1,224 @@
1
+ import { existsSync, readFileSync, readdirSync } from "fs";
2
+ import { execSync } from "child_process";
3
+ import { join, resolve } from "path";
4
+ import { checkVersionCompatibility } from "../../lexicon-manifest";
5
+ import { debug } from "../debug";
6
+ import { loadPlugins, resolveProjectLexicons } from "../plugins";
7
+
8
+ export interface DoctorCheck {
9
+ name: string;
10
+ status: "pass" | "fail" | "warn";
11
+ message?: string;
12
+ }
13
+
14
+ export interface DoctorReport {
15
+ checks: DoctorCheck[];
16
+ success: boolean;
17
+ }
18
+
19
+ export async function doctorCommand(path: string): Promise<DoctorReport> {
20
+ const checks: DoctorCheck[] = [];
21
+ const projectPath = path || ".";
22
+
23
+ // Check 0: Bun is installed
24
+ try {
25
+ const bunVersion = execSync("bun --version", { encoding: "utf-8" }).trim();
26
+ checks.push({ name: "bun-installed", status: "pass", message: `v${bunVersion}` });
27
+ } catch (e) {
28
+ debug("bun version check failed:", e);
29
+ checks.push({ name: "bun-installed", status: "fail", message: "Bun is not installed — see https://bun.sh" });
30
+ }
31
+
32
+ // Check 1: Config exists and parses
33
+ const configPaths = [
34
+ join(projectPath, "chant.config.json"),
35
+ join(projectPath, "chant.config.ts"),
36
+ ];
37
+ let config: Record<string, unknown> | null = null;
38
+ const configFound = configPaths.find(p => existsSync(p));
39
+ if (!configFound) {
40
+ checks.push({ name: "config-exists", status: "fail", message: "No chant.config.json or chant.config.ts found" });
41
+ } else {
42
+ try {
43
+ if (configFound.endsWith(".json")) {
44
+ config = JSON.parse(readFileSync(configFound, "utf-8"));
45
+ }
46
+ checks.push({ name: "config-exists", status: "pass" });
47
+ } catch (err) {
48
+ checks.push({ name: "config-exists", status: "fail", message: `Config parse error: ${err instanceof Error ? err.message : String(err)}` });
49
+ }
50
+ }
51
+
52
+ // Check 2: src/ directory exists with .ts files
53
+ const srcDir = join(projectPath, "src");
54
+ if (!existsSync(srcDir)) {
55
+ checks.push({ name: "src-directory", status: "fail", message: "src/ directory not found" });
56
+ } else {
57
+ try {
58
+ const tsFiles = (readdirSync(srcDir, { recursive: true }) as string[]).filter(
59
+ (f) => f.endsWith(".ts")
60
+ );
61
+ if (tsFiles.length === 0) {
62
+ checks.push({ name: "src-directory", status: "warn", message: "src/ exists but contains no .ts files" });
63
+ } else {
64
+ checks.push({ name: "src-directory", status: "pass" });
65
+ }
66
+ } catch (e) {
67
+ debug("src directory read failed:", e);
68
+ checks.push({ name: "src-directory", status: "fail", message: "Cannot read src/ directory" });
69
+ }
70
+ }
71
+
72
+ // Check 3: .chant/types/core/ exists and is not empty
73
+ const coreTypesDir = join(projectPath, ".chant", "types", "core");
74
+ if (!existsSync(coreTypesDir)) {
75
+ checks.push({ name: "core-types", status: "fail", message: ".chant/types/core/ not found — run chant update" });
76
+ } else {
77
+ try {
78
+ const files = readdirSync(coreTypesDir);
79
+ if (files.length === 0) {
80
+ checks.push({ name: "core-types", status: "fail", message: ".chant/types/core/ is empty" });
81
+ } else {
82
+ checks.push({ name: "core-types", status: "pass" });
83
+ }
84
+ } catch (e) {
85
+ debug("core types directory read failed:", e);
86
+ checks.push({ name: "core-types", status: "fail", message: "Cannot read .chant/types/core/" });
87
+ }
88
+ }
89
+
90
+ // Check 4-6: Per-lexicon checks
91
+ const lexicons = (config as any)?.lexicons as string[] | undefined;
92
+ if (lexicons && Array.isArray(lexicons)) {
93
+ for (const lex of lexicons) {
94
+ const lexDir = join(projectPath, ".chant", "types", `lexicon-${lex}`);
95
+ if (!existsSync(lexDir)) {
96
+ checks.push({ name: `lexicon-${lex}-types`, status: "fail", message: `.chant/types/lexicon-${lex}/ not found — run chant update` });
97
+ } else {
98
+ const files = readdirSync(lexDir);
99
+ if (files.length === 0) {
100
+ checks.push({ name: `lexicon-${lex}-types`, status: "fail", message: `.chant/types/lexicon-${lex}/ is empty` });
101
+ } else {
102
+ checks.push({ name: `lexicon-${lex}-types`, status: "pass" });
103
+ }
104
+ }
105
+
106
+ // Check manifest version compatibility
107
+ const manifestPath = join(lexDir, "manifest.json");
108
+ if (existsSync(manifestPath)) {
109
+ try {
110
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
111
+ if (manifest.chantVersion) {
112
+ // Use a placeholder current version for now
113
+ const currentVersion = "0.1.0";
114
+ if (!checkVersionCompatibility(manifest.chantVersion, currentVersion)) {
115
+ checks.push({ name: `lexicon-${lex}-compat`, status: "warn", message: `Lexicon ${lex} requires chant ${manifest.chantVersion}` });
116
+ } else {
117
+ checks.push({ name: `lexicon-${lex}-compat`, status: "pass" });
118
+ }
119
+ }
120
+ } catch (e) {
121
+ debug(`manifest read failed for lexicon ${lex}:`, e);
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ // Check 7: No stale/orphaned lexicon directories
128
+ const typesDir = join(projectPath, ".chant", "types");
129
+ if (existsSync(typesDir)) {
130
+ try {
131
+ const dirs = readdirSync(typesDir);
132
+ for (const dir of dirs) {
133
+ if (dir === "core") continue;
134
+ if (!dir.startsWith("lexicon-")) continue;
135
+ const lexName = dir.replace("lexicon-", "");
136
+ if (lexicons && !lexicons.includes(lexName)) {
137
+ checks.push({ name: `stale-${dir}`, status: "warn", message: `Orphaned directory .chant/types/${dir}/ — lexicon "${lexName}" not in config` });
138
+ }
139
+ }
140
+ } catch (e) {
141
+ debug("types directory read failed:", e);
142
+ }
143
+ }
144
+
145
+ // Check 8: tsconfig.json has paths
146
+ const tsconfigPath = join(projectPath, "tsconfig.json");
147
+ if (existsSync(tsconfigPath)) {
148
+ try {
149
+ // Simple JSON parse — tsconfig may have comments, but we try
150
+ const raw = readFileSync(tsconfigPath, "utf-8");
151
+ // Strip single-line comments for basic parsing
152
+ const cleaned = raw.replace(/\/\/.*$/gm, "");
153
+ const tsconfig = JSON.parse(cleaned);
154
+ if (!tsconfig.compilerOptions?.paths) {
155
+ checks.push({ name: "tsconfig-paths", status: "warn", message: "tsconfig.json missing compilerOptions.paths" });
156
+ } else {
157
+ checks.push({ name: "tsconfig-paths", status: "pass" });
158
+ }
159
+ } catch (e) {
160
+ debug("tsconfig.json parse failed:", e);
161
+ checks.push({ name: "tsconfig-paths", status: "warn", message: "Could not parse tsconfig.json" });
162
+ }
163
+ }
164
+
165
+ // Check 9: .mcp.json exists and has chant entry
166
+ const mcpPath = join(projectPath, ".mcp.json");
167
+ if (!existsSync(mcpPath)) {
168
+ checks.push({ name: "mcp-config", status: "warn", message: ".mcp.json not found — run chant agent setup" });
169
+ } else {
170
+ try {
171
+ const mcp = JSON.parse(readFileSync(mcpPath, "utf-8"));
172
+ if (!mcp.mcpServers?.chant) {
173
+ checks.push({ name: "mcp-config", status: "warn", message: ".mcp.json missing mcpServers.chant entry" });
174
+ } else {
175
+ checks.push({ name: "mcp-config", status: "pass" });
176
+ }
177
+ } catch (e) {
178
+ debug(".mcp.json parse failed:", e);
179
+ checks.push({ name: "mcp-config", status: "fail", message: ".mcp.json is invalid JSON" });
180
+ }
181
+ }
182
+
183
+ // Check: Lexicon project docs/ directory
184
+ const isLexiconProject = existsSync(join(projectPath, "src", "plugin.ts"));
185
+ if (isLexiconProject) {
186
+ if (existsSync(join(projectPath, "docs"))) {
187
+ checks.push({ name: "lexicon-docs", status: "pass" });
188
+ } else {
189
+ checks.push({ name: "lexicon-docs", status: "warn", message: "docs/ directory not found — run `just docs` to generate" });
190
+ }
191
+ }
192
+
193
+ // Check: Skills installed for each plugin
194
+ try {
195
+ const lexiconNames = await resolveProjectLexicons(resolve(projectPath));
196
+ const plugins = await loadPlugins(lexiconNames);
197
+ for (const plugin of plugins) {
198
+ if (!plugin.skills) continue;
199
+ const skills = plugin.skills();
200
+ if (skills.length === 0) continue;
201
+ const skillsDir = join(projectPath, ".chant", "skills", plugin.name);
202
+ let missing = 0;
203
+ for (const skill of skills) {
204
+ if (!existsSync(join(skillsDir, `${skill.name}.md`))) {
205
+ missing++;
206
+ }
207
+ }
208
+ if (missing === 0) {
209
+ checks.push({ name: `skills-${plugin.name}`, status: "pass", message: `${skills.length} skill(s) installed` });
210
+ } else if (missing < skills.length) {
211
+ checks.push({ name: `skills-${plugin.name}`, status: "warn", message: `${missing}/${skills.length} skill(s) missing — run chant update` });
212
+ } else {
213
+ checks.push({ name: `skills-${plugin.name}`, status: "warn", message: `No skills installed — run chant update` });
214
+ }
215
+ }
216
+ } catch (e) {
217
+ debug("skills check failed:", e);
218
+ }
219
+
220
+ return {
221
+ checks,
222
+ success: checks.every(c => c.status !== "fail"),
223
+ };
224
+ }