@dzhechkov/harness-core 0.3.103 → 0.3.105
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/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/recall-hook-policy.d.ts +99 -0
- package/dist/recall-hook-policy.d.ts.map +1 -0
- package/dist/recall-hook-policy.js +132 -0
- package/dist/recall-hook-policy.js.map +1 -0
- package/dist/recall-usage.d.ts +82 -0
- package/dist/recall-usage.d.ts.map +1 -0
- package/dist/recall-usage.js +283 -0
- package/dist/recall-usage.js.map +1 -0
- package/dist/statusline.d.ts +2 -0
- package/dist/statusline.d.ts.map +1 -1
- package/dist/statusline.js +23 -0
- package/dist/statusline.js.map +1 -1
- package/package.json +4 -4
- package/src/index.ts +34 -0
- package/src/recall-hook-policy.ts +170 -0
- package/src/recall-usage.ts +373 -0
- package/src/statusline.ts +23 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure recall-usage accounting for the dz APPLY leg.
|
|
3
|
+
*
|
|
4
|
+
* The live hook writes one JSONL event when a learned pattern is actually injected into a prompt.
|
|
5
|
+
* This module parses that append-only log, folds it into per-pattern usage stats, and compacts it
|
|
6
|
+
* into aggregate JSONL rows when it crosses a bounded size. It deliberately knows nothing about the
|
|
7
|
+
* filesystem; callers own reads/writes so the hook and statusline can keep their never-block rules.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const RECALL_USAGE_LOG_RELATIVE = '.dz/recall-usage.jsonl';
|
|
13
|
+
export const RECALL_USAGE_LOG_MAX_BYTES = 1_048_576;
|
|
14
|
+
export const RECALL_USAGE_COMPACT_TARGET_BYTES = Math.floor(RECALL_USAGE_LOG_MAX_BYTES * 0.75);
|
|
15
|
+
|
|
16
|
+
export interface RecallUsageReadRecord {
|
|
17
|
+
readonly dzId: string;
|
|
18
|
+
readonly score: number;
|
|
19
|
+
readonly ts: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RecallUsageAggregateRecord {
|
|
23
|
+
readonly kind: 'aggregate';
|
|
24
|
+
readonly dzId: string;
|
|
25
|
+
readonly reads: number;
|
|
26
|
+
readonly firstReadAt: string;
|
|
27
|
+
readonly lastReadAt: string;
|
|
28
|
+
readonly maxScore: number;
|
|
29
|
+
readonly totalScore: number;
|
|
30
|
+
readonly compactedAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type RecallUsageRecord = RecallUsageReadRecord | RecallUsageAggregateRecord;
|
|
34
|
+
|
|
35
|
+
export interface ParsedRecallUsageLog {
|
|
36
|
+
readonly records: readonly RecallUsageRecord[];
|
|
37
|
+
readonly validLines: number;
|
|
38
|
+
readonly invalidLines: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RecallUsageStat {
|
|
42
|
+
readonly dzId: string;
|
|
43
|
+
readonly reads: number;
|
|
44
|
+
readonly firstReadAt: string;
|
|
45
|
+
readonly lastReadAt: string;
|
|
46
|
+
readonly maxScore: number;
|
|
47
|
+
readonly avgScore: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface RecallPatternUsageRef {
|
|
51
|
+
readonly dzId: string;
|
|
52
|
+
readonly pattern: string;
|
|
53
|
+
readonly domain?: string;
|
|
54
|
+
readonly reward?: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface RecallUsagePatternRow extends RecallPatternUsageRef {
|
|
58
|
+
readonly reads: number;
|
|
59
|
+
readonly firstReadAt?: string;
|
|
60
|
+
readonly lastReadAt?: string;
|
|
61
|
+
readonly maxScore?: number;
|
|
62
|
+
readonly avgScore?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface RecallUsageReport {
|
|
66
|
+
readonly totalPatterns: number;
|
|
67
|
+
readonly usedPatterns: number;
|
|
68
|
+
readonly neverReadPatterns: number;
|
|
69
|
+
readonly totalReads: number;
|
|
70
|
+
readonly unknownReadPatterns: number;
|
|
71
|
+
readonly invalidLines: number;
|
|
72
|
+
readonly top: readonly RecallUsagePatternRow[];
|
|
73
|
+
readonly neverRead: readonly RecallUsagePatternRow[];
|
|
74
|
+
readonly unknown: readonly RecallUsageStat[];
|
|
75
|
+
readonly all: readonly RecallUsagePatternRow[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface Acc {
|
|
79
|
+
dzId: string;
|
|
80
|
+
reads: number;
|
|
81
|
+
firstReadAt: string;
|
|
82
|
+
firstMs: number;
|
|
83
|
+
lastReadAt: string;
|
|
84
|
+
lastMs: number;
|
|
85
|
+
maxScore: number;
|
|
86
|
+
totalScore: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function formatRecallUsageRecord(input: {
|
|
90
|
+
readonly dzId?: unknown;
|
|
91
|
+
readonly score?: unknown;
|
|
92
|
+
readonly ts?: unknown;
|
|
93
|
+
}): string | undefined {
|
|
94
|
+
const rec = normalizeReadRecord(input);
|
|
95
|
+
return rec === undefined ? undefined : `${JSON.stringify(rec)}\n`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function parseRecallUsageLog(text: string): ParsedRecallUsageLog {
|
|
99
|
+
const records: RecallUsageRecord[] = [];
|
|
100
|
+
let invalidLines = 0;
|
|
101
|
+
for (const line of text.split('\n')) {
|
|
102
|
+
const trimmed = line.trim();
|
|
103
|
+
if (trimmed === '') continue;
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(trimmed) as unknown;
|
|
106
|
+
const record = normalizeRecord(parsed);
|
|
107
|
+
if (record === undefined) {
|
|
108
|
+
invalidLines += 1;
|
|
109
|
+
} else {
|
|
110
|
+
records.push(record);
|
|
111
|
+
}
|
|
112
|
+
} catch {
|
|
113
|
+
invalidLines += 1;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return { records, validLines: records.length, invalidLines };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function aggregateRecallUsage(records: readonly RecallUsageRecord[]): readonly RecallUsageStat[] {
|
|
120
|
+
const byId = new Map<string, Acc>();
|
|
121
|
+
for (const rec of records) {
|
|
122
|
+
if (isAggregate(rec)) {
|
|
123
|
+
mergeAggregate(byId, rec);
|
|
124
|
+
} else {
|
|
125
|
+
mergeRead(byId, rec);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return [...byId.values()]
|
|
129
|
+
.map((a) => ({
|
|
130
|
+
dzId: a.dzId,
|
|
131
|
+
reads: a.reads,
|
|
132
|
+
firstReadAt: a.firstReadAt,
|
|
133
|
+
lastReadAt: a.lastReadAt,
|
|
134
|
+
maxScore: a.maxScore,
|
|
135
|
+
avgScore: a.totalScore / a.reads,
|
|
136
|
+
}))
|
|
137
|
+
.sort(compareStats);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function buildRecallUsageReport(
|
|
141
|
+
patterns: readonly RecallPatternUsageRef[],
|
|
142
|
+
parsed: ParsedRecallUsageLog,
|
|
143
|
+
): RecallUsageReport {
|
|
144
|
+
const stats = aggregateRecallUsage(parsed.records);
|
|
145
|
+
const statsById = new Map(stats.map((s) => [s.dzId, s]));
|
|
146
|
+
const patternIds = new Set<string>();
|
|
147
|
+
const all: RecallUsagePatternRow[] = [];
|
|
148
|
+
|
|
149
|
+
for (const p of patterns) {
|
|
150
|
+
if (p.dzId.trim() === '') continue;
|
|
151
|
+
if (patternIds.has(p.dzId)) continue;
|
|
152
|
+
patternIds.add(p.dzId);
|
|
153
|
+
const stat = statsById.get(p.dzId);
|
|
154
|
+
const base = patternRef(p);
|
|
155
|
+
if (stat === undefined) {
|
|
156
|
+
all.push({ ...base, reads: 0 });
|
|
157
|
+
} else {
|
|
158
|
+
all.push({
|
|
159
|
+
...base,
|
|
160
|
+
reads: stat.reads,
|
|
161
|
+
firstReadAt: stat.firstReadAt,
|
|
162
|
+
lastReadAt: stat.lastReadAt,
|
|
163
|
+
maxScore: stat.maxScore,
|
|
164
|
+
avgScore: stat.avgScore,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const top = all.filter((r) => r.reads > 0).sort(compareRows);
|
|
170
|
+
const neverRead = all.filter((r) => r.reads === 0).sort((a, b) => a.dzId.localeCompare(b.dzId));
|
|
171
|
+
const unknown = stats.filter((s) => !patternIds.has(s.dzId)).sort(compareStats);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
totalPatterns: all.length,
|
|
175
|
+
usedPatterns: top.length,
|
|
176
|
+
neverReadPatterns: neverRead.length,
|
|
177
|
+
totalReads: stats.reduce((sum, s) => sum + s.reads, 0),
|
|
178
|
+
unknownReadPatterns: unknown.length,
|
|
179
|
+
invalidLines: parsed.invalidLines,
|
|
180
|
+
top,
|
|
181
|
+
neverRead,
|
|
182
|
+
unknown,
|
|
183
|
+
all: all.sort(compareRows),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function shouldCompactRecallUsageLogSize(
|
|
188
|
+
sizeBytes: number,
|
|
189
|
+
maxBytes: number = RECALL_USAGE_LOG_MAX_BYTES,
|
|
190
|
+
): boolean {
|
|
191
|
+
return Number.isFinite(sizeBytes) && sizeBytes > validMax(maxBytes);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function compactRecallUsageLog(
|
|
195
|
+
text: string,
|
|
196
|
+
opts: { readonly maxBytes?: number; readonly targetBytes?: number; readonly compactedAt?: string } = {},
|
|
197
|
+
): string {
|
|
198
|
+
const maxBytes = validMax(opts.maxBytes ?? RECALL_USAGE_LOG_MAX_BYTES);
|
|
199
|
+
const targetBytes = validTarget(opts.targetBytes ?? Math.floor(maxBytes * 0.75), maxBytes);
|
|
200
|
+
const compactedAt = validTs(opts.compactedAt) ? opts.compactedAt : new Date(0).toISOString();
|
|
201
|
+
const stats = aggregateRecallUsage(parseRecallUsageLog(text).records);
|
|
202
|
+
const lines = stats.map((s) => aggregateLine(s, compactedAt));
|
|
203
|
+
let out = joinLines(lines);
|
|
204
|
+
if (byteLength(out) <= maxBytes) return out;
|
|
205
|
+
|
|
206
|
+
const kept: string[] = [];
|
|
207
|
+
let used = 0;
|
|
208
|
+
for (const line of lines) {
|
|
209
|
+
const cost = byteLength(`${line}\n`);
|
|
210
|
+
if (kept.length > 0 && used + cost > targetBytes) continue;
|
|
211
|
+
if (cost > maxBytes) continue;
|
|
212
|
+
if (used + cost <= maxBytes) {
|
|
213
|
+
kept.push(line);
|
|
214
|
+
used += cost;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
out = joinLines(kept);
|
|
218
|
+
return byteLength(out) <= maxBytes ? out : '';
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeRecord(value: unknown): RecallUsageRecord | undefined {
|
|
222
|
+
if (!isRecord(value)) return undefined;
|
|
223
|
+
if (value['kind'] === 'aggregate') return normalizeAggregateRecord(value);
|
|
224
|
+
return normalizeReadRecord(value);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalizeReadRecord(value: unknown): RecallUsageReadRecord | undefined {
|
|
228
|
+
if (!isRecord(value)) return undefined;
|
|
229
|
+
const dzId = value['dzId'];
|
|
230
|
+
const score = value['score'];
|
|
231
|
+
const ts = value['ts'];
|
|
232
|
+
if (typeof dzId !== 'string' || dzId.trim() === '') return undefined;
|
|
233
|
+
if (typeof score !== 'number' || !Number.isFinite(score)) return undefined;
|
|
234
|
+
if (!validTs(ts)) return undefined;
|
|
235
|
+
return { dzId: dzId.trim(), score, ts };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizeAggregateRecord(value: Record<string, unknown>): RecallUsageAggregateRecord | undefined {
|
|
239
|
+
const dzId = value['dzId'];
|
|
240
|
+
const reads = value['reads'];
|
|
241
|
+
const firstReadAt = value['firstReadAt'];
|
|
242
|
+
const lastReadAt = value['lastReadAt'];
|
|
243
|
+
const maxScore = value['maxScore'];
|
|
244
|
+
const totalScore = value['totalScore'];
|
|
245
|
+
const compactedAt = value['compactedAt'];
|
|
246
|
+
if (typeof dzId !== 'string' || dzId.trim() === '') return undefined;
|
|
247
|
+
if (typeof reads !== 'number' || !Number.isInteger(reads) || reads <= 0) return undefined;
|
|
248
|
+
if (!validTs(firstReadAt) || !validTs(lastReadAt) || !validTs(compactedAt)) return undefined;
|
|
249
|
+
if (typeof maxScore !== 'number' || !Number.isFinite(maxScore)) return undefined;
|
|
250
|
+
if (typeof totalScore !== 'number' || !Number.isFinite(totalScore)) return undefined;
|
|
251
|
+
return { kind: 'aggregate', dzId: dzId.trim(), reads, firstReadAt, lastReadAt, maxScore, totalScore, compactedAt };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function mergeRead(byId: Map<string, Acc>, rec: RecallUsageReadRecord): void {
|
|
255
|
+
const ms = Date.parse(rec.ts);
|
|
256
|
+
const prev = byId.get(rec.dzId);
|
|
257
|
+
if (prev === undefined) {
|
|
258
|
+
byId.set(rec.dzId, {
|
|
259
|
+
dzId: rec.dzId,
|
|
260
|
+
reads: 1,
|
|
261
|
+
firstReadAt: rec.ts,
|
|
262
|
+
firstMs: ms,
|
|
263
|
+
lastReadAt: rec.ts,
|
|
264
|
+
lastMs: ms,
|
|
265
|
+
maxScore: rec.score,
|
|
266
|
+
totalScore: rec.score,
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
prev.reads += 1;
|
|
271
|
+
prev.totalScore += rec.score;
|
|
272
|
+
prev.maxScore = Math.max(prev.maxScore, rec.score);
|
|
273
|
+
if (ms < prev.firstMs) {
|
|
274
|
+
prev.firstMs = ms;
|
|
275
|
+
prev.firstReadAt = rec.ts;
|
|
276
|
+
}
|
|
277
|
+
if (ms >= prev.lastMs) {
|
|
278
|
+
prev.lastMs = ms;
|
|
279
|
+
prev.lastReadAt = rec.ts;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function mergeAggregate(byId: Map<string, Acc>, rec: RecallUsageAggregateRecord): void {
|
|
284
|
+
const firstMs = Date.parse(rec.firstReadAt);
|
|
285
|
+
const lastMs = Date.parse(rec.lastReadAt);
|
|
286
|
+
const prev = byId.get(rec.dzId);
|
|
287
|
+
if (prev === undefined) {
|
|
288
|
+
byId.set(rec.dzId, {
|
|
289
|
+
dzId: rec.dzId,
|
|
290
|
+
reads: rec.reads,
|
|
291
|
+
firstReadAt: rec.firstReadAt,
|
|
292
|
+
firstMs,
|
|
293
|
+
lastReadAt: rec.lastReadAt,
|
|
294
|
+
lastMs,
|
|
295
|
+
maxScore: rec.maxScore,
|
|
296
|
+
totalScore: rec.totalScore,
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
prev.reads += rec.reads;
|
|
301
|
+
prev.totalScore += rec.totalScore;
|
|
302
|
+
prev.maxScore = Math.max(prev.maxScore, rec.maxScore);
|
|
303
|
+
if (firstMs < prev.firstMs) {
|
|
304
|
+
prev.firstMs = firstMs;
|
|
305
|
+
prev.firstReadAt = rec.firstReadAt;
|
|
306
|
+
}
|
|
307
|
+
if (lastMs >= prev.lastMs) {
|
|
308
|
+
prev.lastMs = lastMs;
|
|
309
|
+
prev.lastReadAt = rec.lastReadAt;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function aggregateLine(stat: RecallUsageStat, compactedAt: string): string {
|
|
314
|
+
const rec: RecallUsageAggregateRecord = {
|
|
315
|
+
kind: 'aggregate',
|
|
316
|
+
dzId: stat.dzId,
|
|
317
|
+
reads: stat.reads,
|
|
318
|
+
firstReadAt: stat.firstReadAt,
|
|
319
|
+
lastReadAt: stat.lastReadAt,
|
|
320
|
+
maxScore: stat.maxScore,
|
|
321
|
+
totalScore: stat.avgScore * stat.reads,
|
|
322
|
+
compactedAt,
|
|
323
|
+
};
|
|
324
|
+
return JSON.stringify(rec);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function patternRef(p: RecallPatternUsageRef): RecallPatternUsageRef {
|
|
328
|
+
return {
|
|
329
|
+
dzId: p.dzId,
|
|
330
|
+
pattern: p.pattern,
|
|
331
|
+
...(p.domain !== undefined ? { domain: p.domain } : {}),
|
|
332
|
+
...(p.reward !== undefined ? { reward: p.reward } : {}),
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function compareStats(a: RecallUsageStat, b: RecallUsageStat): number {
|
|
337
|
+
return b.reads - a.reads || Date.parse(b.lastReadAt) - Date.parse(a.lastReadAt) || a.dzId.localeCompare(b.dzId);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function compareRows(a: RecallUsagePatternRow, b: RecallUsagePatternRow): number {
|
|
341
|
+
const aLast = a.lastReadAt === undefined ? 0 : Date.parse(a.lastReadAt);
|
|
342
|
+
const bLast = b.lastReadAt === undefined ? 0 : Date.parse(b.lastReadAt);
|
|
343
|
+
return b.reads - a.reads || bLast - aLast || a.dzId.localeCompare(b.dzId);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function isAggregate(rec: RecallUsageRecord): rec is RecallUsageAggregateRecord {
|
|
347
|
+
return 'kind' in rec && rec.kind === 'aggregate';
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
351
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function validTs(value: unknown): value is string {
|
|
355
|
+
return typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Date.parse(value));
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function validMax(value: number): number {
|
|
359
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : RECALL_USAGE_LOG_MAX_BYTES;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function validTarget(value: number, maxBytes: number): number {
|
|
363
|
+
if (!Number.isFinite(value) || value <= 0) return Math.floor(maxBytes * 0.75);
|
|
364
|
+
return Math.min(Math.floor(value), maxBytes);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function joinLines(lines: readonly string[]): string {
|
|
368
|
+
return lines.length === 0 ? '' : `${lines.join('\n')}\n`;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function byteLength(text: string): number {
|
|
372
|
+
return text.length;
|
|
373
|
+
}
|
package/src/statusline.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
19
19
|
import { createRequire } from 'node:module';
|
|
20
20
|
|
|
21
21
|
import { listBrain } from './brain.js';
|
|
22
|
+
import { RECALL_USAGE_LOG_RELATIVE, aggregateRecallUsage, parseRecallUsageLog } from './recall-usage.js';
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Live learning state for one in-flight `/feature-adr` run — the per-run visibility panel
|
|
@@ -49,6 +50,8 @@ export interface FeatureAdrState {
|
|
|
49
50
|
export interface StatuslineData {
|
|
50
51
|
/** Count of learned patterns in the project's unified memory store. */
|
|
51
52
|
readonly patterns: number;
|
|
53
|
+
/** Count of learned patterns that the live recall hook has actually injected at least once. */
|
|
54
|
+
readonly usedPatterns?: number;
|
|
52
55
|
/** Number of sources registered in the durable cross-project knowledge brain. */
|
|
53
56
|
readonly brainSources: number;
|
|
54
57
|
/** Hours since the last `dz consolidate` run, if a watermark is present. */
|
|
@@ -138,6 +141,18 @@ function countLearnedPatterns(projectRoot: string): number {
|
|
|
138
141
|
return countJsonlPatternsReadonly(projectRoot);
|
|
139
142
|
}
|
|
140
143
|
|
|
144
|
+
function countUsedPatternsReadonly(projectRoot: string): number | undefined {
|
|
145
|
+
const path = join(projectRoot, RECALL_USAGE_LOG_RELATIVE);
|
|
146
|
+
if (!existsSync(path)) return undefined;
|
|
147
|
+
try {
|
|
148
|
+
const parsed = parseRecallUsageLog(readFileSync(path, 'utf-8'));
|
|
149
|
+
if (parsed.records.length === 0) return undefined;
|
|
150
|
+
return aggregateRecallUsage(parsed.records).length;
|
|
151
|
+
} catch {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
141
156
|
/** Hours since the last consolidation, or `undefined` when never consolidated / unreadable. */
|
|
142
157
|
function consolidatedAgeHours(projectRoot: string, now: number): number | undefined {
|
|
143
158
|
const path = consolidateWatermarkPath(projectRoot);
|
|
@@ -268,6 +283,13 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
|
|
|
268
283
|
brainSources = 0;
|
|
269
284
|
}
|
|
270
285
|
|
|
286
|
+
let usedPatterns: number | undefined;
|
|
287
|
+
try {
|
|
288
|
+
usedPatterns = countUsedPatternsReadonly(root);
|
|
289
|
+
} catch {
|
|
290
|
+
usedPatterns = undefined;
|
|
291
|
+
}
|
|
292
|
+
|
|
271
293
|
const ageH = consolidatedAgeHours(root, now);
|
|
272
294
|
|
|
273
295
|
// Live /feature-adr panel — attached ONLY when a fresh run is in flight (readonly, never throws).
|
|
@@ -280,6 +302,7 @@ export function statuslineData(projectRoot: string, now: number = Date.now()): S
|
|
|
280
302
|
|
|
281
303
|
return {
|
|
282
304
|
patterns,
|
|
305
|
+
...(usedPatterns !== undefined ? { usedPatterns } : {}),
|
|
283
306
|
brainSources,
|
|
284
307
|
...(ageH !== undefined ? { consolidatedAgeH: ageH } : {}),
|
|
285
308
|
...(featureAdr !== undefined ? { featureAdr } : {}),
|