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,767 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConventionRecord,
|
|
3
|
+
DecisionRecord,
|
|
4
|
+
ExpertiseRecord,
|
|
5
|
+
FailureRecord,
|
|
6
|
+
GuideRecord,
|
|
7
|
+
Outcome,
|
|
8
|
+
PatternRecord,
|
|
9
|
+
ReferenceRecord,
|
|
10
|
+
} from "../schemas/record.ts";
|
|
11
|
+
|
|
12
|
+
export function formatTimeAgo(date: Date): string {
|
|
13
|
+
const now = new Date();
|
|
14
|
+
const diffMs = now.getTime() - date.getTime();
|
|
15
|
+
const diffMins = Math.floor(diffMs / 60000);
|
|
16
|
+
const diffHours = Math.floor(diffMs / 3600000);
|
|
17
|
+
const diffDays = Math.floor(diffMs / 86400000);
|
|
18
|
+
|
|
19
|
+
if (diffMins < 1) return "just now";
|
|
20
|
+
if (diffMins < 60) return `${diffMins}m ago`;
|
|
21
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
22
|
+
return `${diffDays}d ago`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatEvidence(evidence: ConventionRecord["evidence"]): string {
|
|
26
|
+
if (!evidence) return "";
|
|
27
|
+
const parts: string[] = [];
|
|
28
|
+
if (evidence.commit) parts.push(`commit: ${evidence.commit}`);
|
|
29
|
+
if (evidence.date) parts.push(`date: ${evidence.date}`);
|
|
30
|
+
if (evidence.issue) parts.push(`issue: ${evidence.issue}`);
|
|
31
|
+
if (evidence.file) parts.push(`file: ${evidence.file}`);
|
|
32
|
+
return parts.length > 0 ? ` [${parts.join(", ")}]` : "";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatOutcome(outcomes: Outcome[] | undefined): string {
|
|
36
|
+
if (!outcomes || outcomes.length === 0) return "";
|
|
37
|
+
const latest = outcomes[outcomes.length - 1];
|
|
38
|
+
const statusSymbol =
|
|
39
|
+
latest.status === "success" ? "✓" : latest.status === "partial" ? "~" : "✗";
|
|
40
|
+
const parts: string[] = [statusSymbol];
|
|
41
|
+
if (latest.duration !== undefined) parts.push(`${latest.duration}ms`);
|
|
42
|
+
if (latest.agent) parts.push(`@${latest.agent}`);
|
|
43
|
+
if (outcomes.length > 1) parts.push(`(${outcomes.length}x)`);
|
|
44
|
+
return ` [${parts.join(" ")}]`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function formatLinks(r: ExpertiseRecord): string {
|
|
48
|
+
const parts: string[] = [];
|
|
49
|
+
if (r.relates_to && r.relates_to.length > 0) {
|
|
50
|
+
parts.push(`relates to: ${r.relates_to.join(", ")}`);
|
|
51
|
+
}
|
|
52
|
+
if (r.supersedes && r.supersedes.length > 0) {
|
|
53
|
+
parts.push(`supersedes: ${r.supersedes.join(", ")}`);
|
|
54
|
+
}
|
|
55
|
+
return parts.length > 0 ? ` [${parts.join("; ")}]` : "";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function formatRecordMeta(r: ExpertiseRecord, full: boolean): string {
|
|
59
|
+
if (!full) return formatLinks(r);
|
|
60
|
+
const parts = [`(${r.classification})${formatEvidence(r.evidence)}`];
|
|
61
|
+
if (r.tags && r.tags.length > 0) {
|
|
62
|
+
parts.push(`[tags: ${r.tags.join(", ")}]`);
|
|
63
|
+
}
|
|
64
|
+
return ` ${parts.join(" ")}${formatLinks(r)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function idTag(r: ExpertiseRecord): string {
|
|
68
|
+
return r.id ? `[${r.id}] ` : "";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function formatConventions(records: ConventionRecord[], full = false): string {
|
|
72
|
+
if (records.length === 0) return "";
|
|
73
|
+
const lines = ["### Conventions"];
|
|
74
|
+
for (const r of records) {
|
|
75
|
+
lines.push(`- ${idTag(r)}${r.content}${formatRecordMeta(r, full)}`);
|
|
76
|
+
}
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function formatPatterns(records: PatternRecord[], full = false): string {
|
|
81
|
+
if (records.length === 0) return "";
|
|
82
|
+
const lines = ["### Patterns"];
|
|
83
|
+
for (const r of records) {
|
|
84
|
+
let line = `- ${idTag(r)}**${r.name}**: ${r.description}`;
|
|
85
|
+
if (r.files && r.files.length > 0) {
|
|
86
|
+
line += ` (${r.files.join(", ")})`;
|
|
87
|
+
}
|
|
88
|
+
line += formatRecordMeta(r, full);
|
|
89
|
+
lines.push(line);
|
|
90
|
+
}
|
|
91
|
+
return lines.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatFailures(records: FailureRecord[], full = false): string {
|
|
95
|
+
if (records.length === 0) return "";
|
|
96
|
+
const lines = ["### Known Failures"];
|
|
97
|
+
for (const r of records) {
|
|
98
|
+
lines.push(`- ${idTag(r)}${r.description}${formatRecordMeta(r, full)}`);
|
|
99
|
+
lines.push(` → ${r.resolution}`);
|
|
100
|
+
}
|
|
101
|
+
return lines.join("\n");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function formatDecisions(records: DecisionRecord[], full = false): string {
|
|
105
|
+
if (records.length === 0) return "";
|
|
106
|
+
const lines = ["### Decisions"];
|
|
107
|
+
for (const r of records) {
|
|
108
|
+
lines.push(
|
|
109
|
+
`- ${idTag(r)}**${r.title}**: ${r.rationale}${formatRecordMeta(r, full)}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function formatReferences(records: ReferenceRecord[], full = false): string {
|
|
116
|
+
if (records.length === 0) return "";
|
|
117
|
+
const lines = ["### References"];
|
|
118
|
+
for (const r of records) {
|
|
119
|
+
let line = `- ${idTag(r)}**${r.name}**: ${r.description}`;
|
|
120
|
+
if (r.files && r.files.length > 0) {
|
|
121
|
+
line += ` (${r.files.join(", ")})`;
|
|
122
|
+
}
|
|
123
|
+
line += formatRecordMeta(r, full);
|
|
124
|
+
lines.push(line);
|
|
125
|
+
}
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function formatGuides(records: GuideRecord[], full = false): string {
|
|
130
|
+
if (records.length === 0) return "";
|
|
131
|
+
const lines = ["### Guides"];
|
|
132
|
+
for (const r of records) {
|
|
133
|
+
lines.push(
|
|
134
|
+
`- ${idTag(r)}**${r.name}**: ${r.description}${formatRecordMeta(r, full)}`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return lines.join("\n");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function truncate(text: string, maxLen = 100): string {
|
|
141
|
+
if (text.length <= maxLen) return text;
|
|
142
|
+
// Try to cut at first sentence boundary within limit
|
|
143
|
+
const sentenceEnd = text.search(/[.!?]\s/);
|
|
144
|
+
if (sentenceEnd > 0 && sentenceEnd < maxLen) {
|
|
145
|
+
return text.slice(0, sentenceEnd + 1);
|
|
146
|
+
}
|
|
147
|
+
return `${text.slice(0, maxLen)}...`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function getRecordSummary(record: ExpertiseRecord): string {
|
|
151
|
+
switch (record.type) {
|
|
152
|
+
case "convention":
|
|
153
|
+
return truncate(record.content, 60);
|
|
154
|
+
case "pattern":
|
|
155
|
+
return record.name;
|
|
156
|
+
case "failure":
|
|
157
|
+
return truncate(record.description, 60);
|
|
158
|
+
case "decision":
|
|
159
|
+
return record.title;
|
|
160
|
+
case "reference":
|
|
161
|
+
return record.name;
|
|
162
|
+
case "guide":
|
|
163
|
+
return record.name;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function compactId(r: ExpertiseRecord): string {
|
|
168
|
+
return r.id ? ` (${r.id})` : "";
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function compactLine(r: ExpertiseRecord): string {
|
|
172
|
+
const links = formatLinks(r);
|
|
173
|
+
const id = compactId(r);
|
|
174
|
+
const outcome = formatOutcome(r.outcomes);
|
|
175
|
+
switch (r.type) {
|
|
176
|
+
case "convention":
|
|
177
|
+
return `- [convention] ${truncate(r.content)}${id}${outcome}${links}`;
|
|
178
|
+
case "pattern": {
|
|
179
|
+
const files =
|
|
180
|
+
r.files && r.files.length > 0 ? ` (${r.files.join(", ")})` : "";
|
|
181
|
+
return `- [pattern] ${r.name}: ${truncate(r.description)}${files}${id}${outcome}${links}`;
|
|
182
|
+
}
|
|
183
|
+
case "failure":
|
|
184
|
+
return `- [failure] ${truncate(r.description)} → ${truncate(r.resolution)}${id}${outcome}${links}`;
|
|
185
|
+
case "decision":
|
|
186
|
+
return `- [decision] ${r.title}: ${truncate(r.rationale)}${id}${outcome}${links}`;
|
|
187
|
+
case "reference": {
|
|
188
|
+
const refFiles =
|
|
189
|
+
r.files && r.files.length > 0
|
|
190
|
+
? `: ${r.files.join(", ")}`
|
|
191
|
+
: `: ${truncate(r.description)}`;
|
|
192
|
+
return `- [reference] ${r.name}${refFiles}${id}${outcome}${links}`;
|
|
193
|
+
}
|
|
194
|
+
case "guide":
|
|
195
|
+
return `- [guide] ${r.name}: ${truncate(r.description)}${id}${outcome}${links}`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function formatDomainExpertiseCompact(
|
|
200
|
+
domain: string,
|
|
201
|
+
records: ExpertiseRecord[],
|
|
202
|
+
lastUpdated: Date | null,
|
|
203
|
+
): string {
|
|
204
|
+
const updatedStr = lastUpdated
|
|
205
|
+
? `, updated ${formatTimeAgo(lastUpdated)}`
|
|
206
|
+
: "";
|
|
207
|
+
const lines: string[] = [];
|
|
208
|
+
|
|
209
|
+
lines.push(`## ${domain} (${records.length} records${updatedStr})`);
|
|
210
|
+
for (const r of records) {
|
|
211
|
+
lines.push(compactLine(r));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return lines.join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export function formatPrimeOutputCompact(domainSections: string[]): string {
|
|
218
|
+
const lines: string[] = [];
|
|
219
|
+
|
|
220
|
+
lines.push("# Project Expertise (via Mulch)");
|
|
221
|
+
lines.push("");
|
|
222
|
+
|
|
223
|
+
if (domainSections.length === 0) {
|
|
224
|
+
lines.push(
|
|
225
|
+
"No expertise recorded yet. Use `mulch add <domain>` to create a domain, then `mulch record` to add records.",
|
|
226
|
+
);
|
|
227
|
+
} else {
|
|
228
|
+
lines.push(domainSections.join("\n\n"));
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
lines.push("");
|
|
232
|
+
lines.push("## Quick Reference");
|
|
233
|
+
lines.push("");
|
|
234
|
+
lines.push(
|
|
235
|
+
'- `mulch search "query"` — find relevant records before implementing',
|
|
236
|
+
);
|
|
237
|
+
lines.push(
|
|
238
|
+
"- `mulch prime --files src/foo.ts` — load records for specific files",
|
|
239
|
+
);
|
|
240
|
+
lines.push("- `mulch prime --context` — load records for git-changed files");
|
|
241
|
+
lines.push('- `mulch record <domain> --type <type> --description "..."`');
|
|
242
|
+
lines.push(
|
|
243
|
+
" - Types: `convention`, `pattern`, `failure`, `decision`, `reference`, `guide`",
|
|
244
|
+
);
|
|
245
|
+
lines.push(" - Evidence: `--evidence-commit <sha>`, `--evidence-bead <id>`");
|
|
246
|
+
lines.push("- `mulch doctor` — check record health");
|
|
247
|
+
|
|
248
|
+
return lines.join("\n");
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function formatDomainExpertise(
|
|
252
|
+
domain: string,
|
|
253
|
+
records: ExpertiseRecord[],
|
|
254
|
+
lastUpdated: Date | null,
|
|
255
|
+
options: { full?: boolean } = {},
|
|
256
|
+
): string {
|
|
257
|
+
const full = options.full ?? false;
|
|
258
|
+
const updatedStr = lastUpdated
|
|
259
|
+
? `, updated ${formatTimeAgo(lastUpdated)}`
|
|
260
|
+
: "";
|
|
261
|
+
const lines: string[] = [];
|
|
262
|
+
|
|
263
|
+
lines.push(`## ${domain} (${records.length} records${updatedStr})`);
|
|
264
|
+
lines.push("");
|
|
265
|
+
|
|
266
|
+
const conventions = records.filter(
|
|
267
|
+
(r): r is ConventionRecord => r.type === "convention",
|
|
268
|
+
);
|
|
269
|
+
const patterns = records.filter(
|
|
270
|
+
(r): r is PatternRecord => r.type === "pattern",
|
|
271
|
+
);
|
|
272
|
+
const failures = records.filter(
|
|
273
|
+
(r): r is FailureRecord => r.type === "failure",
|
|
274
|
+
);
|
|
275
|
+
const decisions = records.filter(
|
|
276
|
+
(r): r is DecisionRecord => r.type === "decision",
|
|
277
|
+
);
|
|
278
|
+
const references = records.filter(
|
|
279
|
+
(r): r is ReferenceRecord => r.type === "reference",
|
|
280
|
+
);
|
|
281
|
+
const guides = records.filter((r): r is GuideRecord => r.type === "guide");
|
|
282
|
+
|
|
283
|
+
const sections = [
|
|
284
|
+
formatConventions(conventions, full),
|
|
285
|
+
formatPatterns(patterns, full),
|
|
286
|
+
formatFailures(failures, full),
|
|
287
|
+
formatDecisions(decisions, full),
|
|
288
|
+
formatReferences(references, full),
|
|
289
|
+
formatGuides(guides, full),
|
|
290
|
+
].filter((s) => s.length > 0);
|
|
291
|
+
|
|
292
|
+
lines.push(sections.join("\n\n"));
|
|
293
|
+
|
|
294
|
+
return lines.join("\n");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function formatPrimeOutput(domainSections: string[]): string {
|
|
298
|
+
const lines: string[] = [];
|
|
299
|
+
|
|
300
|
+
lines.push("# Project Expertise (via Mulch)");
|
|
301
|
+
lines.push("");
|
|
302
|
+
lines.push(
|
|
303
|
+
"> **Context Recovery**: Run `mulch prime` after compaction, clear, or new session",
|
|
304
|
+
);
|
|
305
|
+
lines.push("");
|
|
306
|
+
lines.push("## Rules");
|
|
307
|
+
lines.push("");
|
|
308
|
+
lines.push(
|
|
309
|
+
"- **Record learnings**: When you discover a pattern, fix a bug, or make a design decision — record it with `mulch record`",
|
|
310
|
+
);
|
|
311
|
+
lines.push(
|
|
312
|
+
"- **Check expertise first**: Before implementing, check if relevant expertise exists with `mulch search` or `mulch prime --context`",
|
|
313
|
+
);
|
|
314
|
+
lines.push(
|
|
315
|
+
"- **Targeted priming**: Use `mulch prime --files src/foo.ts` to load only records relevant to specific files",
|
|
316
|
+
);
|
|
317
|
+
lines.push(
|
|
318
|
+
"- **Do NOT** store expertise in code comments, markdown files, or memory tools — use `mulch record`",
|
|
319
|
+
);
|
|
320
|
+
lines.push(
|
|
321
|
+
"- Run `mulch doctor` if you are unsure whether records are healthy",
|
|
322
|
+
);
|
|
323
|
+
lines.push("");
|
|
324
|
+
|
|
325
|
+
if (domainSections.length === 0) {
|
|
326
|
+
lines.push(
|
|
327
|
+
"No expertise recorded yet. Use `mulch add <domain>` to create a domain, then `mulch record` to add records.",
|
|
328
|
+
);
|
|
329
|
+
lines.push("");
|
|
330
|
+
} else {
|
|
331
|
+
lines.push(domainSections.join("\n\n"));
|
|
332
|
+
lines.push("");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
lines.push("");
|
|
336
|
+
lines.push("## Recording New Learnings");
|
|
337
|
+
lines.push("");
|
|
338
|
+
lines.push(
|
|
339
|
+
"When you discover a pattern, convention, failure, or make an architectural decision:",
|
|
340
|
+
);
|
|
341
|
+
lines.push("");
|
|
342
|
+
lines.push("```bash");
|
|
343
|
+
lines.push('mulch record <domain> --type convention "description"');
|
|
344
|
+
lines.push(
|
|
345
|
+
'mulch record <domain> --type failure --description "..." --resolution "..."',
|
|
346
|
+
);
|
|
347
|
+
lines.push(
|
|
348
|
+
'mulch record <domain> --type decision --title "..." --rationale "..."',
|
|
349
|
+
);
|
|
350
|
+
lines.push(
|
|
351
|
+
'mulch record <domain> --type pattern --name "..." --description "..." --files "..."',
|
|
352
|
+
);
|
|
353
|
+
lines.push(
|
|
354
|
+
'mulch record <domain> --type reference --name "..." --description "..." --files "..."',
|
|
355
|
+
);
|
|
356
|
+
lines.push(
|
|
357
|
+
'mulch record <domain> --type guide --name "..." --description "..."',
|
|
358
|
+
);
|
|
359
|
+
lines.push("```");
|
|
360
|
+
lines.push("");
|
|
361
|
+
lines.push("**Link evidence** to records when available:");
|
|
362
|
+
lines.push("");
|
|
363
|
+
lines.push("```bash");
|
|
364
|
+
lines.push(
|
|
365
|
+
'mulch record <domain> --type pattern --name "..." --description "..." --evidence-commit abc123',
|
|
366
|
+
);
|
|
367
|
+
lines.push(
|
|
368
|
+
'mulch record <domain> --type decision --title "..." --rationale "..." --evidence-bead seeds-xxx',
|
|
369
|
+
);
|
|
370
|
+
lines.push("```");
|
|
371
|
+
lines.push("");
|
|
372
|
+
lines.push("**Batch record** multiple records at once:");
|
|
373
|
+
lines.push("");
|
|
374
|
+
lines.push("```bash");
|
|
375
|
+
lines.push("mulch record <domain> --batch records.json # from file");
|
|
376
|
+
lines.push(
|
|
377
|
+
'echo \'[{"type":"convention","content":"..."}]\' | mulch record <domain> --stdin # from stdin',
|
|
378
|
+
);
|
|
379
|
+
lines.push("```");
|
|
380
|
+
lines.push("");
|
|
381
|
+
lines.push("## Searching Expertise");
|
|
382
|
+
lines.push("");
|
|
383
|
+
lines.push(
|
|
384
|
+
"Use `mulch search` to find relevant records across all domains. Results are ranked by relevance (BM25):",
|
|
385
|
+
);
|
|
386
|
+
lines.push("");
|
|
387
|
+
lines.push("```bash");
|
|
388
|
+
lines.push(
|
|
389
|
+
'mulch search "file locking" # multi-word queries ranked by relevance',
|
|
390
|
+
);
|
|
391
|
+
lines.push(
|
|
392
|
+
'mulch search "atomic" --domain cli # limit to a specific domain',
|
|
393
|
+
);
|
|
394
|
+
lines.push(
|
|
395
|
+
'mulch search "ESM" --type convention # filter by record type',
|
|
396
|
+
);
|
|
397
|
+
lines.push('mulch search "concurrency" --tag safety # filter by tag');
|
|
398
|
+
lines.push("```");
|
|
399
|
+
lines.push("");
|
|
400
|
+
lines.push(
|
|
401
|
+
"Search before implementing — existing expertise may already cover your use case.",
|
|
402
|
+
);
|
|
403
|
+
lines.push("");
|
|
404
|
+
lines.push("## Domain Maintenance");
|
|
405
|
+
lines.push("");
|
|
406
|
+
lines.push(
|
|
407
|
+
"When a domain grows large, compact it to keep expertise focused:",
|
|
408
|
+
);
|
|
409
|
+
lines.push("");
|
|
410
|
+
lines.push("```bash");
|
|
411
|
+
lines.push(
|
|
412
|
+
"mulch compact --auto --dry-run # preview what would be merged",
|
|
413
|
+
);
|
|
414
|
+
lines.push(
|
|
415
|
+
"mulch compact --auto # merge same-type record groups",
|
|
416
|
+
);
|
|
417
|
+
lines.push("```");
|
|
418
|
+
lines.push("");
|
|
419
|
+
lines.push("Use `mulch diff` to review what expertise changed:");
|
|
420
|
+
lines.push("");
|
|
421
|
+
lines.push("```bash");
|
|
422
|
+
lines.push(
|
|
423
|
+
"mulch diff HEAD~3 # see record changes over last 3 commits",
|
|
424
|
+
);
|
|
425
|
+
lines.push("```");
|
|
426
|
+
lines.push("");
|
|
427
|
+
lines.push("## Session End");
|
|
428
|
+
lines.push("");
|
|
429
|
+
lines.push(
|
|
430
|
+
"**IMPORTANT**: Before ending your session, record what you learned and sync:",
|
|
431
|
+
);
|
|
432
|
+
lines.push("");
|
|
433
|
+
lines.push("```");
|
|
434
|
+
lines.push(
|
|
435
|
+
"[ ] mulch learn # see what files changed — decide what to record",
|
|
436
|
+
);
|
|
437
|
+
lines.push("[ ] mulch record ... # record learnings (see above)");
|
|
438
|
+
lines.push(
|
|
439
|
+
"[ ] mulch sync # validate, stage, and commit .mulch/ changes",
|
|
440
|
+
);
|
|
441
|
+
lines.push("```");
|
|
442
|
+
lines.push("");
|
|
443
|
+
lines.push(
|
|
444
|
+
"Do NOT skip this. Unrecorded learnings are lost for the next session.",
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
return lines.join("\n");
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
export type PrimeFormat = "markdown" | "xml" | "plain";
|
|
451
|
+
|
|
452
|
+
// --- XML format (optimized for Claude) ---
|
|
453
|
+
|
|
454
|
+
function xmlEscape(str: string): string {
|
|
455
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
export function formatDomainExpertiseXml(
|
|
459
|
+
domain: string,
|
|
460
|
+
records: ExpertiseRecord[],
|
|
461
|
+
lastUpdated: Date | null,
|
|
462
|
+
): string {
|
|
463
|
+
const updatedStr = lastUpdated
|
|
464
|
+
? ` updated="${formatTimeAgo(lastUpdated)}"`
|
|
465
|
+
: "";
|
|
466
|
+
const lines: string[] = [];
|
|
467
|
+
|
|
468
|
+
lines.push(
|
|
469
|
+
`<domain name="${xmlEscape(domain)}" entries="${records.length}"${updatedStr}>`,
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
for (const r of records) {
|
|
473
|
+
const idAttr = r.id ? ` id="${xmlEscape(r.id)}"` : "";
|
|
474
|
+
lines.push(` <${r.type}${idAttr} classification="${r.classification}">`);
|
|
475
|
+
|
|
476
|
+
switch (r.type) {
|
|
477
|
+
case "convention":
|
|
478
|
+
lines.push(` ${xmlEscape(r.content)}`);
|
|
479
|
+
break;
|
|
480
|
+
case "pattern":
|
|
481
|
+
lines.push(` <name>${xmlEscape(r.name)}</name>`);
|
|
482
|
+
lines.push(
|
|
483
|
+
` <description>${xmlEscape(r.description)}</description>`,
|
|
484
|
+
);
|
|
485
|
+
if (r.files && r.files.length > 0) {
|
|
486
|
+
lines.push(` <files>${r.files.map(xmlEscape).join(", ")}</files>`);
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
case "failure":
|
|
490
|
+
lines.push(
|
|
491
|
+
` <description>${xmlEscape(r.description)}</description>`,
|
|
492
|
+
);
|
|
493
|
+
lines.push(` <resolution>${xmlEscape(r.resolution)}</resolution>`);
|
|
494
|
+
break;
|
|
495
|
+
case "decision":
|
|
496
|
+
lines.push(` <title>${xmlEscape(r.title)}</title>`);
|
|
497
|
+
lines.push(` <rationale>${xmlEscape(r.rationale)}</rationale>`);
|
|
498
|
+
break;
|
|
499
|
+
case "reference":
|
|
500
|
+
lines.push(` <name>${xmlEscape(r.name)}</name>`);
|
|
501
|
+
lines.push(
|
|
502
|
+
` <description>${xmlEscape(r.description)}</description>`,
|
|
503
|
+
);
|
|
504
|
+
if (r.files && r.files.length > 0) {
|
|
505
|
+
lines.push(` <files>${r.files.map(xmlEscape).join(", ")}</files>`);
|
|
506
|
+
}
|
|
507
|
+
break;
|
|
508
|
+
case "guide":
|
|
509
|
+
lines.push(` <name>${xmlEscape(r.name)}</name>`);
|
|
510
|
+
lines.push(
|
|
511
|
+
` <description>${xmlEscape(r.description)}</description>`,
|
|
512
|
+
);
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
if (r.tags && r.tags.length > 0) {
|
|
516
|
+
lines.push(` <tags>${r.tags.map(xmlEscape).join(", ")}</tags>`);
|
|
517
|
+
}
|
|
518
|
+
if (r.relates_to && r.relates_to.length > 0) {
|
|
519
|
+
lines.push(` <relates_to>${r.relates_to.join(", ")}</relates_to>`);
|
|
520
|
+
}
|
|
521
|
+
if (r.supersedes && r.supersedes.length > 0) {
|
|
522
|
+
lines.push(` <supersedes>${r.supersedes.join(", ")}</supersedes>`);
|
|
523
|
+
}
|
|
524
|
+
if (r.outcomes && r.outcomes.length > 0) {
|
|
525
|
+
for (const outcome of r.outcomes) {
|
|
526
|
+
const durationAttr =
|
|
527
|
+
outcome.duration !== undefined
|
|
528
|
+
? ` duration="${outcome.duration}"`
|
|
529
|
+
: "";
|
|
530
|
+
const agentAttr = outcome.agent
|
|
531
|
+
? ` agent="${xmlEscape(outcome.agent)}"`
|
|
532
|
+
: "";
|
|
533
|
+
const testResultsContent = outcome.test_results
|
|
534
|
+
? `${xmlEscape(outcome.test_results)}`
|
|
535
|
+
: "";
|
|
536
|
+
lines.push(
|
|
537
|
+
` <outcome status="${outcome.status}"${durationAttr}${agentAttr}>${testResultsContent}</outcome>`,
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
lines.push(` </${r.type}>`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
lines.push("</domain>");
|
|
545
|
+
return lines.join("\n");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
export function formatPrimeOutputXml(domainSections: string[]): string {
|
|
549
|
+
const lines: string[] = [];
|
|
550
|
+
lines.push("<expertise>");
|
|
551
|
+
|
|
552
|
+
if (domainSections.length === 0) {
|
|
553
|
+
lines.push(
|
|
554
|
+
" <empty>No expertise recorded yet. Use mulch add and mulch record to get started.</empty>",
|
|
555
|
+
);
|
|
556
|
+
} else {
|
|
557
|
+
lines.push(domainSections.join("\n"));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
lines.push("</expertise>");
|
|
561
|
+
return lines.join("\n");
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// --- Plain text format (optimized for Codex) ---
|
|
565
|
+
|
|
566
|
+
export function formatDomainExpertisePlain(
|
|
567
|
+
domain: string,
|
|
568
|
+
records: ExpertiseRecord[],
|
|
569
|
+
lastUpdated: Date | null,
|
|
570
|
+
): string {
|
|
571
|
+
const updatedStr = lastUpdated
|
|
572
|
+
? ` (updated ${formatTimeAgo(lastUpdated)})`
|
|
573
|
+
: "";
|
|
574
|
+
const lines: string[] = [];
|
|
575
|
+
|
|
576
|
+
lines.push(`[${domain}] ${records.length} records${updatedStr}`);
|
|
577
|
+
lines.push("");
|
|
578
|
+
|
|
579
|
+
const conventions = records.filter(
|
|
580
|
+
(r): r is ConventionRecord => r.type === "convention",
|
|
581
|
+
);
|
|
582
|
+
const patterns = records.filter(
|
|
583
|
+
(r): r is PatternRecord => r.type === "pattern",
|
|
584
|
+
);
|
|
585
|
+
const failures = records.filter(
|
|
586
|
+
(r): r is FailureRecord => r.type === "failure",
|
|
587
|
+
);
|
|
588
|
+
const decisions = records.filter(
|
|
589
|
+
(r): r is DecisionRecord => r.type === "decision",
|
|
590
|
+
);
|
|
591
|
+
|
|
592
|
+
if (conventions.length > 0) {
|
|
593
|
+
lines.push("Conventions:");
|
|
594
|
+
for (const r of conventions) {
|
|
595
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
596
|
+
lines.push(` - ${id}${r.content}${formatLinks(r)}`);
|
|
597
|
+
}
|
|
598
|
+
lines.push("");
|
|
599
|
+
}
|
|
600
|
+
if (patterns.length > 0) {
|
|
601
|
+
lines.push("Patterns:");
|
|
602
|
+
for (const r of patterns) {
|
|
603
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
604
|
+
let line = ` - ${id}${r.name}: ${r.description}`;
|
|
605
|
+
if (r.files && r.files.length > 0) {
|
|
606
|
+
line += ` (${r.files.join(", ")})`;
|
|
607
|
+
}
|
|
608
|
+
line += formatLinks(r);
|
|
609
|
+
lines.push(line);
|
|
610
|
+
}
|
|
611
|
+
lines.push("");
|
|
612
|
+
}
|
|
613
|
+
if (failures.length > 0) {
|
|
614
|
+
lines.push("Known Failures:");
|
|
615
|
+
for (const r of failures) {
|
|
616
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
617
|
+
lines.push(` - ${id}${r.description}${formatLinks(r)}`);
|
|
618
|
+
lines.push(` Fix: ${r.resolution}`);
|
|
619
|
+
}
|
|
620
|
+
lines.push("");
|
|
621
|
+
}
|
|
622
|
+
if (decisions.length > 0) {
|
|
623
|
+
lines.push("Decisions:");
|
|
624
|
+
for (const r of decisions) {
|
|
625
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
626
|
+
lines.push(` - ${id}${r.title}: ${r.rationale}${formatLinks(r)}`);
|
|
627
|
+
}
|
|
628
|
+
lines.push("");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const references = records.filter(
|
|
632
|
+
(r): r is ReferenceRecord => r.type === "reference",
|
|
633
|
+
);
|
|
634
|
+
const guides = records.filter((r): r is GuideRecord => r.type === "guide");
|
|
635
|
+
|
|
636
|
+
if (references.length > 0) {
|
|
637
|
+
lines.push("References:");
|
|
638
|
+
for (const r of references) {
|
|
639
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
640
|
+
let line = ` - ${id}${r.name}: ${r.description}`;
|
|
641
|
+
if (r.files && r.files.length > 0) {
|
|
642
|
+
line += ` (${r.files.join(", ")})`;
|
|
643
|
+
}
|
|
644
|
+
line += formatLinks(r);
|
|
645
|
+
lines.push(line);
|
|
646
|
+
}
|
|
647
|
+
lines.push("");
|
|
648
|
+
}
|
|
649
|
+
if (guides.length > 0) {
|
|
650
|
+
lines.push("Guides:");
|
|
651
|
+
for (const r of guides) {
|
|
652
|
+
const id = r.id ? `[${r.id}] ` : "";
|
|
653
|
+
lines.push(` - ${id}${r.name}: ${r.description}${formatLinks(r)}`);
|
|
654
|
+
}
|
|
655
|
+
lines.push("");
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
return lines.join("\n").trimEnd();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function formatPrimeOutputPlain(domainSections: string[]): string {
|
|
662
|
+
const lines: string[] = [];
|
|
663
|
+
lines.push("Project Expertise (via Mulch)");
|
|
664
|
+
lines.push("============================");
|
|
665
|
+
lines.push("");
|
|
666
|
+
|
|
667
|
+
if (domainSections.length === 0) {
|
|
668
|
+
lines.push(
|
|
669
|
+
"No expertise recorded yet. Use `mulch add <domain>` and `mulch record` to get started.",
|
|
670
|
+
);
|
|
671
|
+
} else {
|
|
672
|
+
lines.push(domainSections.join("\n\n"));
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return lines.join("\n");
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export interface McpDomain {
|
|
679
|
+
domain: string;
|
|
680
|
+
entry_count: number;
|
|
681
|
+
records: ExpertiseRecord[];
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export function formatMcpOutput(domains: McpDomain[]): string {
|
|
685
|
+
return JSON.stringify({ type: "expertise", domains }, null, 2);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function getSessionEndReminder(format: PrimeFormat): string {
|
|
689
|
+
switch (format) {
|
|
690
|
+
case "xml":
|
|
691
|
+
return [
|
|
692
|
+
'<session_close_protocol priority="critical">',
|
|
693
|
+
" <instruction>Before saying done or complete, you MUST run this checklist:</instruction>",
|
|
694
|
+
" <checklist>",
|
|
695
|
+
" <step>mulch learn — see what files changed, decide what to record</step>",
|
|
696
|
+
" <step>mulch record <domain> --type <type> --description "..."</step>",
|
|
697
|
+
" <step>mulch sync — validate, stage, and commit .mulch/ changes</step>",
|
|
698
|
+
" </checklist>",
|
|
699
|
+
" <warning>NEVER skip this. Unrecorded learnings are lost for the next session.</warning>",
|
|
700
|
+
"</session_close_protocol>",
|
|
701
|
+
].join("\n");
|
|
702
|
+
case "plain":
|
|
703
|
+
return [
|
|
704
|
+
"=== SESSION CLOSE PROTOCOL (CRITICAL) ===",
|
|
705
|
+
"",
|
|
706
|
+
'Before saying "done" or "complete", you MUST run this checklist:',
|
|
707
|
+
"",
|
|
708
|
+
"[ ] 1. mulch learn (see what files changed — decide what to record)",
|
|
709
|
+
'[ ] 2. mulch record <domain> --type <type> --description "..."',
|
|
710
|
+
"[ ] 3. mulch sync (validate, stage, and commit .mulch/ changes)",
|
|
711
|
+
"",
|
|
712
|
+
"NEVER skip this. Unrecorded learnings are lost for the next session.",
|
|
713
|
+
].join("\n");
|
|
714
|
+
default:
|
|
715
|
+
return [
|
|
716
|
+
"# \u{1F6A8} SESSION CLOSE PROTOCOL \u{1F6A8}",
|
|
717
|
+
"",
|
|
718
|
+
'**CRITICAL**: Before saying "done" or "complete", you MUST run this checklist:',
|
|
719
|
+
"",
|
|
720
|
+
"```",
|
|
721
|
+
"[ ] 1. mulch learn # see what files changed — decide what to record",
|
|
722
|
+
'[ ] 2. mulch record <domain> --type <type> --description "..."',
|
|
723
|
+
"[ ] 3. mulch sync # validate, stage, and commit .mulch/ changes",
|
|
724
|
+
"```",
|
|
725
|
+
"",
|
|
726
|
+
"**NEVER skip this.** Unrecorded learnings are lost for the next session.",
|
|
727
|
+
].join("\n");
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function formatStatusOutput(
|
|
732
|
+
domainStats: Array<{
|
|
733
|
+
domain: string;
|
|
734
|
+
count: number;
|
|
735
|
+
lastUpdated: Date | null;
|
|
736
|
+
}>,
|
|
737
|
+
governance: { max_entries: number; warn_entries: number; hard_limit: number },
|
|
738
|
+
): string {
|
|
739
|
+
const lines: string[] = [];
|
|
740
|
+
lines.push("Mulch Status");
|
|
741
|
+
lines.push("============");
|
|
742
|
+
lines.push("");
|
|
743
|
+
|
|
744
|
+
if (domainStats.length === 0) {
|
|
745
|
+
lines.push(
|
|
746
|
+
"No domains configured. Run `mulch add <domain>` to get started.",
|
|
747
|
+
);
|
|
748
|
+
return lines.join("\n");
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
for (const { domain, count, lastUpdated } of domainStats) {
|
|
752
|
+
const updatedStr = lastUpdated ? formatTimeAgo(lastUpdated) : "never";
|
|
753
|
+
let status = "";
|
|
754
|
+
if (count >= governance.hard_limit) {
|
|
755
|
+
status = " ⚠ OVER HARD LIMIT — must decompose";
|
|
756
|
+
} else if (count >= governance.warn_entries) {
|
|
757
|
+
status = " ⚠ consider splitting domain";
|
|
758
|
+
} else if (count >= governance.max_entries) {
|
|
759
|
+
status = " — approaching limit";
|
|
760
|
+
}
|
|
761
|
+
lines.push(
|
|
762
|
+
` ${domain}: ${count} records (updated ${updatedStr})${status}`,
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
return lines.join("\n");
|
|
767
|
+
}
|