mulch-cli 0.4.3 → 0.6.0

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 (193) hide show
  1. package/README.md +24 -4
  2. package/package.json +11 -16
  3. package/src/api.ts +310 -0
  4. package/src/cli.ts +54 -0
  5. package/src/commands/add.ts +61 -0
  6. package/src/commands/compact.ts +924 -0
  7. package/src/commands/delete.ts +103 -0
  8. package/src/commands/diff.ts +209 -0
  9. package/src/commands/doctor.ts +586 -0
  10. package/src/commands/edit.ts +253 -0
  11. package/src/commands/init.ts +33 -0
  12. package/src/commands/learn.ts +170 -0
  13. package/src/commands/onboard.ts +362 -0
  14. package/src/commands/prime.ts +327 -0
  15. package/src/commands/prune.ts +128 -0
  16. package/src/commands/query.ts +177 -0
  17. package/src/commands/ready.ts +194 -0
  18. package/src/commands/record.ts +959 -0
  19. package/src/commands/search.ts +234 -0
  20. package/src/commands/setup.ts +823 -0
  21. package/src/commands/status.ts +83 -0
  22. package/src/commands/sync.ts +224 -0
  23. package/src/commands/update.ts +112 -0
  24. package/src/commands/validate.ts +107 -0
  25. package/src/index.ts +50 -0
  26. package/src/schemas/config.ts +31 -0
  27. package/src/schemas/index.ts +18 -0
  28. package/src/schemas/record-schema.ts +177 -0
  29. package/src/schemas/record.ts +83 -0
  30. package/src/utils/bm25.ts +243 -0
  31. package/src/utils/budget.ts +157 -0
  32. package/src/utils/config.ts +117 -0
  33. package/src/utils/expertise.ts +379 -0
  34. package/src/utils/format.ts +767 -0
  35. package/src/utils/git.ts +89 -0
  36. package/src/utils/index.ts +54 -0
  37. package/src/utils/json-output.ts +13 -0
  38. package/src/utils/lock.ts +82 -0
  39. package/src/utils/markers.ts +51 -0
  40. package/src/utils/scoring.ts +101 -0
  41. package/src/utils/version.ts +46 -0
  42. package/dist/cli.d.ts +0 -3
  43. package/dist/cli.d.ts.map +0 -1
  44. package/dist/cli.js +0 -50
  45. package/dist/cli.js.map +0 -1
  46. package/dist/commands/add.d.ts +0 -3
  47. package/dist/commands/add.d.ts.map +0 -1
  48. package/dist/commands/add.js +0 -47
  49. package/dist/commands/add.js.map +0 -1
  50. package/dist/commands/compact.d.ts +0 -5
  51. package/dist/commands/compact.d.ts.map +0 -1
  52. package/dist/commands/compact.js +0 -709
  53. package/dist/commands/compact.js.map +0 -1
  54. package/dist/commands/delete.d.ts +0 -3
  55. package/dist/commands/delete.d.ts.map +0 -1
  56. package/dist/commands/delete.js +0 -82
  57. package/dist/commands/delete.js.map +0 -1
  58. package/dist/commands/diff.d.ts +0 -11
  59. package/dist/commands/diff.d.ts.map +0 -1
  60. package/dist/commands/diff.js +0 -170
  61. package/dist/commands/diff.js.map +0 -1
  62. package/dist/commands/doctor.d.ts +0 -3
  63. package/dist/commands/doctor.d.ts.map +0 -1
  64. package/dist/commands/doctor.js +0 -391
  65. package/dist/commands/doctor.js.map +0 -1
  66. package/dist/commands/edit.d.ts +0 -3
  67. package/dist/commands/edit.d.ts.map +0 -1
  68. package/dist/commands/edit.js +0 -210
  69. package/dist/commands/edit.js.map +0 -1
  70. package/dist/commands/init.d.ts +0 -3
  71. package/dist/commands/init.d.ts.map +0 -1
  72. package/dist/commands/init.js +0 -30
  73. package/dist/commands/init.js.map +0 -1
  74. package/dist/commands/learn.d.ts +0 -12
  75. package/dist/commands/learn.d.ts.map +0 -1
  76. package/dist/commands/learn.js +0 -130
  77. package/dist/commands/learn.js.map +0 -1
  78. package/dist/commands/onboard.d.ts +0 -10
  79. package/dist/commands/onboard.d.ts.map +0 -1
  80. package/dist/commands/onboard.js +0 -286
  81. package/dist/commands/onboard.js.map +0 -1
  82. package/dist/commands/prime.d.ts +0 -3
  83. package/dist/commands/prime.d.ts.map +0 -1
  84. package/dist/commands/prime.js +0 -242
  85. package/dist/commands/prime.js.map +0 -1
  86. package/dist/commands/prune.d.ts +0 -8
  87. package/dist/commands/prune.d.ts.map +0 -1
  88. package/dist/commands/prune.js +0 -90
  89. package/dist/commands/prune.js.map +0 -1
  90. package/dist/commands/query.d.ts +0 -3
  91. package/dist/commands/query.d.ts.map +0 -1
  92. package/dist/commands/query.js +0 -118
  93. package/dist/commands/query.js.map +0 -1
  94. package/dist/commands/ready.d.ts +0 -3
  95. package/dist/commands/ready.d.ts.map +0 -1
  96. package/dist/commands/ready.js +0 -160
  97. package/dist/commands/ready.js.map +0 -1
  98. package/dist/commands/record.d.ts +0 -13
  99. package/dist/commands/record.d.ts.map +0 -1
  100. package/dist/commands/record.js +0 -688
  101. package/dist/commands/record.js.map +0 -1
  102. package/dist/commands/search.d.ts +0 -3
  103. package/dist/commands/search.d.ts.map +0 -1
  104. package/dist/commands/search.js +0 -163
  105. package/dist/commands/search.js.map +0 -1
  106. package/dist/commands/setup.d.ts +0 -29
  107. package/dist/commands/setup.d.ts.map +0 -1
  108. package/dist/commands/setup.js +0 -548
  109. package/dist/commands/setup.js.map +0 -1
  110. package/dist/commands/status.d.ts +0 -3
  111. package/dist/commands/status.d.ts.map +0 -1
  112. package/dist/commands/status.js +0 -61
  113. package/dist/commands/status.js.map +0 -1
  114. package/dist/commands/sync.d.ts +0 -3
  115. package/dist/commands/sync.d.ts.map +0 -1
  116. package/dist/commands/sync.js +0 -176
  117. package/dist/commands/sync.js.map +0 -1
  118. package/dist/commands/update.d.ts +0 -3
  119. package/dist/commands/update.d.ts.map +0 -1
  120. package/dist/commands/update.js +0 -72
  121. package/dist/commands/update.js.map +0 -1
  122. package/dist/commands/validate.d.ts +0 -3
  123. package/dist/commands/validate.d.ts.map +0 -1
  124. package/dist/commands/validate.js +0 -86
  125. package/dist/commands/validate.js.map +0 -1
  126. package/dist/index.d.ts +0 -7
  127. package/dist/index.d.ts.map +0 -1
  128. package/dist/index.js +0 -8
  129. package/dist/index.js.map +0 -1
  130. package/dist/schemas/config.d.ts +0 -17
  131. package/dist/schemas/config.d.ts.map +0 -1
  132. package/dist/schemas/config.js +0 -16
  133. package/dist/schemas/config.js.map +0 -1
  134. package/dist/schemas/index.d.ts +0 -5
  135. package/dist/schemas/index.d.ts.map +0 -1
  136. package/dist/schemas/index.js +0 -3
  137. package/dist/schemas/index.js.map +0 -1
  138. package/dist/schemas/record-schema.d.ts +0 -379
  139. package/dist/schemas/record-schema.d.ts.map +0 -1
  140. package/dist/schemas/record-schema.js +0 -148
  141. package/dist/schemas/record-schema.js.map +0 -1
  142. package/dist/schemas/record.d.ts +0 -60
  143. package/dist/schemas/record.d.ts.map +0 -1
  144. package/dist/schemas/record.js +0 -2
  145. package/dist/schemas/record.js.map +0 -1
  146. package/dist/utils/bm25.d.ts +0 -39
  147. package/dist/utils/bm25.d.ts.map +0 -1
  148. package/dist/utils/bm25.js +0 -171
  149. package/dist/utils/bm25.js.map +0 -1
  150. package/dist/utils/budget.d.ts +0 -35
  151. package/dist/utils/budget.d.ts.map +0 -1
  152. package/dist/utils/budget.js +0 -114
  153. package/dist/utils/budget.js.map +0 -1
  154. package/dist/utils/config.d.ts +0 -12
  155. package/dist/utils/config.d.ts.map +0 -1
  156. package/dist/utils/config.js +0 -89
  157. package/dist/utils/config.js.map +0 -1
  158. package/dist/utils/expertise.d.ts +0 -57
  159. package/dist/utils/expertise.d.ts.map +0 -1
  160. package/dist/utils/expertise.js +0 -264
  161. package/dist/utils/expertise.js.map +0 -1
  162. package/dist/utils/format.d.ts +0 -31
  163. package/dist/utils/format.d.ts.map +0 -1
  164. package/dist/utils/format.js +0 -556
  165. package/dist/utils/format.js.map +0 -1
  166. package/dist/utils/git.d.ts +0 -6
  167. package/dist/utils/git.d.ts.map +0 -1
  168. package/dist/utils/git.js +0 -81
  169. package/dist/utils/git.js.map +0 -1
  170. package/dist/utils/index.d.ts +0 -8
  171. package/dist/utils/index.d.ts.map +0 -1
  172. package/dist/utils/index.js +0 -8
  173. package/dist/utils/index.js.map +0 -1
  174. package/dist/utils/json-output.d.ts +0 -8
  175. package/dist/utils/json-output.d.ts.map +0 -1
  176. package/dist/utils/json-output.js +0 -7
  177. package/dist/utils/json-output.js.map +0 -1
  178. package/dist/utils/lock.d.ts +0 -6
  179. package/dist/utils/lock.d.ts.map +0 -1
  180. package/dist/utils/lock.js +0 -70
  181. package/dist/utils/lock.js.map +0 -1
  182. package/dist/utils/markers.d.ts +0 -22
  183. package/dist/utils/markers.d.ts.map +0 -1
  184. package/dist/utils/markers.js +0 -42
  185. package/dist/utils/markers.js.map +0 -1
  186. package/dist/utils/scoring.d.ts +0 -73
  187. package/dist/utils/scoring.d.ts.map +0 -1
  188. package/dist/utils/scoring.js +0 -80
  189. package/dist/utils/scoring.js.map +0 -1
  190. package/dist/utils/version.d.ts +0 -15
  191. package/dist/utils/version.d.ts.map +0 -1
  192. package/dist/utils/version.js +0 -48
  193. package/dist/utils/version.js.map +0 -1
@@ -0,0 +1,253 @@
1
+ import Ajv from "ajv";
2
+ import chalk from "chalk";
3
+ import { type Command, Option } from "commander";
4
+ import { recordSchema } from "../schemas/record-schema.ts";
5
+ import type { Classification, Outcome } from "../schemas/record.ts";
6
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
7
+ import {
8
+ readExpertiseFile,
9
+ resolveRecordId,
10
+ writeExpertiseFile,
11
+ } from "../utils/expertise.ts";
12
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
13
+ import { withFileLock } from "../utils/lock.ts";
14
+
15
+ export function registerEditCommand(program: Command): void {
16
+ program
17
+ .command("edit")
18
+ .argument("<domain>", "expertise domain")
19
+ .argument("<id>", "record ID (e.g. mx-abc123, abc123, or abc)")
20
+ .description("Edit an existing expertise record")
21
+ .addOption(
22
+ new Option(
23
+ "--classification <classification>",
24
+ "update classification",
25
+ ).choices(["foundational", "tactical", "observational"]),
26
+ )
27
+ .option("--content <content>", "update content (convention)")
28
+ .option("--name <name>", "update name (pattern)")
29
+ .option("--description <description>", "update description")
30
+ .option("--resolution <resolution>", "update resolution (failure)")
31
+ .option("--title <title>", "update title (decision)")
32
+ .option("--rationale <rationale>", "update rationale (decision)")
33
+ .option("--files <files>", "update related files (comma-separated)")
34
+ .option("--relates-to <ids>", "update linked record IDs (comma-separated)")
35
+ .option(
36
+ "--supersedes <ids>",
37
+ "update superseded record IDs (comma-separated)",
38
+ )
39
+ .addOption(
40
+ new Option("--outcome-status <status>", "set outcome status").choices([
41
+ "success",
42
+ "failure",
43
+ "partial",
44
+ ]),
45
+ )
46
+ .option("--outcome-duration <ms>", "set outcome duration in milliseconds")
47
+ .option("--outcome-test-results <text>", "set outcome test results summary")
48
+ .option("--outcome-agent <agent>", "set outcome agent name")
49
+ .action(
50
+ async (domain: string, id: string, options: Record<string, unknown>) => {
51
+ const jsonMode = program.opts().json === true;
52
+ try {
53
+ const config = await readConfig();
54
+
55
+ if (!config.domains.includes(domain)) {
56
+ if (jsonMode) {
57
+ outputJsonError(
58
+ "edit",
59
+ `Domain "${domain}" not found in config. Available domains: ${config.domains.join(", ") || "(none)"}`,
60
+ );
61
+ } else {
62
+ console.error(
63
+ chalk.red(`Error: domain "${domain}" not found in config.`),
64
+ );
65
+ console.error(
66
+ chalk.red(
67
+ `Available domains: ${config.domains.join(", ") || "(none)"}`,
68
+ ),
69
+ );
70
+ }
71
+ process.exitCode = 1;
72
+ return;
73
+ }
74
+
75
+ const filePath = getExpertisePath(domain);
76
+ await withFileLock(filePath, async () => {
77
+ const records = await readExpertiseFile(filePath);
78
+
79
+ const resolved = resolveRecordId(records, id);
80
+ if (!resolved.ok) {
81
+ if (jsonMode) {
82
+ outputJsonError("edit", resolved.error);
83
+ } else {
84
+ console.error(chalk.red(`Error: ${resolved.error}`));
85
+ }
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+ const targetIndex = resolved.index;
90
+
91
+ const record = { ...records[targetIndex] };
92
+
93
+ // Apply updates based on record type
94
+ if (options.classification) {
95
+ record.classification = options.classification as Classification;
96
+ }
97
+ if (typeof options.relatesTo === "string") {
98
+ record.relates_to = options.relatesTo
99
+ .split(",")
100
+ .map((id: string) => id.trim())
101
+ .filter(Boolean);
102
+ }
103
+ if (typeof options.supersedes === "string") {
104
+ record.supersedes = options.supersedes
105
+ .split(",")
106
+ .map((id: string) => id.trim())
107
+ .filter(Boolean);
108
+ }
109
+ if (options.outcomeStatus) {
110
+ const o: Outcome = {
111
+ status: options.outcomeStatus as
112
+ | "success"
113
+ | "failure"
114
+ | "partial",
115
+ };
116
+ if (options.outcomeDuration !== undefined) {
117
+ o.duration = Number.parseFloat(
118
+ options.outcomeDuration as string,
119
+ );
120
+ }
121
+ if (options.outcomeTestResults) {
122
+ o.test_results = options.outcomeTestResults as string;
123
+ }
124
+ if (options.outcomeAgent) {
125
+ o.agent = options.outcomeAgent as string;
126
+ }
127
+ record.outcomes = [...(record.outcomes ?? []), o];
128
+ }
129
+
130
+ switch (record.type) {
131
+ case "convention":
132
+ if (options.content) {
133
+ record.content = options.content as string;
134
+ }
135
+ break;
136
+ case "pattern":
137
+ if (options.name) {
138
+ record.name = options.name as string;
139
+ }
140
+ if (options.description) {
141
+ record.description = options.description as string;
142
+ }
143
+ if (typeof options.files === "string") {
144
+ record.files = (options.files as string).split(",");
145
+ }
146
+ break;
147
+ case "failure":
148
+ if (options.description) {
149
+ record.description = options.description as string;
150
+ }
151
+ if (options.resolution) {
152
+ record.resolution = options.resolution as string;
153
+ }
154
+ break;
155
+ case "decision":
156
+ if (options.title) {
157
+ record.title = options.title as string;
158
+ }
159
+ if (options.rationale) {
160
+ record.rationale = options.rationale as string;
161
+ }
162
+ break;
163
+ case "reference":
164
+ if (options.name) {
165
+ record.name = options.name as string;
166
+ }
167
+ if (options.description) {
168
+ record.description = options.description as string;
169
+ }
170
+ if (typeof options.files === "string") {
171
+ record.files = (options.files as string).split(",");
172
+ }
173
+ break;
174
+ case "guide":
175
+ if (options.name) {
176
+ record.name = options.name as string;
177
+ }
178
+ if (options.description) {
179
+ record.description = options.description as string;
180
+ }
181
+ break;
182
+ }
183
+
184
+ // Validate the updated record
185
+ const ajv = new Ajv();
186
+ const validate = ajv.compile(recordSchema);
187
+ if (!validate(record)) {
188
+ const errors = (validate.errors ?? []).map(
189
+ (err) => `${err.instancePath} ${err.message}`,
190
+ );
191
+ if (jsonMode) {
192
+ outputJsonError(
193
+ "edit",
194
+ `Updated record failed schema validation: ${errors.join("; ")}`,
195
+ );
196
+ } else {
197
+ console.error(
198
+ chalk.red("Error: updated record failed schema validation:"),
199
+ );
200
+ for (const err of validate.errors ?? []) {
201
+ console.error(
202
+ chalk.red(` ${err.instancePath} ${err.message}`),
203
+ );
204
+ }
205
+ }
206
+ process.exitCode = 1;
207
+ return;
208
+ }
209
+
210
+ records[targetIndex] = record;
211
+ await writeExpertiseFile(filePath, records);
212
+
213
+ if (jsonMode) {
214
+ outputJson({
215
+ success: true,
216
+ command: "edit",
217
+ domain,
218
+ id: record.id ?? null,
219
+ type: record.type,
220
+ record,
221
+ });
222
+ } else {
223
+ console.log(
224
+ chalk.green(
225
+ `\u2714 Updated ${record.type} ${record.id ?? ""} in ${domain}`,
226
+ ),
227
+ );
228
+ }
229
+ });
230
+ } catch (err) {
231
+ if ((err as NodeJS.ErrnoException).code === "ENOENT") {
232
+ if (jsonMode) {
233
+ outputJsonError(
234
+ "edit",
235
+ "No .mulch/ directory found. Run `mulch init` first.",
236
+ );
237
+ } else {
238
+ console.error(
239
+ "Error: No .mulch/ directory found. Run `mulch init` first.",
240
+ );
241
+ }
242
+ } else {
243
+ if (jsonMode) {
244
+ outputJsonError("edit", (err as Error).message);
245
+ } else {
246
+ console.error(`Error: ${(err as Error).message}`);
247
+ }
248
+ }
249
+ process.exitCode = 1;
250
+ }
251
+ },
252
+ );
253
+ }
@@ -0,0 +1,33 @@
1
+ import { existsSync } from "node:fs";
2
+ import chalk from "chalk";
3
+ import type { Command } from "commander";
4
+ import { getMulchDir, initMulchDir } from "../utils/config.ts";
5
+ import { outputJson } from "../utils/json-output.ts";
6
+
7
+ export function registerInitCommand(program: Command): void {
8
+ program
9
+ .command("init")
10
+ .description("Initialize .mulch/ in the current project")
11
+ .action(async () => {
12
+ const jsonMode = program.opts().json === true;
13
+ const mulchDir = getMulchDir();
14
+ const alreadyExists = existsSync(mulchDir);
15
+
16
+ await initMulchDir();
17
+
18
+ if (jsonMode) {
19
+ outputJson({
20
+ success: true,
21
+ command: "init",
22
+ created: !alreadyExists,
23
+ path: mulchDir,
24
+ });
25
+ } else if (alreadyExists) {
26
+ console.log(
27
+ chalk.green("Updated .mulch/ — filled in any missing artifacts."),
28
+ );
29
+ } else {
30
+ console.log(chalk.green(`Initialized .mulch/ in ${process.cwd()}`));
31
+ }
32
+ });
33
+ }
@@ -0,0 +1,170 @@
1
+ import chalk from "chalk";
2
+ import type { Command } from "commander";
3
+ import { getExpertisePath, readConfig } from "../utils/config.ts";
4
+ import { readExpertiseFile } from "../utils/expertise.ts";
5
+ import { getChangedFiles, isGitRepo } from "../utils/git.ts";
6
+ import { outputJson, outputJsonError } from "../utils/json-output.ts";
7
+
8
+ interface DomainMatch {
9
+ domain: string;
10
+ matchedFiles: string[];
11
+ }
12
+
13
+ export async function matchFilesToDomains(
14
+ changedFiles: string[],
15
+ cwd?: string,
16
+ ): Promise<{ matches: DomainMatch[]; unmatched: string[] }> {
17
+ const config = await readConfig(cwd);
18
+ const matched = new Set<string>();
19
+ const matches: DomainMatch[] = [];
20
+
21
+ for (const domain of config.domains) {
22
+ const filePath = getExpertisePath(domain, cwd);
23
+ const records = await readExpertiseFile(filePath);
24
+
25
+ // Collect all file paths from pattern and reference records
26
+ const domainFiles = new Set<string>();
27
+ for (const record of records) {
28
+ if (
29
+ (record.type === "pattern" || record.type === "reference") &&
30
+ record.files
31
+ ) {
32
+ for (const f of record.files) {
33
+ domainFiles.add(f);
34
+ }
35
+ }
36
+ }
37
+
38
+ // Match changed files against domain file paths
39
+ const domainMatched: string[] = [];
40
+ for (const changedFile of changedFiles) {
41
+ for (const domainFile of domainFiles) {
42
+ if (
43
+ changedFile === domainFile ||
44
+ changedFile.endsWith(domainFile) ||
45
+ domainFile.endsWith(changedFile)
46
+ ) {
47
+ domainMatched.push(changedFile);
48
+ matched.add(changedFile);
49
+ break;
50
+ }
51
+ }
52
+ }
53
+
54
+ if (domainMatched.length > 0) {
55
+ matches.push({ domain, matchedFiles: domainMatched });
56
+ }
57
+ }
58
+
59
+ // Sort by match count descending
60
+ matches.sort((a, b) => b.matchedFiles.length - a.matchedFiles.length);
61
+
62
+ const unmatched = changedFiles.filter((f) => !matched.has(f));
63
+ return { matches, unmatched };
64
+ }
65
+
66
+ export function registerLearnCommand(program: Command): void {
67
+ program
68
+ .command("learn")
69
+ .description(
70
+ "Show changed files and suggest domains for recording learnings",
71
+ )
72
+ .option("--since <ref>", "git ref to diff against", "HEAD~1")
73
+ .action(async (options: { since: string }) => {
74
+ const jsonMode = program.opts().json === true;
75
+ const cwd = process.cwd();
76
+
77
+ if (!isGitRepo(cwd)) {
78
+ if (jsonMode) {
79
+ outputJsonError(
80
+ "learn",
81
+ "Not in a git repository. Run this command from within a git repository.",
82
+ );
83
+ } else {
84
+ console.error(chalk.red("Error: not in a git repository."));
85
+ }
86
+ process.exitCode = 1;
87
+ return;
88
+ }
89
+
90
+ try {
91
+ const changedFiles = getChangedFiles(cwd, options.since);
92
+
93
+ if (changedFiles.length === 0) {
94
+ if (jsonMode) {
95
+ outputJson({
96
+ success: true,
97
+ command: "learn",
98
+ changedFiles: [],
99
+ suggestedDomains: [],
100
+ unmatchedFiles: [],
101
+ message: "No changed files found",
102
+ });
103
+ } else {
104
+ console.log("No changed files found. Nothing to learn from.");
105
+ }
106
+ return;
107
+ }
108
+
109
+ const { matches, unmatched } = await matchFilesToDomains(changedFiles);
110
+
111
+ if (jsonMode) {
112
+ outputJson({
113
+ success: true,
114
+ command: "learn",
115
+ changedFiles,
116
+ suggestedDomains: matches.map((m) => ({
117
+ domain: m.domain,
118
+ matchCount: m.matchedFiles.length,
119
+ files: m.matchedFiles,
120
+ })),
121
+ unmatchedFiles: unmatched,
122
+ });
123
+ return;
124
+ }
125
+
126
+ // Plain text output
127
+ console.log(chalk.bold("\nSession learnings check\n"));
128
+
129
+ console.log(chalk.cyan(`Changed files (${changedFiles.length}):`));
130
+ for (const f of changedFiles) {
131
+ console.log(` ${f}`);
132
+ }
133
+
134
+ if (matches.length > 0) {
135
+ console.log(chalk.cyan("\nSuggested domains:"));
136
+ for (const m of matches) {
137
+ const label =
138
+ m.matchedFiles.length === 1 ? "file matches" : "files match";
139
+ console.log(
140
+ ` ${chalk.bold(m.domain)} (${m.matchedFiles.length} ${label} existing records)`,
141
+ );
142
+ }
143
+ }
144
+
145
+ if (unmatched.length > 0) {
146
+ console.log(
147
+ chalk.yellow("\nUnmatched files (no domain association):"),
148
+ );
149
+ for (const f of unmatched) {
150
+ console.log(` ${f}`);
151
+ }
152
+ }
153
+
154
+ console.log(chalk.dim("\nRecord learnings with:"));
155
+ console.log(
156
+ chalk.dim(
157
+ ' mulch record <domain> --type <type> --description "..."',
158
+ ),
159
+ );
160
+ console.log();
161
+ } catch (err) {
162
+ if (jsonMode) {
163
+ outputJsonError("learn", (err as Error).message);
164
+ } else {
165
+ console.error(`Error: ${(err as Error).message}`);
166
+ }
167
+ process.exitCode = 1;
168
+ }
169
+ });
170
+ }