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
@@ -1,709 +0,0 @@
1
- import chalk from "chalk";
2
- import { createInterface } from "node:readline";
3
- import { readConfig, getExpertisePath } from "../utils/config.js";
4
- import { readExpertiseFile, writeExpertiseFile, generateRecordId, resolveRecordId, } from "../utils/expertise.js";
5
- import { withFileLock } from "../utils/lock.js";
6
- import { recordSchema } from "../schemas/record-schema.js";
7
- import { outputJson, outputJsonError } from "../utils/json-output.js";
8
- import { getRecordSummary } from "../utils/format.js";
9
- import _Ajv from "ajv";
10
- const Ajv = _Ajv.default ?? _Ajv;
11
- function findCandidates(domain, records, now, shelfLife, minGroupSize = 3) {
12
- // Group records by type
13
- const byType = new Map();
14
- for (const r of records) {
15
- if (!byType.has(r.type)) {
16
- byType.set(r.type, []);
17
- }
18
- byType.get(r.type).push(r);
19
- }
20
- const candidates = [];
21
- for (const [type, group] of byType) {
22
- if (group.length < 2)
23
- continue;
24
- // Include groups where at least one record is stale or the group is large enough
25
- const hasStale = group.some((r) => {
26
- if (r.classification === "foundational")
27
- return false;
28
- const ageMs = now.getTime() - new Date(r.recorded_at).getTime();
29
- const ageDays = Math.floor(ageMs / (1000 * 60 * 60 * 24));
30
- if (r.classification === "tactical")
31
- return ageDays > shelfLife.tactical;
32
- if (r.classification === "observational")
33
- return ageDays > shelfLife.observational;
34
- return false;
35
- });
36
- if (hasStale || group.length >= minGroupSize) {
37
- candidates.push({
38
- domain,
39
- type,
40
- records: group.map((r) => ({
41
- id: r.id,
42
- summary: getRecordSummary(r),
43
- recorded_at: r.recorded_at,
44
- })),
45
- });
46
- }
47
- }
48
- return candidates;
49
- }
50
- function resolveRecordIds(records, identifiers) {
51
- const indices = [];
52
- for (const id of identifiers) {
53
- const result = resolveRecordId(records, id);
54
- if (!result.ok) {
55
- throw new Error(result.error);
56
- }
57
- indices.push(result.index);
58
- }
59
- return indices;
60
- }
61
- async function confirmAction(prompt) {
62
- const rl = createInterface({
63
- input: process.stdin,
64
- output: process.stdout,
65
- });
66
- return new Promise((resolve) => {
67
- rl.question(`${prompt} (y/N): `, (answer) => {
68
- rl.close();
69
- resolve(answer.toLowerCase() === "y" || answer.toLowerCase() === "yes");
70
- });
71
- });
72
- }
73
- export function registerCompactCommand(program) {
74
- program
75
- .command("compact")
76
- .argument("[domain]", "expertise domain (required for --apply)")
77
- .description("Compact records: analyze candidates or apply a compaction")
78
- .option("--analyze", "show compaction candidates")
79
- .option("--apply", "apply a compaction (replace records with summary)")
80
- .option("--auto", "automatically compact all candidates")
81
- .option("--dry-run", "preview what --auto would do without writing (use with --auto)")
82
- .option("--min-group <size>", "minimum group size for auto-compaction (default: 5)", "5")
83
- .option("--max-records <count>", "maximum records to compact in one run (default: 50)", "50")
84
- .option("--yes", "skip confirmation prompts (use with --auto)")
85
- .option("--records <ids>", "comma-separated record IDs to compact")
86
- .option("--type <type>", "record type for the replacement")
87
- .option("--name <name>", "name for replacement (pattern/reference/guide)")
88
- .option("--title <title>", "title for replacement (decision)")
89
- .option("--description <description>", "description for replacement")
90
- .option("--content <content>", "content for replacement (convention)")
91
- .option("--resolution <resolution>", "resolution for replacement (failure)")
92
- .option("--rationale <rationale>", "rationale for replacement (decision)")
93
- .action(async (domain, options) => {
94
- const jsonMode = program.opts().json === true;
95
- if (options.analyze) {
96
- await handleAnalyze(jsonMode, domain);
97
- }
98
- else if (options.auto) {
99
- await handleAuto(options, jsonMode, domain);
100
- }
101
- else if (options.apply) {
102
- if (!domain) {
103
- const msg = "Domain is required for --apply.";
104
- if (jsonMode) {
105
- outputJsonError("compact", msg);
106
- }
107
- else {
108
- console.error(chalk.red(`Error: ${msg}`));
109
- }
110
- process.exitCode = 1;
111
- return;
112
- }
113
- await handleApply(domain, options, jsonMode);
114
- }
115
- else {
116
- const msg = "Specify --analyze, --auto, or --apply.";
117
- if (jsonMode) {
118
- outputJsonError("compact", msg);
119
- }
120
- else {
121
- console.error(chalk.red(`Error: ${msg}`));
122
- }
123
- process.exitCode = 1;
124
- }
125
- });
126
- }
127
- async function handleAnalyze(jsonMode, domain) {
128
- const config = await readConfig();
129
- const now = new Date();
130
- const shelfLife = config.classification_defaults.shelf_life;
131
- const allCandidates = [];
132
- // Filter to specific domain if provided, otherwise check all domains
133
- const domainsToCheck = domain ? [domain] : config.domains;
134
- // Validate domain if specified
135
- if (domain && !config.domains.includes(domain)) {
136
- const msg = `Domain "${domain}" not found in config.`;
137
- if (jsonMode) {
138
- outputJsonError("compact", msg);
139
- }
140
- else {
141
- console.error(chalk.red(`Error: ${msg}`));
142
- }
143
- process.exitCode = 1;
144
- return;
145
- }
146
- for (const d of domainsToCheck) {
147
- const filePath = getExpertisePath(d);
148
- const records = await readExpertiseFile(filePath);
149
- if (records.length < 2)
150
- continue;
151
- const candidates = findCandidates(d, records, now, shelfLife);
152
- allCandidates.push(...candidates);
153
- }
154
- if (jsonMode) {
155
- outputJson({
156
- success: true,
157
- command: "compact",
158
- action: "analyze",
159
- candidates: allCandidates,
160
- });
161
- return;
162
- }
163
- if (allCandidates.length === 0) {
164
- console.log(chalk.green("No compaction candidates found."));
165
- return;
166
- }
167
- // Group candidates by domain for better organization
168
- const byDomain = new Map();
169
- for (const c of allCandidates) {
170
- if (!byDomain.has(c.domain)) {
171
- byDomain.set(c.domain, []);
172
- }
173
- byDomain.get(c.domain).push(c);
174
- }
175
- const totalGroups = allCandidates.length;
176
- const totalRecords = allCandidates.reduce((sum, c) => sum + c.records.length, 0);
177
- console.log(chalk.bold(`\nCompaction candidates:\n`));
178
- console.log(chalk.dim(`Found ${totalGroups} groups (${totalRecords} records that could be compacted)\n`));
179
- for (const [domain, candidates] of byDomain) {
180
- console.log(chalk.bold(`${domain}:`));
181
- for (const c of candidates) {
182
- console.log(` ${chalk.cyan(c.type)} (${c.records.length} records)`);
183
- for (const r of c.records.slice(0, 3)) {
184
- console.log(` ${r.id ?? "(no id)"}: ${r.summary}`);
185
- }
186
- if (c.records.length > 3) {
187
- console.log(chalk.dim(` ... and ${c.records.length - 3} more`));
188
- }
189
- }
190
- console.log();
191
- }
192
- console.log(chalk.dim("To compact manually:"));
193
- console.log(chalk.dim(" mulch compact <domain> --apply --records <ids> --type <type> [fields...]"));
194
- console.log(chalk.dim("\nTo compact automatically:"));
195
- console.log(chalk.dim(" mulch compact --auto [--dry-run]"));
196
- }
197
- async function handleAuto(options, jsonMode, domain) {
198
- const config = await readConfig();
199
- const now = new Date();
200
- const shelfLife = config.classification_defaults.shelf_life;
201
- const dryRun = options.dryRun === true;
202
- const skipConfirmation = options.yes === true;
203
- const minGroupSize = Number.parseInt(options.minGroup, 10) || 5;
204
- const maxRecords = Number.parseInt(options.maxRecords, 10) || 50;
205
- // Filter to specific domain if provided, otherwise check all domains
206
- const domainsToCheck = domain ? [domain] : config.domains;
207
- // Validate domain if specified
208
- if (domain && !config.domains.includes(domain)) {
209
- const msg = `Domain "${domain}" not found in config.`;
210
- if (jsonMode) {
211
- outputJsonError("compact", msg);
212
- }
213
- else {
214
- console.error(chalk.red(`Error: ${msg}`));
215
- }
216
- process.exitCode = 1;
217
- return;
218
- }
219
- // Collect all candidates across specified domains
220
- const allCandidates = [];
221
- for (const d of domainsToCheck) {
222
- const filePath = getExpertisePath(d);
223
- const records = await readExpertiseFile(filePath);
224
- if (records.length < 2)
225
- continue;
226
- const candidates = findCandidates(d, records, now, shelfLife, minGroupSize);
227
- for (const candidate of candidates) {
228
- allCandidates.push({ domain: d, candidate });
229
- }
230
- }
231
- if (allCandidates.length === 0) {
232
- if (jsonMode) {
233
- outputJson({
234
- success: true,
235
- command: "compact",
236
- action: dryRun ? "dry-run" : "auto",
237
- compacted: 0,
238
- results: [],
239
- });
240
- }
241
- else {
242
- console.log(chalk.green("No compaction candidates found."));
243
- }
244
- return;
245
- }
246
- // Calculate total records to compact and apply max limit
247
- let totalRecordsToCompact = 0;
248
- const candidatesToProcess = [];
249
- for (const item of allCandidates) {
250
- if (totalRecordsToCompact + item.candidate.records.length > maxRecords) {
251
- break;
252
- }
253
- candidatesToProcess.push(item);
254
- totalRecordsToCompact += item.candidate.records.length;
255
- }
256
- // Show summary
257
- if (!jsonMode && !dryRun) {
258
- console.log(chalk.bold(`\nCompaction summary:\n`));
259
- console.log(` ${candidatesToProcess.length} groups will be compacted`);
260
- console.log(` ${totalRecordsToCompact} records → ${candidatesToProcess.length} records\n`);
261
- for (const { domain, candidate } of candidatesToProcess) {
262
- console.log(chalk.cyan(`${domain}/${candidate.type}`) + ` (${candidate.records.length} records)`);
263
- for (const r of candidate.records.slice(0, 3)) {
264
- console.log(` ${r.id ?? "(no id)"}: ${r.summary}`);
265
- }
266
- if (candidate.records.length > 3) {
267
- console.log(chalk.dim(` ... and ${candidate.records.length - 3} more`));
268
- }
269
- console.log();
270
- }
271
- if (allCandidates.length > candidatesToProcess.length) {
272
- const skipped = allCandidates.length - candidatesToProcess.length;
273
- console.log(chalk.yellow(`Note: ${skipped} additional groups skipped due to --max-records limit\n`));
274
- }
275
- }
276
- // Dry-run mode: show detailed preview of what would be done
277
- if (dryRun) {
278
- if (jsonMode) {
279
- outputJson({
280
- success: true,
281
- command: "compact",
282
- action: "dry-run",
283
- wouldCompact: totalRecordsToCompact,
284
- groups: candidatesToProcess.map(({ domain, candidate }) => ({
285
- domain,
286
- type: candidate.type,
287
- count: candidate.records.length,
288
- records: candidate.records,
289
- })),
290
- });
291
- }
292
- else {
293
- console.log(chalk.bold(`\nDry-run preview:\n`));
294
- console.log(` ${candidatesToProcess.length} groups would be compacted`);
295
- console.log(` ${totalRecordsToCompact} records → ${candidatesToProcess.length} records\n`);
296
- for (const { domain, candidate } of candidatesToProcess) {
297
- console.log(chalk.cyan(`${domain}/${candidate.type}`) + ` (${candidate.records.length} records)`);
298
- for (const r of candidate.records.slice(0, 3)) {
299
- console.log(` ${r.id ?? "(no id)"}: ${r.summary}`);
300
- }
301
- if (candidate.records.length > 3) {
302
- console.log(chalk.dim(` ... and ${candidate.records.length - 3} more`));
303
- }
304
- console.log();
305
- }
306
- if (allCandidates.length > candidatesToProcess.length) {
307
- const skipped = allCandidates.length - candidatesToProcess.length;
308
- console.log(chalk.yellow(`Note: ${skipped} additional groups skipped due to --max-records limit\n`));
309
- }
310
- console.log(chalk.green(`✓ Dry-run complete. Would compact ${totalRecordsToCompact} records across ${candidatesToProcess.length} groups.`));
311
- console.log(chalk.dim(" Run without --dry-run to apply changes."));
312
- }
313
- return;
314
- }
315
- // Ask for confirmation unless --yes was passed
316
- if (!jsonMode && !skipConfirmation) {
317
- const confirmed = await confirmAction("Proceed with compaction?");
318
- if (!confirmed) {
319
- console.log(chalk.yellow("Compaction cancelled."));
320
- return;
321
- }
322
- }
323
- // Apply compaction
324
- let totalCompacted = 0;
325
- const results = [];
326
- // Group candidates by domain for efficient processing
327
- const byDomain = new Map();
328
- for (const { domain, candidate } of candidatesToProcess) {
329
- if (!byDomain.has(domain)) {
330
- byDomain.set(domain, []);
331
- }
332
- byDomain.get(domain).push(candidate);
333
- }
334
- for (const [domain, candidates] of byDomain) {
335
- const filePath = getExpertisePath(domain);
336
- await withFileLock(filePath, async () => {
337
- const records = await readExpertiseFile(filePath);
338
- let updatedRecords = [...records];
339
- for (const candidate of candidates) {
340
- // Find the actual record objects for this candidate
341
- const recordsToCompact = updatedRecords.filter((r) => r.type === candidate.type && candidate.records.some((cr) => cr.id === r.id));
342
- if (recordsToCompact.length < 2)
343
- continue;
344
- // Create merged replacement record
345
- const replacement = mergeRecords(recordsToCompact);
346
- // Remove old records
347
- const idsToRemove = new Set(recordsToCompact.map((r) => r.id));
348
- updatedRecords = updatedRecords.filter((r) => !idsToRemove.has(r.id));
349
- // Add replacement
350
- updatedRecords.push(replacement);
351
- totalCompacted += recordsToCompact.length;
352
- results.push({ domain, type: candidate.type, count: recordsToCompact.length });
353
- }
354
- // Write back if changes were made
355
- if (updatedRecords.length !== records.length) {
356
- await writeExpertiseFile(filePath, updatedRecords);
357
- }
358
- });
359
- }
360
- if (jsonMode) {
361
- outputJson({
362
- success: true,
363
- command: "compact",
364
- action: "auto",
365
- compacted: totalCompacted,
366
- results,
367
- });
368
- return;
369
- }
370
- console.log(chalk.green(`\n✓ Auto-compacted ${totalCompacted} records across ${results.length} groups`));
371
- for (const r of results) {
372
- console.log(chalk.dim(` ${r.domain}/${r.type}: ${r.count} records → 1`));
373
- }
374
- }
375
- export function mergeRecords(records) {
376
- if (records.length === 0) {
377
- throw new Error("Cannot merge empty record list");
378
- }
379
- const type = records[0].type;
380
- const recordedAt = new Date().toISOString();
381
- const supersedes = records.map((r) => r.id).filter(Boolean);
382
- // Merge tags (unique union)
383
- const allTags = records.flatMap((r) => r.tags ?? []);
384
- const tags = allTags.length > 0 ? Array.from(new Set(allTags)) : undefined;
385
- // Merge files (for pattern/reference types)
386
- const allFiles = records.flatMap((r) => ("files" in r ? r.files ?? [] : []));
387
- const files = allFiles.length > 0 ? Array.from(new Set(allFiles)) : undefined;
388
- let result;
389
- switch (type) {
390
- case "convention": {
391
- const contents = records.map((r) => r.content);
392
- const content = contents.join("\n\n");
393
- result = {
394
- type: "convention",
395
- content,
396
- classification: "foundational",
397
- recorded_at: recordedAt,
398
- supersedes,
399
- };
400
- if (tags)
401
- result.tags = tags;
402
- break;
403
- }
404
- case "pattern": {
405
- const patterns = records;
406
- const name = patterns.reduce((longest, p) => p.name.length > longest.length ? p.name : longest, patterns[0].name);
407
- const description = patterns.map((p) => p.description).join("\n\n");
408
- result = {
409
- type: "pattern",
410
- name,
411
- description,
412
- classification: "foundational",
413
- recorded_at: recordedAt,
414
- supersedes,
415
- };
416
- if (tags)
417
- result.tags = tags;
418
- if (files)
419
- result.files = files;
420
- break;
421
- }
422
- case "failure": {
423
- const failures = records;
424
- const description = failures.map((f) => f.description).join("\n\n");
425
- const resolution = failures.map((f) => f.resolution).join("\n\n");
426
- result = {
427
- type: "failure",
428
- description,
429
- resolution,
430
- classification: "foundational",
431
- recorded_at: recordedAt,
432
- supersedes,
433
- };
434
- if (tags)
435
- result.tags = tags;
436
- break;
437
- }
438
- case "decision": {
439
- const decisions = records;
440
- const title = decisions.reduce((longest, d) => d.title.length > longest.length ? d.title : longest, decisions[0].title);
441
- const rationale = decisions.map((d) => d.rationale).join("\n\n");
442
- result = {
443
- type: "decision",
444
- title,
445
- rationale,
446
- classification: "foundational",
447
- recorded_at: recordedAt,
448
- supersedes,
449
- };
450
- if (tags)
451
- result.tags = tags;
452
- break;
453
- }
454
- case "reference": {
455
- const references = records;
456
- const name = references.reduce((longest, r) => r.name.length > longest.length ? r.name : longest, references[0].name);
457
- const description = references.map((r) => r.description).join("\n\n");
458
- result = {
459
- type: "reference",
460
- name,
461
- description,
462
- classification: "foundational",
463
- recorded_at: recordedAt,
464
- supersedes,
465
- };
466
- if (tags)
467
- result.tags = tags;
468
- if (files)
469
- result.files = files;
470
- break;
471
- }
472
- case "guide": {
473
- const guides = records;
474
- const name = guides.reduce((longest, g) => g.name.length > longest.length ? g.name : longest, guides[0].name);
475
- const description = guides.map((g) => g.description).join("\n\n");
476
- result = {
477
- type: "guide",
478
- name,
479
- description,
480
- classification: "foundational",
481
- recorded_at: recordedAt,
482
- supersedes,
483
- };
484
- if (tags)
485
- result.tags = tags;
486
- break;
487
- }
488
- default: {
489
- throw new Error(`Unknown record type: ${type}`);
490
- }
491
- }
492
- // Generate ID for the merged record
493
- result.id = generateRecordId(result);
494
- return result;
495
- }
496
- async function handleApply(domain, options, jsonMode) {
497
- const config = await readConfig();
498
- if (!config.domains.includes(domain)) {
499
- const msg = `Domain "${domain}" not found in config.`;
500
- if (jsonMode) {
501
- outputJsonError("compact", msg);
502
- }
503
- else {
504
- console.error(chalk.red(`Error: ${msg}`));
505
- }
506
- process.exitCode = 1;
507
- return;
508
- }
509
- if (typeof options.records !== "string") {
510
- const msg = "--records is required for --apply.";
511
- if (jsonMode) {
512
- outputJsonError("compact", msg);
513
- }
514
- else {
515
- console.error(chalk.red(`Error: ${msg}`));
516
- }
517
- process.exitCode = 1;
518
- return;
519
- }
520
- const filePath = getExpertisePath(domain);
521
- await withFileLock(filePath, async () => {
522
- const records = await readExpertiseFile(filePath);
523
- const identifiers = options.records.split(",").map((s) => s.trim()).filter(Boolean);
524
- let indicesToRemove;
525
- try {
526
- indicesToRemove = resolveRecordIds(records, identifiers);
527
- }
528
- catch (err) {
529
- const msg = err.message;
530
- if (jsonMode) {
531
- outputJsonError("compact", msg);
532
- }
533
- else {
534
- console.error(chalk.red(`Error: ${msg}`));
535
- }
536
- process.exitCode = 1;
537
- return;
538
- }
539
- if (indicesToRemove.length < 2) {
540
- const msg = "Compaction requires at least 2 records.";
541
- if (jsonMode) {
542
- outputJsonError("compact", msg);
543
- }
544
- else {
545
- console.error(chalk.red(`Error: ${msg}`));
546
- }
547
- process.exitCode = 1;
548
- return;
549
- }
550
- // Build replacement record
551
- const recordType = options.type ?? records[indicesToRemove[0]].type;
552
- const recordedAt = new Date().toISOString();
553
- const compactedFrom = indicesToRemove.map((i) => records[i].id).filter(Boolean);
554
- let replacement;
555
- switch (recordType) {
556
- case "convention": {
557
- const content = options.content ?? options.description;
558
- if (!content) {
559
- const msg = "Replacement convention requires --content or --description.";
560
- if (jsonMode) {
561
- outputJsonError("compact", msg);
562
- }
563
- else {
564
- console.error(chalk.red(`Error: ${msg}`));
565
- }
566
- process.exitCode = 1;
567
- return;
568
- }
569
- replacement = { type: "convention", content, classification: "foundational", recorded_at: recordedAt };
570
- break;
571
- }
572
- case "pattern": {
573
- const name = options.name;
574
- const description = options.description;
575
- if (!name || !description) {
576
- const msg = "Replacement pattern requires --name and --description.";
577
- if (jsonMode) {
578
- outputJsonError("compact", msg);
579
- }
580
- else {
581
- console.error(chalk.red(`Error: ${msg}`));
582
- }
583
- process.exitCode = 1;
584
- return;
585
- }
586
- replacement = { type: "pattern", name, description, classification: "foundational", recorded_at: recordedAt };
587
- break;
588
- }
589
- case "failure": {
590
- const description = options.description;
591
- const resolution = options.resolution;
592
- if (!description || !resolution) {
593
- const msg = "Replacement failure requires --description and --resolution.";
594
- if (jsonMode) {
595
- outputJsonError("compact", msg);
596
- }
597
- else {
598
- console.error(chalk.red(`Error: ${msg}`));
599
- }
600
- process.exitCode = 1;
601
- return;
602
- }
603
- replacement = { type: "failure", description, resolution, classification: "foundational", recorded_at: recordedAt };
604
- break;
605
- }
606
- case "decision": {
607
- const title = options.title;
608
- const rationale = options.rationale;
609
- if (!title || !rationale) {
610
- const msg = "Replacement decision requires --title and --rationale.";
611
- if (jsonMode) {
612
- outputJsonError("compact", msg);
613
- }
614
- else {
615
- console.error(chalk.red(`Error: ${msg}`));
616
- }
617
- process.exitCode = 1;
618
- return;
619
- }
620
- replacement = { type: "decision", title, rationale, classification: "foundational", recorded_at: recordedAt };
621
- break;
622
- }
623
- case "reference": {
624
- const name = options.name;
625
- const description = options.description;
626
- if (!name || !description) {
627
- const msg = "Replacement reference requires --name and --description.";
628
- if (jsonMode) {
629
- outputJsonError("compact", msg);
630
- }
631
- else {
632
- console.error(chalk.red(`Error: ${msg}`));
633
- }
634
- process.exitCode = 1;
635
- return;
636
- }
637
- replacement = { type: "reference", name, description, classification: "foundational", recorded_at: recordedAt };
638
- break;
639
- }
640
- case "guide": {
641
- const name = options.name;
642
- const description = options.description;
643
- if (!name || !description) {
644
- const msg = "Replacement guide requires --name and --description.";
645
- if (jsonMode) {
646
- outputJsonError("compact", msg);
647
- }
648
- else {
649
- console.error(chalk.red(`Error: ${msg}`));
650
- }
651
- process.exitCode = 1;
652
- return;
653
- }
654
- replacement = { type: "guide", name, description, classification: "foundational", recorded_at: recordedAt };
655
- break;
656
- }
657
- default: {
658
- const msg = `Unknown record type "${recordType}".`;
659
- if (jsonMode) {
660
- outputJsonError("compact", msg);
661
- }
662
- else {
663
- console.error(chalk.red(`Error: ${msg}`));
664
- }
665
- process.exitCode = 1;
666
- return;
667
- }
668
- }
669
- // Add supersedes links to the compacted-from records
670
- if (compactedFrom.length > 0) {
671
- replacement.supersedes = compactedFrom;
672
- }
673
- // Validate replacement
674
- const ajv = new Ajv();
675
- const validate = ajv.compile(recordSchema);
676
- replacement.id = generateRecordId(replacement);
677
- if (!validate(replacement)) {
678
- const errors = (validate.errors ?? []).map((err) => `${err.instancePath} ${err.message}`);
679
- const msg = `Replacement record failed validation: ${errors.join("; ")}`;
680
- if (jsonMode) {
681
- outputJsonError("compact", msg);
682
- }
683
- else {
684
- console.error(chalk.red(`Error: ${msg}`));
685
- }
686
- process.exitCode = 1;
687
- return;
688
- }
689
- // Remove old records and append replacement
690
- const removeSet = new Set(indicesToRemove);
691
- const remaining = records.filter((_, i) => !removeSet.has(i));
692
- remaining.push(replacement);
693
- await writeExpertiseFile(filePath, remaining);
694
- if (jsonMode) {
695
- outputJson({
696
- success: true,
697
- command: "compact",
698
- action: "applied",
699
- domain,
700
- removed: indicesToRemove.length,
701
- replacement,
702
- });
703
- }
704
- else {
705
- console.log(chalk.green(`\u2714 Compacted ${indicesToRemove.length} ${recordType} records into 1 in ${domain}`));
706
- }
707
- });
708
- }
709
- //# sourceMappingURL=compact.js.map