@dzhechkov/harness-core 0.8.2 → 0.8.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/.dz-manifest.json +146 -58
  2. package/README.md +72 -2
  3. package/dist/cmd-usage.d.ts +148 -0
  4. package/dist/cmd-usage.d.ts.map +1 -0
  5. package/dist/cmd-usage.js +548 -0
  6. package/dist/cmd-usage.js.map +1 -0
  7. package/dist/compounding.d.ts +4 -0
  8. package/dist/compounding.d.ts.map +1 -1
  9. package/dist/compounding.js +6 -0
  10. package/dist/compounding.js.map +1 -1
  11. package/dist/contract-checklist.d.ts +123 -0
  12. package/dist/contract-checklist.d.ts.map +1 -0
  13. package/dist/contract-checklist.js +700 -0
  14. package/dist/contract-checklist.js.map +1 -0
  15. package/dist/feature-adr-checkpoints.d.ts +11 -2
  16. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  17. package/dist/feature-adr-checkpoints.js +37 -2
  18. package/dist/feature-adr-checkpoints.js.map +1 -1
  19. package/dist/feature-adr-routing.d.ts +58 -23
  20. package/dist/feature-adr-routing.d.ts.map +1 -1
  21. package/dist/feature-adr-routing.js +208 -59
  22. package/dist/feature-adr-routing.js.map +1 -1
  23. package/dist/guard.d.ts +25 -0
  24. package/dist/guard.d.ts.map +1 -1
  25. package/dist/guard.js +59 -1
  26. package/dist/guard.js.map +1 -1
  27. package/dist/index.d.ts +9 -2
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +8 -1
  30. package/dist/index.js.map +1 -1
  31. package/dist/loop-blobs.generated.js +8 -8
  32. package/dist/loop-blobs.generated.js.map +1 -1
  33. package/dist/loop-plan.d.ts +13 -1
  34. package/dist/loop-plan.d.ts.map +1 -1
  35. package/dist/loop-plan.js +15 -1
  36. package/dist/loop-plan.js.map +1 -1
  37. package/dist/loop-render.d.ts.map +1 -1
  38. package/dist/loop-render.js +51 -6
  39. package/dist/loop-render.js.map +1 -1
  40. package/dist/loop-trace.d.ts +20 -1
  41. package/dist/loop-trace.d.ts.map +1 -1
  42. package/dist/loop-trace.js +83 -1
  43. package/dist/loop-trace.js.map +1 -1
  44. package/dist/model-recommender.d.ts +8 -0
  45. package/dist/model-recommender.d.ts.map +1 -1
  46. package/dist/model-recommender.js +31 -4
  47. package/dist/model-recommender.js.map +1 -1
  48. package/dist/qe-bridge.d.ts.map +1 -1
  49. package/dist/qe-bridge.js +9 -0
  50. package/dist/qe-bridge.js.map +1 -1
  51. package/dist/restart-advisor.d.ts +103 -0
  52. package/dist/restart-advisor.d.ts.map +1 -0
  53. package/dist/restart-advisor.js +445 -0
  54. package/dist/restart-advisor.js.map +1 -0
  55. package/dist/slop-lint.d.ts +128 -0
  56. package/dist/slop-lint.d.ts.map +1 -0
  57. package/dist/slop-lint.js +607 -0
  58. package/dist/slop-lint.js.map +1 -0
  59. package/dist/workflow-run.d.ts.map +1 -1
  60. package/dist/workflow-run.js +18 -12
  61. package/dist/workflow-run.js.map +1 -1
  62. package/package.json +19 -15
  63. package/sbom.json +277 -57
  64. package/src/cmd-usage.ts +720 -0
  65. package/src/compounding.ts +13 -0
  66. package/src/contract-checklist.ts +973 -0
  67. package/src/deadwood-allowlist.json +80 -0
  68. package/src/feature-adr-checkpoints.ts +38 -2
  69. package/src/feature-adr-routing.ts +238 -55
  70. package/src/guard.ts +79 -1
  71. package/src/index.ts +81 -1
  72. package/src/loop-blobs.generated.ts +8 -8
  73. package/src/loop-plan.ts +36 -3
  74. package/src/loop-render.ts +50 -6
  75. package/src/loop-trace.ts +91 -2
  76. package/src/model-recommender.ts +35 -4
  77. package/src/qe-bridge.ts +9 -0
  78. package/src/restart-advisor.ts +579 -0
  79. package/src/slop-lint.ts +762 -0
  80. package/src/slop-markers.json +71 -0
  81. package/src/workflow-run.ts +18 -11
@@ -0,0 +1,720 @@
1
+ /**
2
+ * Command-invocation accounting for `dz deadwood`.
3
+ *
4
+ * The write leg is a single best-effort append. The report leg is pure: callers
5
+ * inject log text, inventory, an allowlist, and a clock. A usage report is
6
+ * advisory only and never mutates the harness surface it describes.
7
+ *
8
+ * @packageDocumentation
9
+ */
10
+
11
+ // Static node imports are load-bearing. A deferred `require` in the earlier
12
+ // recall-usage seam compiled into ESM without `require`, then failed inside its
13
+ // own never-block catch and silently wrote no evidence.
14
+ import {
15
+ appendFileSync,
16
+ existsSync,
17
+ readFileSync,
18
+ renameSync,
19
+ statSync,
20
+ writeFileSync,
21
+ } from 'node:fs';
22
+ import { dirname, join, resolve } from 'node:path';
23
+
24
+ import { withNamedLockSync } from './named-lock.js';
25
+
26
+ export const CMD_USAGE_LOG_RELATIVE = '.dz/cmd-usage.jsonl';
27
+ export const CMD_USAGE_LOG_MAX_BYTES = 1_048_576;
28
+ export const CMD_USAGE_COMPACT_TARGET_BYTES = Math.floor(CMD_USAGE_LOG_MAX_BYTES * 0.75);
29
+ export const CMD_USAGE_SCHEMA = 1;
30
+ export const CMD_USAGE_V1_EPOCH_MS = Date.parse('2026-08-28T00:00:00.000Z');
31
+ export const DEADWOOD_FUTURE_TOLERANCE_MS = 86_400_000;
32
+ export const DEADWOOD_MIN_OBSERVED_DAYS = 28;
33
+ export const DEADWOOD_MIN_RECORDS = 100;
34
+
35
+ const DAY_MS = 86_400_000;
36
+
37
+ export type DeadwoodSurfaceKind = 'command' | 'skill' | 'rule';
38
+
39
+ export interface CmdUsageInvocationRecord {
40
+ readonly kind: 'cmd';
41
+ readonly cmd: string;
42
+ readonly ts: string;
43
+ readonly v: 1;
44
+ }
45
+
46
+ export interface CmdUsageAggregateRecord {
47
+ readonly kind: 'agg';
48
+ readonly cmd: string;
49
+ readonly runs: number;
50
+ readonly firstAt: string;
51
+ readonly lastAt: string;
52
+ readonly v: 1;
53
+ }
54
+
55
+ export type CmdUsageRecord = CmdUsageInvocationRecord | CmdUsageAggregateRecord;
56
+
57
+ export interface ParsedCmdUsage {
58
+ readonly records: readonly CmdUsageRecord[];
59
+ readonly skipped: number;
60
+ readonly outOfRange: number;
61
+ }
62
+
63
+ export interface CmdUsageStat {
64
+ readonly cmd: string;
65
+ readonly runs: number;
66
+ readonly runsInWindow: number;
67
+ readonly firstAt: string;
68
+ readonly lastAt: string;
69
+ }
70
+
71
+ export interface DeadwoodAllowlistEntry {
72
+ readonly surface: string;
73
+ readonly kind: DeadwoodSurfaceKind;
74
+ readonly reason: string;
75
+ }
76
+
77
+ export interface DeadwoodInventoryItem {
78
+ readonly surface: string;
79
+ readonly kind: DeadwoodSurfaceKind;
80
+ /** Alternate typed command tokens folded into this canonical command candidate. */
81
+ readonly aliases?: readonly string[];
82
+ }
83
+
84
+ export type DeadwoodVerdict = 'insufficient-data' | 'ready';
85
+
86
+ export interface DeadwoodUsedSurface {
87
+ readonly state: 'used';
88
+ readonly surface: string;
89
+ readonly kind: Exclude<DeadwoodSurfaceKind, 'skill'>;
90
+ readonly runs: number;
91
+ readonly runsInWindow: number;
92
+ readonly firstAt: string;
93
+ readonly lastAt: string;
94
+ }
95
+
96
+ export interface DeadwoodCandidate {
97
+ readonly state: 'zero-usage';
98
+ readonly surface: string;
99
+ readonly kind: Exclude<DeadwoodSurfaceKind, 'skill'>;
100
+ readonly runs: number;
101
+ readonly runsInWindow: 0;
102
+ readonly firstAt: string | null;
103
+ readonly lastAt: string | null;
104
+ }
105
+
106
+ export interface DeadwoodExemptSurface {
107
+ readonly state: 'excluded-safety';
108
+ readonly surface: string;
109
+ readonly kind: Exclude<DeadwoodSurfaceKind, 'skill'>;
110
+ readonly reason: string;
111
+ readonly runs: number;
112
+ readonly runsInWindow: 0;
113
+ readonly firstAt: string | null;
114
+ readonly lastAt: string | null;
115
+ }
116
+
117
+ export interface DeadwoodUnjudgedSurface {
118
+ readonly state: 'insufficient-data' | 'no-instrumentation';
119
+ readonly surface: string;
120
+ readonly kind: DeadwoodSurfaceKind;
121
+ readonly reason: string;
122
+ }
123
+
124
+ export interface DeadwoodReport {
125
+ readonly verdict: DeadwoodVerdict;
126
+ readonly windowWeeks: number;
127
+ readonly observedDays: number;
128
+ readonly recordCount: number;
129
+ readonly ruleObservedDays: number;
130
+ readonly ruleAuditCount: number;
131
+ readonly candidates: readonly DeadwoodCandidate[];
132
+ readonly exempt: readonly DeadwoodExemptSurface[];
133
+ readonly used: readonly DeadwoodUsedSurface[];
134
+ readonly insufficient: readonly DeadwoodUnjudgedSurface[];
135
+ readonly noInstrumentation: readonly DeadwoodUnjudgedSurface[];
136
+ readonly staleAllowlist: readonly DeadwoodAllowlistEntry[];
137
+ readonly skippedLines: number;
138
+ readonly outOfRange: number;
139
+ }
140
+
141
+ export interface DeadwoodInput {
142
+ readonly cmdUsageText: string;
143
+ readonly guardAuditText?: string;
144
+ readonly inventory: readonly DeadwoodInventoryItem[];
145
+ readonly allowlistText: string;
146
+ readonly weeks: number;
147
+ readonly now: Date;
148
+ }
149
+
150
+ interface RuleUsage {
151
+ readonly auditTimestamps: readonly string[];
152
+ readonly stats: ReadonlyMap<string, CmdUsageStat>;
153
+ readonly skipped: number;
154
+ readonly outOfRange: number;
155
+ }
156
+
157
+ /** Walk up to the nearest `.dz` directory without ever making telemetry throw. */
158
+ export function resolveCmdUsageRoot(startDir: string): string {
159
+ try {
160
+ let dir = resolve(startDir);
161
+ for (let level = 0; level < 64; level += 1) {
162
+ try {
163
+ if (statSync(join(dir, '.dz')).isDirectory()) return dir;
164
+ } catch {
165
+ /* not here; keep walking */
166
+ }
167
+ const parent = dirname(dir);
168
+ if (parent === dir) return startDir;
169
+ dir = parent;
170
+ }
171
+ } catch {
172
+ /* fail open to the caller-provided root */
173
+ }
174
+ return startDir;
175
+ }
176
+
177
+ /** One append per parsed command, with no argv/cwd/env payload. Never throws. */
178
+ export function recordCommandInvocation(root: string, cmd: string, now: Date = new Date()): void {
179
+ try {
180
+ const resolvedRoot = resolveCmdUsageRoot(root);
181
+ if (cmd === '' || cmd === 'deadwood') return;
182
+ if (!existsSync(join(resolvedRoot, '.dz'))) return;
183
+ const record: CmdUsageInvocationRecord = {
184
+ kind: 'cmd',
185
+ cmd,
186
+ ts: now.toISOString(),
187
+ v: CMD_USAGE_SCHEMA,
188
+ };
189
+ appendFileSync(join(resolvedRoot, CMD_USAGE_LOG_RELATIVE), `${JSON.stringify(record)}\n`, 'utf8');
190
+ } catch {
191
+ /* advisory telemetry must never affect the command it observes */
192
+ }
193
+ }
194
+
195
+ /** ADR-compatible name retained for callers/tests written before the plan renamed the seam. */
196
+ export const appendCommandUsage = recordCommandInvocation;
197
+
198
+ function validTimestamp(value: unknown): value is string {
199
+ return typeof value === 'string' && Number.isFinite(Date.parse(value));
200
+ }
201
+
202
+ function timestampRange(record: CmdUsageRecord): { first: number; last: number } {
203
+ if (record.kind === 'cmd') {
204
+ const ms = Date.parse(record.ts);
205
+ return { first: ms, last: ms };
206
+ }
207
+ return { first: Date.parse(record.firstAt), last: Date.parse(record.lastAt) };
208
+ }
209
+
210
+ function parseCmdRecord(value: unknown): CmdUsageRecord | null {
211
+ if (typeof value !== 'object' || value === null) return null;
212
+ const row = value as Record<string, unknown>;
213
+ if (row.v !== CMD_USAGE_SCHEMA || typeof row.cmd !== 'string' || row.cmd.trim() === '') return null;
214
+ if (row.kind === 'cmd' && validTimestamp(row.ts)) {
215
+ return { kind: 'cmd', cmd: row.cmd, ts: row.ts, v: CMD_USAGE_SCHEMA };
216
+ }
217
+ if (
218
+ row.kind === 'agg' &&
219
+ Number.isInteger(row.runs) &&
220
+ typeof row.runs === 'number' &&
221
+ row.runs > 0 &&
222
+ validTimestamp(row.firstAt) &&
223
+ validTimestamp(row.lastAt) &&
224
+ Date.parse(row.firstAt) <= Date.parse(row.lastAt)
225
+ ) {
226
+ return {
227
+ kind: 'agg',
228
+ cmd: row.cmd,
229
+ runs: row.runs,
230
+ firstAt: row.firstAt,
231
+ lastAt: row.lastAt,
232
+ v: CMD_USAGE_SCHEMA,
233
+ };
234
+ }
235
+ return null;
236
+ }
237
+
238
+ /** Parse independently per line; torn or schema-invalid rows are counted and skipped. */
239
+ export function parseCmdUsageLines(text: string, now: Date): ParsedCmdUsage {
240
+ const records: CmdUsageRecord[] = [];
241
+ let skipped = 0;
242
+ let outOfRange = 0;
243
+ const newestAllowed = now.getTime() + DEADWOOD_FUTURE_TOLERANCE_MS;
244
+ for (const line of text.split('\n')) {
245
+ if (line.trim() === '') continue;
246
+ let record: CmdUsageRecord | null = null;
247
+ try {
248
+ record = parseCmdRecord(JSON.parse(line) as unknown);
249
+ } catch {
250
+ record = null;
251
+ }
252
+ if (record === null) {
253
+ skipped += 1;
254
+ continue;
255
+ }
256
+ const { first, last } = timestampRange(record);
257
+ if (first < CMD_USAGE_V1_EPOCH_MS || last > newestAllowed) {
258
+ outOfRange += 1;
259
+ continue;
260
+ }
261
+ records.push(record);
262
+ }
263
+ return { records, skipped, outOfRange };
264
+ }
265
+
266
+ function recordWeight(record: CmdUsageRecord): number {
267
+ return record.kind === 'agg' ? record.runs : 1;
268
+ }
269
+
270
+ function observedDays(records: readonly CmdUsageRecord[], now: Date): number {
271
+ let first = Number.POSITIVE_INFINITY;
272
+ for (const record of records) first = Math.min(first, timestampRange(record).first);
273
+ return Number.isFinite(first) ? Math.max(0, (now.getTime() - first) / DAY_MS) : 0;
274
+ }
275
+
276
+ function recordCount(records: readonly CmdUsageRecord[]): number {
277
+ return records.reduce((sum, record) => sum + recordWeight(record), 0);
278
+ }
279
+
280
+ /** Measured corpus span (first accepted event to last), or null when no event can be read. */
281
+ export function measureCmdUsageDepthDays(text: string, now: Date): number | null {
282
+ const parsed = parseCmdUsageLines(text, now);
283
+ let first = Number.POSITIVE_INFINITY;
284
+ let last = Number.NEGATIVE_INFINITY;
285
+ for (const record of parsed.records) {
286
+ const range = timestampRange(record);
287
+ first = Math.min(first, range.first);
288
+ last = Math.max(last, range.last);
289
+ }
290
+ return Number.isFinite(first) && Number.isFinite(last)
291
+ ? Math.max(0, (last - first) / DAY_MS)
292
+ : null;
293
+ }
294
+
295
+ /** A verdict needs both a meaningful sample and history spanning the requested window. */
296
+ export function decideDeadwoodWindow(
297
+ depthDays: number,
298
+ count: number,
299
+ weeks: number,
300
+ ): { ok: boolean; reason: string } {
301
+ const requestedDays = Number.isFinite(weeks) && weeks > 0 ? weeks * 7 : Number.POSITIVE_INFINITY;
302
+ const requiredDays = Math.max(DEADWOOD_MIN_OBSERVED_DAYS, requestedDays);
303
+ if (!Number.isFinite(depthDays) || depthDays < requiredDays) {
304
+ return {
305
+ ok: false,
306
+ reason: `history is ${Math.max(0, Math.floor(depthDays))}d deep; ${requiredDays}d required for this window`,
307
+ };
308
+ }
309
+ if (!Number.isFinite(count) || count < DEADWOOD_MIN_RECORDS) {
310
+ return { ok: false, reason: `${Math.max(0, Math.floor(count))} records; ${DEADWOOD_MIN_RECORDS} required` };
311
+ }
312
+ return { ok: true, reason: 'enough history and records for an advisory classification' };
313
+ }
314
+
315
+ /** Fold raw and compacted rows into the same per-command accounting shape. */
316
+ export function foldCmdUsage(
317
+ records: readonly CmdUsageRecord[],
318
+ weeks: number,
319
+ now: Date,
320
+ ): Map<string, CmdUsageStat> {
321
+ const windowStart = now.getTime() - Math.max(0, weeks) * 7 * DAY_MS;
322
+ const mutable = new Map<string, { runs: number; runsInWindow: number; firstAt: string; lastAt: string }>();
323
+ for (const record of records) {
324
+ const firstAt = record.kind === 'agg' ? record.firstAt : record.ts;
325
+ const lastAt = record.kind === 'agg' ? record.lastAt : record.ts;
326
+ const runs = recordWeight(record);
327
+ const runsInWindow = Date.parse(lastAt) >= windowStart ? runs : 0;
328
+ const prior = mutable.get(record.cmd);
329
+ if (prior === undefined) {
330
+ mutable.set(record.cmd, { runs, runsInWindow, firstAt, lastAt });
331
+ continue;
332
+ }
333
+ prior.runs += runs;
334
+ prior.runsInWindow += runsInWindow;
335
+ if (Date.parse(firstAt) < Date.parse(prior.firstAt)) prior.firstAt = firstAt;
336
+ if (Date.parse(lastAt) > Date.parse(prior.lastAt)) prior.lastAt = lastAt;
337
+ }
338
+ return new Map(
339
+ [...mutable.entries()].map(([cmd, stat]) => [cmd, { cmd, ...stat }]),
340
+ );
341
+ }
342
+
343
+ /** Parse the committed safety allowlist; a reason-less exemption is refused, never skipped. */
344
+ export function loadDeadwoodAllowlist(json: string): DeadwoodAllowlistEntry[] {
345
+ const parsed = JSON.parse(json) as unknown;
346
+ const entries = Array.isArray(parsed)
347
+ ? parsed
348
+ : typeof parsed === 'object' && parsed !== null && Array.isArray((parsed as { entries?: unknown }).entries)
349
+ ? (parsed as { entries: unknown[] }).entries
350
+ : null;
351
+ if (entries === null) throw new Error('deadwood allowlist must be an array or an object with entries[]');
352
+ return entries.map((value, index) => {
353
+ const row = typeof value === 'object' && value !== null ? value as Record<string, unknown> : {};
354
+ const surface = typeof row.surface === 'string'
355
+ ? row.surface
356
+ : typeof row.name === 'string' ? row.name : `<entry ${index}>`;
357
+ const kind = row.kind;
358
+ const reason = row.reason;
359
+ if (surface.trim() === '') throw new Error(`deadwood allowlist entry ${index} has an empty surface`);
360
+ if (kind !== 'command' && kind !== 'skill' && kind !== 'rule') {
361
+ throw new Error(`deadwood allowlist entry ${surface} has invalid kind`);
362
+ }
363
+ if (typeof reason !== 'string' || reason.trim() === '') {
364
+ throw new Error(`deadwood allowlist entry ${surface} requires a non-empty reason`);
365
+ }
366
+ return { surface, kind, reason: reason.trim() };
367
+ });
368
+ }
369
+
370
+ function parseGuardAuditUsage(text: string, weeks: number, now: Date): RuleUsage {
371
+ const auditTimestamps: string[] = [];
372
+ const hits: CmdUsageInvocationRecord[] = [];
373
+ let skipped = 0;
374
+ let outOfRange = 0;
375
+ const newestAllowed = now.getTime() + DEADWOOD_FUTURE_TOLERANCE_MS;
376
+ for (const line of text.split('\n')) {
377
+ if (line.trim() === '') continue;
378
+ let row: Record<string, unknown>;
379
+ try {
380
+ const value = JSON.parse(line) as unknown;
381
+ if (typeof value !== 'object' || value === null) throw new Error('not an object');
382
+ row = value as Record<string, unknown>;
383
+ } catch {
384
+ skipped += 1;
385
+ continue;
386
+ }
387
+ if (!validTimestamp(row.ts)) {
388
+ skipped += 1;
389
+ continue;
390
+ }
391
+ const tsMs = Date.parse(row.ts);
392
+ if (tsMs < CMD_USAGE_V1_EPOCH_MS || tsMs > newestAllowed) {
393
+ outOfRange += 1;
394
+ continue;
395
+ }
396
+ auditTimestamps.push(row.ts);
397
+ const violations = Array.isArray(row.violations) ? row.violations : [];
398
+ for (const value of violations) {
399
+ const rule = typeof value === 'object' && value !== null
400
+ ? (value as { rule?: unknown }).rule
401
+ : undefined;
402
+ if (typeof rule === 'string' && rule.trim() !== '') {
403
+ hits.push({ kind: 'cmd', cmd: rule, ts: row.ts, v: CMD_USAGE_SCHEMA });
404
+ }
405
+ }
406
+ }
407
+ return { auditTimestamps, stats: foldCmdUsage(hits, weeks, now), skipped, outOfRange };
408
+ }
409
+
410
+ function timestampDepthDays(timestamps: readonly string[], now: Date): number {
411
+ let first = Number.POSITIVE_INFINITY;
412
+ for (const ts of timestamps) first = Math.min(first, Date.parse(ts));
413
+ return Number.isFinite(first) ? Math.max(0, (now.getTime() - first) / DAY_MS) : 0;
414
+ }
415
+
416
+ function allowlistKey(kind: DeadwoodSurfaceKind, surface: string): string {
417
+ return `${kind}\u0000${surface}`;
418
+ }
419
+
420
+ function emptyReport(
421
+ input: DeadwoodInput,
422
+ parsed: ParsedCmdUsage,
423
+ rules: RuleUsage,
424
+ allowlist: readonly DeadwoodAllowlistEntry[],
425
+ ): DeadwoodReport {
426
+ const inventoryKeys = new Set(input.inventory.map((item) => allowlistKey(item.kind, item.surface)));
427
+ const insufficient: DeadwoodUnjudgedSurface[] = [];
428
+ const noInstrumentation: DeadwoodUnjudgedSurface[] = [];
429
+ for (const item of input.inventory) {
430
+ if (item.kind === 'skill') {
431
+ noInstrumentation.push({
432
+ state: 'no-instrumentation',
433
+ surface: item.surface,
434
+ kind: item.kind,
435
+ reason: 'no skill-invocation signal exists in dz; this report cannot judge the skill',
436
+ });
437
+ } else {
438
+ insufficient.push({
439
+ state: 'insufficient-data',
440
+ surface: item.surface,
441
+ kind: item.kind,
442
+ reason: 'the command-usage corpus has not reached the requested history and sample floor',
443
+ });
444
+ }
445
+ }
446
+ return {
447
+ verdict: 'insufficient-data',
448
+ windowWeeks: input.weeks,
449
+ observedDays: observedDays(parsed.records, input.now),
450
+ recordCount: recordCount(parsed.records),
451
+ ruleObservedDays: timestampDepthDays(rules.auditTimestamps, input.now),
452
+ ruleAuditCount: rules.auditTimestamps.length,
453
+ candidates: [],
454
+ exempt: [],
455
+ used: [],
456
+ insufficient,
457
+ noInstrumentation,
458
+ staleAllowlist: allowlist.filter((entry) => !inventoryKeys.has(allowlistKey(entry.kind, entry.surface))),
459
+ skippedLines: parsed.skipped + rules.skipped,
460
+ outOfRange: parsed.outOfRange + rules.outOfRange,
461
+ };
462
+ }
463
+
464
+ function classifyInstrumented(
465
+ item: DeadwoodInventoryItem & { kind: 'command' | 'rule' },
466
+ stat: CmdUsageStat | undefined,
467
+ allowlist: ReadonlyMap<string, DeadwoodAllowlistEntry>,
468
+ destination: {
469
+ used: DeadwoodUsedSurface[];
470
+ candidates: DeadwoodCandidate[];
471
+ exempt: DeadwoodExemptSurface[];
472
+ },
473
+ ): void {
474
+ if (stat !== undefined && stat.runsInWindow > 0) {
475
+ destination.used.push({
476
+ state: 'used',
477
+ ...item,
478
+ runs: stat.runs,
479
+ runsInWindow: stat.runsInWindow,
480
+ firstAt: stat.firstAt,
481
+ lastAt: stat.lastAt,
482
+ });
483
+ return;
484
+ }
485
+ const runs = stat?.runs ?? 0;
486
+ const firstAt = stat?.firstAt ?? null;
487
+ const lastAt = stat?.lastAt ?? null;
488
+ const allowed = allowlist.get(allowlistKey(item.kind, item.surface));
489
+ if (allowed !== undefined) {
490
+ destination.exempt.push({
491
+ state: 'excluded-safety',
492
+ ...item,
493
+ reason: allowed.reason,
494
+ runs,
495
+ runsInWindow: 0,
496
+ firstAt,
497
+ lastAt,
498
+ });
499
+ return;
500
+ }
501
+ destination.candidates.push({
502
+ state: 'zero-usage',
503
+ ...item,
504
+ runs,
505
+ runsInWindow: 0,
506
+ firstAt,
507
+ lastAt,
508
+ });
509
+ }
510
+
511
+ function commandStatForInventoryItem(
512
+ item: DeadwoodInventoryItem,
513
+ stats: ReadonlyMap<string, CmdUsageStat>,
514
+ ): CmdUsageStat | undefined {
515
+ const tokens = new Set([item.surface, ...(item.aliases ?? [])]);
516
+ let merged: CmdUsageStat | undefined;
517
+ for (const token of tokens) {
518
+ const stat = stats.get(token);
519
+ if (stat === undefined) continue;
520
+ if (merged === undefined) {
521
+ merged = { ...stat, cmd: item.surface };
522
+ continue;
523
+ }
524
+ merged = {
525
+ cmd: item.surface,
526
+ runs: merged.runs + stat.runs,
527
+ runsInWindow: merged.runsInWindow + stat.runsInWindow,
528
+ firstAt: Date.parse(merged.firstAt) <= Date.parse(stat.firstAt) ? merged.firstAt : stat.firstAt,
529
+ lastAt: Date.parse(merged.lastAt) >= Date.parse(stat.lastAt) ? merged.lastAt : stat.lastAt,
530
+ };
531
+ }
532
+ return merged;
533
+ }
534
+
535
+ /** Build the single structure consumed by both the human and JSON renderers. */
536
+ export function buildDeadwoodReport(input: DeadwoodInput): DeadwoodReport {
537
+ const allowlistEntries = loadDeadwoodAllowlist(input.allowlistText);
538
+ const allowlist = new Map(
539
+ allowlistEntries.map((entry) => [allowlistKey(entry.kind, entry.surface), entry]),
540
+ );
541
+ const parsed = parseCmdUsageLines(input.cmdUsageText, input.now);
542
+ const rules = parseGuardAuditUsage(input.guardAuditText ?? '', input.weeks, input.now);
543
+ const depth = observedDays(parsed.records, input.now);
544
+ const count = recordCount(parsed.records);
545
+ if (!decideDeadwoodWindow(depth, count, input.weeks).ok) {
546
+ return emptyReport(input, parsed, rules, allowlistEntries);
547
+ }
548
+
549
+ const commandStats = foldCmdUsage(parsed.records, input.weeks, input.now);
550
+ const ruleDepth = timestampDepthDays(rules.auditTimestamps, input.now);
551
+ const ruleWindow = decideDeadwoodWindow(ruleDepth, rules.auditTimestamps.length, input.weeks);
552
+ const candidates: DeadwoodCandidate[] = [];
553
+ const exempt: DeadwoodExemptSurface[] = [];
554
+ const used: DeadwoodUsedSurface[] = [];
555
+ const insufficient: DeadwoodUnjudgedSurface[] = [];
556
+ const noInstrumentation: DeadwoodUnjudgedSurface[] = [];
557
+ const inventoryKeys = new Set<string>();
558
+
559
+ for (const item of [...input.inventory].sort((a, b) =>
560
+ a.kind.localeCompare(b.kind) || a.surface.localeCompare(b.surface))) {
561
+ inventoryKeys.add(allowlistKey(item.kind, item.surface));
562
+ if (item.kind === 'skill') {
563
+ noInstrumentation.push({
564
+ state: 'no-instrumentation',
565
+ surface: item.surface,
566
+ kind: item.kind,
567
+ reason: 'no skill-invocation signal exists in dz; this report cannot judge the skill',
568
+ });
569
+ continue;
570
+ }
571
+ if (item.kind === 'rule' && !ruleWindow.ok) {
572
+ insufficient.push({
573
+ state: 'insufficient-data',
574
+ ...item,
575
+ reason: `guard-audit cannot support this window: ${ruleWindow.reason}`,
576
+ });
577
+ continue;
578
+ }
579
+ classifyInstrumented(
580
+ { surface: item.surface, kind: item.kind },
581
+ item.kind === 'command'
582
+ ? commandStatForInventoryItem(item, commandStats)
583
+ : rules.stats.get(item.surface),
584
+ allowlist,
585
+ { candidates, exempt, used },
586
+ );
587
+ }
588
+
589
+ return {
590
+ verdict: 'ready',
591
+ windowWeeks: input.weeks,
592
+ observedDays: depth,
593
+ recordCount: count,
594
+ ruleObservedDays: ruleDepth,
595
+ ruleAuditCount: rules.auditTimestamps.length,
596
+ candidates,
597
+ exempt,
598
+ used,
599
+ insufficient,
600
+ noInstrumentation,
601
+ staleAllowlist: allowlistEntries.filter((entry) =>
602
+ !inventoryKeys.has(allowlistKey(entry.kind, entry.surface))),
603
+ skippedLines: parsed.skipped + rules.skipped,
604
+ outOfRange: parsed.outOfRange + rules.outOfRange,
605
+ };
606
+ }
607
+
608
+ function renderSurfaceLabel(item: { kind: DeadwoodSurfaceKind; surface: string }): string {
609
+ return `${item.kind} ${item.surface}`;
610
+ }
611
+
612
+ const ADVISORY_CAVEAT =
613
+ 'zero usage != useless — this list is a candidate for a deprecation mark in --help, not for deletion.';
614
+
615
+ /** Render one report object as either human text or stable JSON. */
616
+ export function renderDeadwoodReport(report: DeadwoodReport, format: 'text' | 'json'): string {
617
+ if (format === 'json') return JSON.stringify(report, null, 2);
618
+ const out: string[] = [
619
+ 'dz deadwood — advisory unused harness surface report',
620
+ `window: ${report.windowWeeks} week(s) · command history ${Math.floor(report.observedDays)}d / ${report.recordCount} invocation(s)`,
621
+ ];
622
+ if (report.verdict === 'insufficient-data') {
623
+ out.push('VERDICT: INSUFFICIENT_DATA');
624
+ out.push(` accruing: ${Math.floor(report.observedDays)}d / ${report.recordCount} command record(s)`);
625
+ out.push(` ${ADVISORY_CAVEAT}`);
626
+ out.push(` ${report.noInstrumentation.length} skill(s): no invocation signal; not judged`);
627
+ if (report.skippedLines > 0 || report.outOfRange > 0) {
628
+ out.push(` integrity: ${report.skippedLines} skipped malformed line(s) · ${report.outOfRange} out-of-range timestamp(s)`);
629
+ }
630
+ return out.join('\n');
631
+ }
632
+
633
+ out.push(`VERDICT: ${report.candidates.length === 0 ? 'NO_DEADWOOD' : 'CANDIDATES'}`);
634
+ out.push('');
635
+ out.push('DEPRECATION CANDIDATES (human review required):');
636
+ if (report.candidates.length === 0) out.push(' none');
637
+ for (const item of report.candidates) {
638
+ out.push(` zero-usage · ${renderSurfaceLabel(item)} · runs(all-time)=${item.runs} · last=${item.lastAt ?? 'never'}`);
639
+ out.push(` ${ADVISORY_CAVEAT}`);
640
+ }
641
+ out.push('');
642
+ out.push('SAFETY EXCLUDED:');
643
+ if (report.exempt.length === 0) out.push(' none');
644
+ for (const item of report.exempt) {
645
+ out.push(` excluded-safety(${item.reason}) · ${renderSurfaceLabel(item)}`);
646
+ }
647
+ out.push('');
648
+ out.push(`USED: ${report.used.length}`);
649
+ for (const item of report.used) {
650
+ out.push(` used · ${renderSurfaceLabel(item)} · ${item.runsInWindow} run(s) in window`);
651
+ }
652
+ out.push(`INSUFFICIENT SIGNAL: ${report.insufficient.length}`);
653
+ out.push(`NO INSTRUMENTATION: ${report.noInstrumentation.length} skill(s) — not candidates`);
654
+ out.push(`INTEGRITY: ${report.skippedLines} skipped malformed line(s) · ${report.outOfRange} out-of-range timestamp(s)`);
655
+ if (report.staleAllowlist.length > 0) {
656
+ out.push(`STALE ALLOWLIST: ${report.staleAllowlist.map(renderSurfaceLabel).join(', ')}`);
657
+ }
658
+ return out.join('\n');
659
+ }
660
+
661
+ function compactedRows(records: readonly CmdUsageRecord[]): CmdUsageAggregateRecord[] {
662
+ return [...foldCmdUsage(records, 0, new Date(0)).values()]
663
+ .sort((a, b) => a.cmd.localeCompare(b.cmd))
664
+ .map((stat) => ({
665
+ kind: 'agg',
666
+ cmd: stat.cmd,
667
+ runs: stat.runs,
668
+ firstAt: stat.firstAt,
669
+ lastAt: stat.lastAt,
670
+ v: CMD_USAGE_SCHEMA,
671
+ }));
672
+ }
673
+
674
+ function compactedText(records: readonly CmdUsageRecord[]): string {
675
+ const ordered = [...records].sort((a, b) =>
676
+ timestampRange(a).last - timestampRange(b).last || a.cmd.localeCompare(b.cmd));
677
+ const recent: CmdUsageRecord[] = [];
678
+ let recentBytes = 0;
679
+ const recentBudget = Math.floor(CMD_USAGE_COMPACT_TARGET_BYTES / 2);
680
+ for (let index = ordered.length - 1; index >= 0; index -= 1) {
681
+ const record = ordered[index]!;
682
+ const bytes = Buffer.byteLength(JSON.stringify(record), 'utf8') + 1;
683
+ if (recentBytes + bytes > recentBudget) break;
684
+ recent.unshift(record);
685
+ recentBytes += bytes;
686
+ }
687
+ const older = ordered.slice(0, ordered.length - recent.length);
688
+ const rows: CmdUsageRecord[] = [...compactedRows(older), ...recent];
689
+ const text = rows.map((row) => JSON.stringify(row)).join('\n');
690
+ return text === '' ? '' : `${text}\n`;
691
+ }
692
+
693
+ /**
694
+ * Lazily compact the invocation log on the cold read path.
695
+ *
696
+ * The named lock protects competing compactors. Emitters deliberately stay
697
+ * lock-free; this advisory log accepts a possible racing row rather than making
698
+ * every command wait on a lock. Every error is swallowed so compaction can never
699
+ * turn `dz deadwood` into a gate.
700
+ */
701
+ export function compactCmdUsageIfNeeded(root: string): void {
702
+ try {
703
+ const resolvedRoot = resolveCmdUsageRoot(root);
704
+ const path = join(resolvedRoot, CMD_USAGE_LOG_RELATIVE);
705
+ if (!existsSync(path) || statSync(path).size <= CMD_USAGE_LOG_MAX_BYTES) return;
706
+ withNamedLockSync(resolvedRoot, 'cmd-usage', () => {
707
+ if (!existsSync(path) || statSync(path).size <= CMD_USAGE_LOG_MAX_BYTES) return;
708
+ const parsed = parseCmdUsageLines(readFileSync(path, 'utf8'), new Date());
709
+ // Preserve a recent raw tail for accurate window counts and fold only the older prefix.
710
+ // If high-cardinality aggregate rows still exceed the target, keep them all: the size target
711
+ // is a convenience, while deleting a command's history would fabricate a zero-use candidate.
712
+ const text = compactedText(parsed.records);
713
+ const tmp = `${path}.compact-${process.pid}`;
714
+ writeFileSync(tmp, text, { encoding: 'utf8', mode: 0o600 });
715
+ renameSync(tmp, path);
716
+ });
717
+ } catch {
718
+ /* advisory compaction is fail-open just like the writer and report */
719
+ }
720
+ }