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
package/dist/commands/compact.js
DELETED
|
@@ -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
|