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.
- package/README.md +24 -4
- package/package.json +11 -16
- package/src/api.ts +310 -0
- package/src/cli.ts +54 -0
- package/src/commands/add.ts +61 -0
- package/src/commands/compact.ts +924 -0
- package/src/commands/delete.ts +103 -0
- package/src/commands/diff.ts +209 -0
- package/src/commands/doctor.ts +586 -0
- package/src/commands/edit.ts +253 -0
- package/src/commands/init.ts +33 -0
- package/src/commands/learn.ts +170 -0
- package/src/commands/onboard.ts +362 -0
- package/src/commands/prime.ts +327 -0
- package/src/commands/prune.ts +128 -0
- package/src/commands/query.ts +177 -0
- package/src/commands/ready.ts +194 -0
- package/src/commands/record.ts +959 -0
- package/src/commands/search.ts +234 -0
- package/src/commands/setup.ts +823 -0
- package/src/commands/status.ts +83 -0
- package/src/commands/sync.ts +224 -0
- package/src/commands/update.ts +112 -0
- package/src/commands/validate.ts +107 -0
- package/src/index.ts +50 -0
- package/src/schemas/config.ts +31 -0
- package/src/schemas/index.ts +18 -0
- package/src/schemas/record-schema.ts +177 -0
- package/src/schemas/record.ts +83 -0
- package/src/utils/bm25.ts +243 -0
- package/src/utils/budget.ts +157 -0
- package/src/utils/config.ts +117 -0
- package/src/utils/expertise.ts +379 -0
- package/src/utils/format.ts +767 -0
- package/src/utils/git.ts +89 -0
- package/src/utils/index.ts +54 -0
- package/src/utils/json-output.ts +13 -0
- package/src/utils/lock.ts +82 -0
- package/src/utils/markers.ts +51 -0
- package/src/utils/scoring.ts +101 -0
- package/src/utils/version.ts +46 -0
- package/dist/cli.d.ts +0 -3
- package/dist/cli.d.ts.map +0 -1
- package/dist/cli.js +0 -50
- package/dist/cli.js.map +0 -1
- package/dist/commands/add.d.ts +0 -3
- package/dist/commands/add.d.ts.map +0 -1
- package/dist/commands/add.js +0 -47
- package/dist/commands/add.js.map +0 -1
- package/dist/commands/compact.d.ts +0 -5
- package/dist/commands/compact.d.ts.map +0 -1
- package/dist/commands/compact.js +0 -709
- package/dist/commands/compact.js.map +0 -1
- package/dist/commands/delete.d.ts +0 -3
- package/dist/commands/delete.d.ts.map +0 -1
- package/dist/commands/delete.js +0 -82
- package/dist/commands/delete.js.map +0 -1
- package/dist/commands/diff.d.ts +0 -11
- package/dist/commands/diff.d.ts.map +0 -1
- package/dist/commands/diff.js +0 -170
- package/dist/commands/diff.js.map +0 -1
- package/dist/commands/doctor.d.ts +0 -3
- package/dist/commands/doctor.d.ts.map +0 -1
- package/dist/commands/doctor.js +0 -391
- package/dist/commands/doctor.js.map +0 -1
- package/dist/commands/edit.d.ts +0 -3
- package/dist/commands/edit.d.ts.map +0 -1
- package/dist/commands/edit.js +0 -210
- package/dist/commands/edit.js.map +0 -1
- package/dist/commands/init.d.ts +0 -3
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -30
- package/dist/commands/init.js.map +0 -1
- package/dist/commands/learn.d.ts +0 -12
- package/dist/commands/learn.d.ts.map +0 -1
- package/dist/commands/learn.js +0 -130
- package/dist/commands/learn.js.map +0 -1
- package/dist/commands/onboard.d.ts +0 -10
- package/dist/commands/onboard.d.ts.map +0 -1
- package/dist/commands/onboard.js +0 -286
- package/dist/commands/onboard.js.map +0 -1
- package/dist/commands/prime.d.ts +0 -3
- package/dist/commands/prime.d.ts.map +0 -1
- package/dist/commands/prime.js +0 -242
- package/dist/commands/prime.js.map +0 -1
- package/dist/commands/prune.d.ts +0 -8
- package/dist/commands/prune.d.ts.map +0 -1
- package/dist/commands/prune.js +0 -90
- package/dist/commands/prune.js.map +0 -1
- package/dist/commands/query.d.ts +0 -3
- package/dist/commands/query.d.ts.map +0 -1
- package/dist/commands/query.js +0 -118
- package/dist/commands/query.js.map +0 -1
- package/dist/commands/ready.d.ts +0 -3
- package/dist/commands/ready.d.ts.map +0 -1
- package/dist/commands/ready.js +0 -160
- package/dist/commands/ready.js.map +0 -1
- package/dist/commands/record.d.ts +0 -13
- package/dist/commands/record.d.ts.map +0 -1
- package/dist/commands/record.js +0 -688
- package/dist/commands/record.js.map +0 -1
- package/dist/commands/search.d.ts +0 -3
- package/dist/commands/search.d.ts.map +0 -1
- package/dist/commands/search.js +0 -163
- package/dist/commands/search.js.map +0 -1
- package/dist/commands/setup.d.ts +0 -29
- package/dist/commands/setup.d.ts.map +0 -1
- package/dist/commands/setup.js +0 -548
- package/dist/commands/setup.js.map +0 -1
- package/dist/commands/status.d.ts +0 -3
- package/dist/commands/status.d.ts.map +0 -1
- package/dist/commands/status.js +0 -61
- package/dist/commands/status.js.map +0 -1
- package/dist/commands/sync.d.ts +0 -3
- package/dist/commands/sync.d.ts.map +0 -1
- package/dist/commands/sync.js +0 -176
- package/dist/commands/sync.js.map +0 -1
- package/dist/commands/update.d.ts +0 -3
- package/dist/commands/update.d.ts.map +0 -1
- package/dist/commands/update.js +0 -72
- package/dist/commands/update.js.map +0 -1
- package/dist/commands/validate.d.ts +0 -3
- package/dist/commands/validate.d.ts.map +0 -1
- package/dist/commands/validate.js +0 -86
- package/dist/commands/validate.js.map +0 -1
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -8
- package/dist/index.js.map +0 -1
- package/dist/schemas/config.d.ts +0 -17
- package/dist/schemas/config.d.ts.map +0 -1
- package/dist/schemas/config.js +0 -16
- package/dist/schemas/config.js.map +0 -1
- package/dist/schemas/index.d.ts +0 -5
- package/dist/schemas/index.d.ts.map +0 -1
- package/dist/schemas/index.js +0 -3
- package/dist/schemas/index.js.map +0 -1
- package/dist/schemas/record-schema.d.ts +0 -379
- package/dist/schemas/record-schema.d.ts.map +0 -1
- package/dist/schemas/record-schema.js +0 -148
- package/dist/schemas/record-schema.js.map +0 -1
- package/dist/schemas/record.d.ts +0 -60
- package/dist/schemas/record.d.ts.map +0 -1
- package/dist/schemas/record.js +0 -2
- package/dist/schemas/record.js.map +0 -1
- package/dist/utils/bm25.d.ts +0 -39
- package/dist/utils/bm25.d.ts.map +0 -1
- package/dist/utils/bm25.js +0 -171
- package/dist/utils/bm25.js.map +0 -1
- package/dist/utils/budget.d.ts +0 -35
- package/dist/utils/budget.d.ts.map +0 -1
- package/dist/utils/budget.js +0 -114
- package/dist/utils/budget.js.map +0 -1
- package/dist/utils/config.d.ts +0 -12
- package/dist/utils/config.d.ts.map +0 -1
- package/dist/utils/config.js +0 -89
- package/dist/utils/config.js.map +0 -1
- package/dist/utils/expertise.d.ts +0 -57
- package/dist/utils/expertise.d.ts.map +0 -1
- package/dist/utils/expertise.js +0 -264
- package/dist/utils/expertise.js.map +0 -1
- package/dist/utils/format.d.ts +0 -31
- package/dist/utils/format.d.ts.map +0 -1
- package/dist/utils/format.js +0 -556
- package/dist/utils/format.js.map +0 -1
- package/dist/utils/git.d.ts +0 -6
- package/dist/utils/git.d.ts.map +0 -1
- package/dist/utils/git.js +0 -81
- package/dist/utils/git.js.map +0 -1
- package/dist/utils/index.d.ts +0 -8
- package/dist/utils/index.d.ts.map +0 -1
- package/dist/utils/index.js +0 -8
- package/dist/utils/index.js.map +0 -1
- package/dist/utils/json-output.d.ts +0 -8
- package/dist/utils/json-output.d.ts.map +0 -1
- package/dist/utils/json-output.js +0 -7
- package/dist/utils/json-output.js.map +0 -1
- package/dist/utils/lock.d.ts +0 -6
- package/dist/utils/lock.d.ts.map +0 -1
- package/dist/utils/lock.js +0 -70
- package/dist/utils/lock.js.map +0 -1
- package/dist/utils/markers.d.ts +0 -22
- package/dist/utils/markers.d.ts.map +0 -1
- package/dist/utils/markers.js +0 -42
- package/dist/utils/markers.js.map +0 -1
- package/dist/utils/scoring.d.ts +0 -73
- package/dist/utils/scoring.d.ts.map +0 -1
- package/dist/utils/scoring.js +0 -80
- package/dist/utils/scoring.js.map +0 -1
- package/dist/utils/version.d.ts +0 -15
- package/dist/utils/version.d.ts.map +0 -1
- package/dist/utils/version.js +0 -48
- 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
|
+
}
|