@dzhechkov/harness-core 0.8.38 → 0.8.40
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/.dz-manifest.json +281 -161
- package/README.md +145 -2
- package/dist/agentdb-index.d.ts +3 -3
- package/dist/agentdb-index.js +5 -5
- package/dist/agentdb-index.js.map +1 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +4 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/backlog.d.ts +2 -1
- package/dist/backlog.d.ts.map +1 -1
- package/dist/backlog.js +3 -2
- package/dist/backlog.js.map +1 -1
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +9 -2
- package/dist/brain.js.map +1 -1
- package/dist/bto-optimize.d.ts.map +1 -1
- package/dist/bto-optimize.js +9 -12
- package/dist/bto-optimize.js.map +1 -1
- package/dist/claim-check.d.ts.map +1 -1
- package/dist/claim-check.js +50 -14
- package/dist/claim-check.js.map +1 -1
- package/dist/cmd-usage.d.ts.map +1 -1
- package/dist/cmd-usage.js +48 -2
- package/dist/cmd-usage.js.map +1 -1
- package/dist/compounding.d.ts +66 -0
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +76 -9
- package/dist/compounding.js.map +1 -1
- package/dist/doctor-instrument.d.ts +65 -0
- package/dist/doctor-instrument.d.ts.map +1 -0
- package/dist/doctor-instrument.js +91 -0
- package/dist/doctor-instrument.js.map +1 -0
- package/dist/experiment-assign.d.ts +110 -0
- package/dist/experiment-assign.d.ts.map +1 -0
- package/dist/experiment-assign.js +229 -0
- package/dist/experiment-assign.js.map +1 -0
- package/dist/feature-adr-checkpoints.d.ts +7 -2
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +20 -5
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-envelope.d.ts +12 -0
- package/dist/feature-adr-envelope.d.ts.map +1 -1
- package/dist/feature-adr-envelope.js +12 -1
- package/dist/feature-adr-envelope.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +4 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +4 -0
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/feature-tier.d.ts.map +1 -1
- package/dist/feature-tier.js +13 -1
- package/dist/feature-tier.js.map +1 -1
- package/dist/guard.d.ts +17 -0
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +15 -0
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +14 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -5
- package/dist/index.js.map +1 -1
- package/dist/ledger-cost-fill.d.ts +58 -0
- package/dist/ledger-cost-fill.d.ts.map +1 -0
- package/dist/ledger-cost-fill.js +78 -0
- package/dist/ledger-cost-fill.js.map +1 -0
- package/dist/loop-blobs.generated.js +2 -2
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/mutation-gate.d.ts +59 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +65 -1
- package/dist/mutation-gate.js.map +1 -1
- package/dist/name-check.d.ts +20 -1
- package/dist/name-check.d.ts.map +1 -1
- package/dist/name-check.js +42 -1
- package/dist/name-check.js.map +1 -1
- package/dist/no-stubs.d.ts +10 -0
- package/dist/no-stubs.d.ts.map +1 -1
- package/dist/no-stubs.js +13 -9
- package/dist/no-stubs.js.map +1 -1
- package/dist/operations.d.ts +13 -0
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +88 -2
- package/dist/operations.js.map +1 -1
- package/dist/patterns.d.ts +24 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +49 -9
- package/dist/patterns.js.map +1 -1
- package/dist/publish-source-scope.d.ts +32 -0
- package/dist/publish-source-scope.d.ts.map +1 -0
- package/dist/publish-source-scope.js +53 -0
- package/dist/publish-source-scope.js.map +1 -0
- package/dist/publish.d.ts +5 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +32 -21
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +4 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +21 -11
- package/dist/qe-bridge.js.map +1 -1
- package/dist/rake-analyzer.d.ts +10 -3
- package/dist/rake-analyzer.d.ts.map +1 -1
- package/dist/rake-analyzer.js +84 -22
- package/dist/rake-analyzer.js.map +1 -1
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +8 -4
- package/dist/recap.js.map +1 -1
- package/dist/registry.d.ts +58 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +62 -5
- package/dist/registry.js.map +1 -1
- package/dist/release.d.ts +20 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +48 -4
- package/dist/release.js.map +1 -1
- package/dist/reqe.d.ts.map +1 -1
- package/dist/reqe.js +11 -1
- package/dist/reqe.js.map +1 -1
- package/dist/round-exec.d.ts +10 -0
- package/dist/round-exec.d.ts.map +1 -1
- package/dist/round-exec.js +3 -2
- package/dist/round-exec.js.map +1 -1
- package/dist/round.d.ts +19 -0
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +1 -0
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +18 -4
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +61 -7
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +8 -1
- package/dist/score.js.map +1 -1
- package/dist/sign.d.ts +23 -0
- package/dist/sign.d.ts.map +1 -1
- package/dist/sign.js +52 -0
- package/dist/sign.js.map +1 -1
- package/dist/skills-verify.d.ts +8 -5
- package/dist/skills-verify.d.ts.map +1 -1
- package/dist/skills-verify.js +59 -7
- package/dist/skills-verify.js.map +1 -1
- package/dist/store-guard-prune.d.ts +22 -0
- package/dist/store-guard-prune.d.ts.map +1 -0
- package/dist/store-guard-prune.js +54 -0
- package/dist/store-guard-prune.js.map +1 -0
- package/dist/sweep-failure-classify.d.ts +45 -0
- package/dist/sweep-failure-classify.d.ts.map +1 -0
- package/dist/sweep-failure-classify.js +90 -0
- package/dist/sweep-failure-classify.js.map +1 -0
- package/dist/workflow-run-dispatch.d.ts +14 -2
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +14 -2
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +460 -160
- package/src/agentdb-index.ts +5 -5
- package/src/amendment-trace.ts +4 -1
- package/src/backlog.ts +3 -2
- package/src/brain.ts +8 -2
- package/src/bto-optimize.ts +10 -8
- package/src/claim-check.ts +48 -12
- package/src/cmd-usage.ts +52 -2
- package/src/compounding.ts +112 -9
- package/src/doctor-instrument.ts +153 -0
- package/src/experiment-assign.ts +276 -0
- package/src/feature-adr-checkpoints.ts +20 -5
- package/src/feature-adr-envelope.ts +24 -1
- package/src/feature-adr-routing.ts +4 -0
- package/src/feature-tier.ts +14 -1
- package/src/guard.ts +24 -0
- package/src/index.ts +22 -5
- package/src/loop-blobs.generated.ts +2 -2
- package/src/mutation-gate.ts +113 -1
- package/src/name-check.ts +43 -1
- package/src/no-stubs.ts +14 -5
- package/src/operations.ts +85 -3
- package/src/patterns.ts +49 -8
- package/src/publish-source-scope.ts +53 -0
- package/src/publish.ts +38 -16
- package/src/qe-bridge.ts +22 -11
- package/src/rake-analyzer.ts +75 -20
- package/src/rake-signatures.json +98 -0
- package/src/recap.ts +8 -4
- package/src/registry.ts +91 -1
- package/src/release.ts +54 -4
- package/src/reqe.ts +11 -1
- package/src/round-exec.ts +13 -2
- package/src/round.ts +20 -0
- package/src/run-records.ts +78 -10
- package/src/score.ts +8 -1
- package/src/sign.ts +53 -0
- package/src/skills-verify.ts +69 -9
- package/src/store-guard-prune.ts +81 -0
- package/src/sweep-failure-classify.ts +88 -0
- package/src/workflow-run-dispatch.ts +14 -2
package/src/agentdb-index.ts
CHANGED
|
@@ -1168,9 +1168,9 @@ export async function importVectorsToAgentdb(
|
|
|
1168
1168
|
}
|
|
1169
1169
|
|
|
1170
1170
|
/**
|
|
1171
|
-
* lesson-quarantine:
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1171
|
+
* lesson-quarantine: mark mirrored rows as promoted after a promotion — the hook daemon reads ONLY
|
|
1172
|
+
* this mirror's metadata, so a promoted lesson must stop being excluded there while retaining its
|
|
1173
|
+
* quarantine history. Best-effort, same custody model as {@link bumpAgentdbUses} (missing db/deps ⇒ no-op).
|
|
1174
1174
|
*/
|
|
1175
1175
|
export function clearAgentdbQuarantine(
|
|
1176
1176
|
projectRoot: string,
|
|
@@ -1194,12 +1194,12 @@ export function clearAgentdbQuarantine(
|
|
|
1194
1194
|
db.pragma('busy_timeout = 5000');
|
|
1195
1195
|
db.exec(REASONING_BANK_SCHEMA);
|
|
1196
1196
|
const stmt = db.prepare(
|
|
1197
|
-
"UPDATE reasoning_patterns SET metadata =
|
|
1197
|
+
"UPDATE reasoning_patterns SET metadata = json_set(metadata, '$.qStatus', 'promoted', '$.promotedAt', ?) WHERE json_extract(metadata, '$.dzId') = ? AND json_extract(metadata, '$.qStatus') = 'quarantined'",
|
|
1198
1198
|
);
|
|
1199
1199
|
const tx = db.transaction(() => {
|
|
1200
1200
|
let cleared = 0;
|
|
1201
1201
|
for (const dzId of dzIds) {
|
|
1202
|
-
const r = stmt.run(dzId);
|
|
1202
|
+
const r = stmt.run(new Date().toISOString(), dzId);
|
|
1203
1203
|
cleared += Number((r as unknown as { changes?: number }).changes ?? 0);
|
|
1204
1204
|
}
|
|
1205
1205
|
return cleared;
|
package/src/amendment-trace.ts
CHANGED
|
@@ -138,7 +138,10 @@ export function extractTestTitles(body: string): string[] {
|
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
export function normalizeTestId(s: string): string {
|
|
141
|
-
|
|
141
|
+
// Unicode letter/number classes: the old `[^a-z0-9]` erased Cyrillic outright, so a Russian test title
|
|
142
|
+
// normalised to '' and tripped the floor as the author's fault (MEASURED 2026-09-04, backlog 191853a2).
|
|
143
|
+
// Re-run over all 519 features on 2026-09-20: zero verdicts changed — this only adds matches.
|
|
144
|
+
return s.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, '');
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
/**
|
package/src/backlog.ts
CHANGED
|
@@ -266,7 +266,7 @@ export function readBacklogConfig(projectRoot: string): BacklogConfig {
|
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
/* ================================================================== */
|
|
269
|
-
/* STORE (AM-1) — .dz/backlog/ideas.jsonl (
|
|
269
|
+
/* STORE (AM-1) — .dz/backlog/ideas.jsonl (JSONL rewritten WHOLE per write, ADR-005 + amendment 2026-09-20) */
|
|
270
270
|
/* ================================================================== */
|
|
271
271
|
|
|
272
272
|
export function backlogDir(projectRoot: string): string {
|
|
@@ -335,7 +335,8 @@ function normaliseIdea(raw: Record<string, unknown>): IdeaRecord | undefined {
|
|
|
335
335
|
return rec;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
/** Read the
|
|
338
|
+
/** Read the store (one current line per id — `writeIdeas` rewrites it whole and never appends). A corrupt
|
|
339
|
+
* line is SKIPPED (never fatal) — the whole store never throws. */
|
|
339
340
|
export function readIdeas(projectRoot: string): IdeaRecord[] {
|
|
340
341
|
const path = ideasPath(projectRoot);
|
|
341
342
|
if (!existsSync(path)) return [];
|
package/src/brain.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdir
|
|
|
21
21
|
import { pathToFileURL } from 'node:url';
|
|
22
22
|
import { createRequire } from 'node:module';
|
|
23
23
|
import { spawn } from 'node:child_process';
|
|
24
|
+
import { openSqliteReadOnly } from '@dzhechkov/memory';
|
|
24
25
|
import { putBookKnowledge, queryBookKnowledge, bookKbPath, type BookKU, type BookKUHit } from './book-kb.js';
|
|
25
26
|
import { indexPatternsToAgentdb, searchAgentdbPatterns, reindexAgentdbRows, type AgentdbRow } from './agentdb-index.js';
|
|
26
27
|
import type { SnapshotRotationReport } from './agentdb-snapshot-rotation.js';
|
|
@@ -193,7 +194,8 @@ export function readBookKus(opts: {
|
|
|
193
194
|
return { kus: [], error: 'better-sqlite3 not installed (run: dz setup --memory agentdb)' };
|
|
194
195
|
}
|
|
195
196
|
try {
|
|
196
|
-
const
|
|
197
|
+
const handle = openSqliteReadOnly(opts.storePath, { Database });
|
|
198
|
+
const db = handle.db as NativeDb;
|
|
197
199
|
try {
|
|
198
200
|
const cols = 'book, ku_id, corpus_version, type, name, problem, content, chapter, pages, metadata';
|
|
199
201
|
const rows = (opts.source !== undefined
|
|
@@ -201,7 +203,11 @@ export function readBookKus(opts: {
|
|
|
201
203
|
: db.prepare(`SELECT ${cols} FROM book_knowledge`).all()) as ProjRow[];
|
|
202
204
|
return { kus: rows.map(rowToKu) };
|
|
203
205
|
} finally {
|
|
204
|
-
|
|
206
|
+
try {
|
|
207
|
+
db.close();
|
|
208
|
+
} finally {
|
|
209
|
+
handle.cleanup();
|
|
210
|
+
}
|
|
205
211
|
}
|
|
206
212
|
} catch (err) {
|
|
207
213
|
return { kus: [], error: `read book KB failed: ${err instanceof Error ? err.message : String(err)}` };
|
package/src/bto-optimize.ts
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
import { existsSync, readFileSync } from 'node:fs';
|
|
22
22
|
|
|
23
|
+
import { maskMarkdown } from './markdown-masker.js';
|
|
24
|
+
|
|
23
25
|
export type BtoDimension = 'METHODOLOGY' | 'DEPTH' | 'CORRECTNESS' | 'USABILITY' | 'ROBUSTNESS';
|
|
24
26
|
export const BTO_DIMENSIONS: readonly BtoDimension[] = Object.freeze(['METHODOLOGY', 'DEPTH', 'CORRECTNESS', 'USABILITY', 'ROBUSTNESS']);
|
|
25
27
|
export type DimScores = Record<BtoDimension, number>;
|
|
@@ -203,22 +205,22 @@ const bodyAfterFrontmatter = (t: string): string => {
|
|
|
203
205
|
|
|
204
206
|
/**
|
|
205
207
|
* ALL structural markers on the BODY, not just space-delimited ATX (QE: `##\tNEW`, bare `##`, and setext
|
|
206
|
-
* `Title\n===` / `Title\n---` evaded the old regex).
|
|
207
|
-
*
|
|
208
|
-
*
|
|
208
|
+
* `Title\n===` / `Title\n---` evaded the old regex). The canonical masker skips fenced code blocks before
|
|
209
|
+
* collecting ATX (`#`..`######` + any/no ws) and setext underlines (a non-empty line immediately followed
|
|
210
|
+
* by `=+`/`-+`). Its `unclosed: 'restore'` policy is load-bearing: measured over 7,121 headed Markdown
|
|
211
|
+
* documents, the old parity toggle missed a deletion in 39 documents, masking with `hide` missed 22, and
|
|
212
|
+
* masking with `restore` missed 0.
|
|
209
213
|
*/
|
|
210
214
|
const headings = (t: string): string[] => {
|
|
211
|
-
const
|
|
215
|
+
const body = bodyAfterFrontmatter(t);
|
|
216
|
+
const lines = maskMarkdown(body, { unclosed: 'restore' }).split('\n');
|
|
212
217
|
const out: string[] = [];
|
|
213
|
-
let inFence = false;
|
|
214
218
|
for (let i = 0; i < lines.length; i++) {
|
|
215
219
|
const line = lines[i]!;
|
|
216
|
-
if (/^\s*(```|~~~)/.test(line)) { inFence = !inFence; continue; }
|
|
217
|
-
if (inFence) continue;
|
|
218
220
|
const atx = /^(#{1,6})(?:\s.*)?$/.exec(line.replace(/\s+$/, ''));
|
|
219
221
|
if (atx) { out.push('ATX:' + line.trim()); continue; }
|
|
220
222
|
const next = lines[i + 1];
|
|
221
|
-
if (line.trim() !== '' &&
|
|
223
|
+
if (line.trim() !== '' && next !== undefined && /^(=+|-+)\s*$/.test(next)) {
|
|
222
224
|
out.push('SETEXT:' + line.trim() + '|' + next.trim());
|
|
223
225
|
}
|
|
224
226
|
}
|
package/src/claim-check.ts
CHANGED
|
@@ -122,6 +122,8 @@ function paragraphAround(lines: readonly string[], i: number): string {
|
|
|
122
122
|
/** Tags that make a claim honest (case-insensitive). */
|
|
123
123
|
// `estimated` agrees with the `estimated: true` honest-uncertainty marker `dz usage` already
|
|
124
124
|
// emits — the two honesty systems must not contradict each other.
|
|
125
|
+
import { maskMarkdown } from './markdown-masker.js';
|
|
126
|
+
|
|
125
127
|
const HONEST_TAGS = ['measured', 'claimed', 'synthetic', 'unvalidated', 'baseline', 'estimated'];
|
|
126
128
|
|
|
127
129
|
/**
|
|
@@ -136,17 +138,27 @@ const HONEST_TAGS = ['measured', 'claimed', 'synthetic', 'unvalidated', 'baselin
|
|
|
136
138
|
*/
|
|
137
139
|
export function isFenced(text: string, line: number): boolean {
|
|
138
140
|
if (typeof text !== 'string' || typeof line !== 'number' || !isFinite(line) || line < 1) return false;
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
141
|
+
// DELEGATED to the canonical masker. The local walk this replaces already tracked the marker
|
|
142
|
+
// CHARACTER — the naive-toggle bug the comment above describes was genuinely fixed — but it still
|
|
143
|
+
// broke two further CommonMark rules, and both were MEASURED 2026-09-20 to answer `false` for a
|
|
144
|
+
// line that IS inside a block: a closing fence may carry NO info string, so a second info-string
|
|
145
|
+
// line closed the block; and a closing fence may not be SHORTER than the opening one, so a
|
|
146
|
+
// three-backtick line closed a four-backtick block. Both make the engine scan a QUOTED example as
|
|
147
|
+
// a real claim, and make the hook's deny path stop exempting it.
|
|
148
|
+
//
|
|
149
|
+
// `unclosed: 'hide'` keeps the local walk's policy: an unclosed opener leaves every later line
|
|
150
|
+
// inside the block. One deliberate difference: the OPENING delimiter line now answers `true` (the
|
|
151
|
+
// local walk answered `false` for it and `true` for the closing one) — an info string is not
|
|
152
|
+
// prose, and the two delimiters answering differently was an artifact, not a decision.
|
|
153
|
+
let masked = false;
|
|
154
|
+
try {
|
|
155
|
+
const target = line - 1;
|
|
156
|
+
maskMarkdown(text.replace(/\r\n/g, '\n'), {
|
|
157
|
+
unclosed: 'hide',
|
|
158
|
+
onMasked: (i?: number) => { if (i === target) masked = true; },
|
|
159
|
+
});
|
|
160
|
+
} catch { return false; }
|
|
161
|
+
return masked;
|
|
150
162
|
}
|
|
151
163
|
|
|
152
164
|
const FENCE_RE = /^\s*(`{3,}|~{3,})/;
|
|
@@ -189,9 +201,33 @@ const MAP_METRIC_RE = new RegExp(
|
|
|
189
201
|
* A shell reproducer is STRUCTURAL, never a word. `(MEASURED — reproducer)` is self-certifying and
|
|
190
202
|
* must not pass; a backticked span whose first token is a command this repo actually measures with is
|
|
191
203
|
* evidence. The allowlist boundary is exactly that: an unknown binary is a claim ABOUT evidence.
|
|
204
|
+
*
|
|
205
|
+
* `python3` joined the list for backlog 0b53470a, on the list's OWN criterion rather than by
|
|
206
|
+
* widening it: `packages/@dzhechkov/health-advisor/test/goap-python-suite.test.js` makes
|
|
207
|
+
* `python3 -m unittest discover` a GATE, so it is a command this repo measures with. Before the
|
|
208
|
+
* addition a Python measurer could not cite itself — MEASURED by twins, one line differing only in
|
|
209
|
+
* the backticked binary: `python3 …` → 1 finding "Tagged MEASURED but cites no reproducer",
|
|
210
|
+
* `pytest …` → 0, `git show …` → 0. Bare `python` was deliberately NOT added (cross-family review
|
|
211
|
+
* r1, MEDIUM): the gate this repo runs is `python3`, and the only measured usage in the tree is
|
|
212
|
+
* `python3 -m unittest` — an entry nothing measures with would be exactly the "claim ABOUT
|
|
213
|
+
* evidence" this list refuses. The list stays CLOSED: a measurer in any other language meets the
|
|
214
|
+
* same wall and needs the same deliberate entry. That is the price of the boundary, not a defect.
|
|
215
|
+
*
|
|
216
|
+
* THE BOUNDARY IS `(?![\w-])`, NOT `\b`, and that turned out to matter far beyond python
|
|
217
|
+
* (cross-family review r1, HIGH). `\b` treats a hyphen as a word boundary, so every backticked
|
|
218
|
+
* FILE NAME that begins with a listed command counted as a reproducer. MEASURED 2026-09-19:
|
|
219
|
+
* `pnpm-lock.yaml`, `dz-harness-hub`, `git-workflow` and `npm-shrinkwrap.json` all passed as
|
|
220
|
+
* evidence for a MEASURED claim, while `node_modules` was correctly refused — only because `_` is
|
|
221
|
+
* a word character and `-` is not. The repository holds hundreds of such tokens, so the check has
|
|
222
|
+
* been accepting file names as measurements for as long as the list has existed.
|
|
223
|
+
*
|
|
224
|
+
* WHAT THIS CHECK DOES NOT DO, said plainly because the previous wording implied more: it verifies
|
|
225
|
+
* the SHAPE of a citation, never that a measurement happened. `git --version` in backticks is
|
|
226
|
+
* accepted by construction — validating arguments per binary is a different mechanism with a
|
|
227
|
+
* different cost, and pretending otherwise would be the very laundering this file exists to stop.
|
|
192
228
|
*/
|
|
193
229
|
const SHELL_REPRO_RE =
|
|
194
|
-
/`\s*\$?\s*(?:ps|stat|lsof|time|git|npm|npx|node|pnpm|yarn|dz|curl|wc|grep|find|cargo|make|docker|kubectl|awk|sed|du|df|vitest|pytest)\
|
|
230
|
+
/`\s*\$?\s*(?:ps|stat|lsof|time|git|npm|npx|node|pnpm|yarn|dz|curl|wc|grep|find|cargo|make|docker|kubectl|awk|sed|du|df|vitest|pytest|python3)(?![\w-])[^`]*`/i;
|
|
195
231
|
|
|
196
232
|
/** Reproducer references that count as evidence backing a MEASURED claim. */
|
|
197
233
|
const REPRODUCER_HINTS = [
|
package/src/cmd-usage.ts
CHANGED
|
@@ -153,6 +153,12 @@ interface RuleUsage {
|
|
|
153
153
|
readonly stats: ReadonlyMap<string, CmdUsageStat>;
|
|
154
154
|
readonly skipped: number;
|
|
155
155
|
readonly outOfRange: number;
|
|
156
|
+
/**
|
|
157
|
+
* Audit rows inside the window that recorded which rules they EVALUATED. Zero means this report
|
|
158
|
+
* has no evidence about rules at all — which is a different answer from "the rule is unused", and
|
|
159
|
+
* the distinction is the whole point of counting it (backlog 1bee49dd).
|
|
160
|
+
*/
|
|
161
|
+
readonly evaluationRows: number;
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
const REPO_BOUNDARY_IO = {
|
|
@@ -401,9 +407,13 @@ export function loadDeadwoodAllowlist(json: string): DeadwoodAllowlistEntry[] {
|
|
|
401
407
|
function parseGuardAuditUsage(text: string, weeks: number, now: Date): RuleUsage {
|
|
402
408
|
const auditTimestamps: string[] = [];
|
|
403
409
|
const hits: CmdUsageInvocationRecord[] = [];
|
|
410
|
+
let evaluationRows = 0;
|
|
404
411
|
let skipped = 0;
|
|
405
412
|
let outOfRange = 0;
|
|
406
413
|
const newestAllowed = now.getTime() + DEADWOOD_FUTURE_TOLERANCE_MS;
|
|
414
|
+
// Same window arithmetic `foldCmdUsage` uses, so "counted as evidence" and "counted as a run"
|
|
415
|
+
// cannot disagree about which rows are inside.
|
|
416
|
+
const windowStart = now.getTime() - Math.max(0, weeks) * 7 * DAY_MS;
|
|
407
417
|
for (const line of text.split('\n')) {
|
|
408
418
|
if (line.trim() === '') continue;
|
|
409
419
|
let row: Record<string, unknown>;
|
|
@@ -425,17 +435,40 @@ function parseGuardAuditUsage(text: string, weeks: number, now: Date): RuleUsage
|
|
|
425
435
|
continue;
|
|
426
436
|
}
|
|
427
437
|
auditTimestamps.push(row.ts);
|
|
438
|
+
// A rule's HEALTHY state is silence, so firing cannot measure whether it is alive. `evaluated`
|
|
439
|
+
// records the rules that actually got their turn; rows written before the field existed carry
|
|
440
|
+
// none, and they contribute evidence about firing only.
|
|
441
|
+
//
|
|
442
|
+
// Two conditions, both named by cross-family review (Codex gpt-5.6-sol, 2026-09-21), both of
|
|
443
|
+
// which turn this evidence into a false accusation if skipped:
|
|
444
|
+
// · the row must be INSIDE the window. An instrumented row older than `now - weeks` yields no
|
|
445
|
+
// in-window runs, so counting it as evidence would let one ancient row flip every absent
|
|
446
|
+
// rule from "cannot judge" to "dead".
|
|
447
|
+
// · the array must carry a USABLE id. `evaluated: []` is a row that recorded nothing; treating
|
|
448
|
+
// it as evidence is the same false accusation by a shorter path.
|
|
449
|
+
const evaluatedIds = new Set<string>();
|
|
450
|
+
if (Array.isArray(row.evaluated)) {
|
|
451
|
+
for (const value of row.evaluated) {
|
|
452
|
+
if (typeof value === 'string' && value.trim() !== '') evaluatedIds.add(value);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
if (evaluatedIds.size > 0 && tsMs >= windowStart) evaluationRows += 1;
|
|
456
|
+
for (const id of evaluatedIds) {
|
|
457
|
+
hits.push({ kind: 'cmd', cmd: id, ts: row.ts, v: CMD_USAGE_SCHEMA });
|
|
458
|
+
}
|
|
428
459
|
const violations = Array.isArray(row.violations) ? row.violations : [];
|
|
429
460
|
for (const value of violations) {
|
|
430
461
|
const rule = typeof value === 'object' && value !== null
|
|
431
462
|
? (value as { rule?: unknown }).rule
|
|
432
463
|
: undefined;
|
|
433
|
-
|
|
464
|
+
// One guard run is ONE run. A rule that both evaluated and fired in the same row would be
|
|
465
|
+
// counted twice — 100 warning evaluations reported as 200 runs (same review, second finding).
|
|
466
|
+
if (typeof rule === 'string' && rule.trim() !== '' && !evaluatedIds.has(rule)) {
|
|
434
467
|
hits.push({ kind: 'cmd', cmd: rule, ts: row.ts, v: CMD_USAGE_SCHEMA });
|
|
435
468
|
}
|
|
436
469
|
}
|
|
437
470
|
}
|
|
438
|
-
return { auditTimestamps, stats: foldCmdUsage(hits, weeks, now), skipped, outOfRange };
|
|
471
|
+
return { auditTimestamps, stats: foldCmdUsage(hits, weeks, now), skipped, outOfRange, evaluationRows };
|
|
439
472
|
}
|
|
440
473
|
|
|
441
474
|
function timestampDepthDays(timestamps: readonly string[], now: Date): number {
|
|
@@ -607,6 +640,23 @@ export function buildDeadwoodReport(input: DeadwoodInput): DeadwoodReport {
|
|
|
607
640
|
});
|
|
608
641
|
continue;
|
|
609
642
|
}
|
|
643
|
+
// A rule the window has no EVALUATION evidence for is unjudged, not unused. Firing is the wrong
|
|
644
|
+
// signal for a guard (silence is its healthy state), so without `evaluated` rows the only honest
|
|
645
|
+
// answer is "this report cannot judge the rule" — exactly what the skill surface already says.
|
|
646
|
+
if (item.kind === 'rule' && rules.evaluationRows === 0
|
|
647
|
+
&& (rules.stats.get(item.surface)?.runsInWindow ?? 0) === 0
|
|
648
|
+
// An explicit allowlist entry is an operator's standing statement about this surface; it keeps
|
|
649
|
+
// its own wording. Only the ACCUSING path — "zero usage, consider deprecating" — is withdrawn.
|
|
650
|
+
&& !allowlist.has(allowlistKey(item.kind, item.surface))) {
|
|
651
|
+
noInstrumentation.push({
|
|
652
|
+
state: 'no-instrumentation',
|
|
653
|
+
surface: item.surface,
|
|
654
|
+
kind: item.kind,
|
|
655
|
+
reason: 'no guard-audit row in this window recorded which rules it evaluated; a rule that '
|
|
656
|
+
+ 'never fires may be a healthy safety net, so firing alone cannot judge it',
|
|
657
|
+
});
|
|
658
|
+
continue;
|
|
659
|
+
}
|
|
610
660
|
classifyInstrumented(
|
|
611
661
|
{ surface: item.surface, kind: item.kind },
|
|
612
662
|
item.kind === 'command'
|
package/src/compounding.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* Everything here is PURE: callers gather facts (files, store rows); this module only computes.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { EVENT_CHAIN_SCOPE, verifyEventChainText } from './event-chain.js';
|
|
18
|
+
import { EVENT_CHAIN_SCOPE, classifyChainDefects, verifyEventChainText } from './event-chain.js';
|
|
19
19
|
import {
|
|
20
20
|
isOffsetIsoTimestamp,
|
|
21
21
|
type PromotionAcceptanceEvidence,
|
|
@@ -160,15 +160,36 @@ export interface GuardEvent {
|
|
|
160
160
|
readonly verdict: string;
|
|
161
161
|
readonly rules: readonly string[]; // violated rule ids
|
|
162
162
|
readonly violations?: readonly { readonly rule: string; readonly contentAnchor?: string }[];
|
|
163
|
+
/**
|
|
164
|
+
* 1-based position of this record among the log's non-empty lines — the ONLY thing that can place
|
|
165
|
+
* it relative to a chain defect. Absent when the caller read the rows without a chain.
|
|
166
|
+
*/
|
|
167
|
+
readonly chainLine?: number;
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
export type FunnelEvidenceSource<T> =
|
|
166
171
|
| { readonly status: 'measured'; readonly rows: readonly T[] }
|
|
167
172
|
| { readonly status: 'not-measured'; readonly reason: string };
|
|
168
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Where the guard journal's chain damage sits, so a PERIOD can be judged instead of the whole FILE.
|
|
176
|
+
*
|
|
177
|
+
* A log damaged once in March and unbroken since is not evidence against September's rows, and
|
|
178
|
+
* refusing to measure September because of March is the same "verdict answers a different question"
|
|
179
|
+
* defect the chain headline was fixed for (backlog b38dd3ba, MEASURED 2026-09-21: 28 defects, all
|
|
180
|
+
* before a run of 1169 unbroken records, suppressed BOTH measured months).
|
|
181
|
+
*/
|
|
182
|
+
export interface GuardAuditChainWindow {
|
|
183
|
+
/** First non-empty line of the current unbroken run: one past the last defect. */
|
|
184
|
+
readonly runFrom: number;
|
|
185
|
+
/** Total defects in the file. Zero means the window imposes nothing. */
|
|
186
|
+
readonly defects: number;
|
|
187
|
+
}
|
|
188
|
+
|
|
169
189
|
export interface LessonToRuleFunnelFacts {
|
|
170
190
|
readonly promotionRuns: FunnelEvidenceSource<PromotionRunEvidence>;
|
|
171
191
|
readonly guardAudits: FunnelEvidenceSource<GuardEvent>;
|
|
192
|
+
readonly guardAuditChain?: GuardAuditChainWindow;
|
|
172
193
|
readonly promotionAcceptances?: readonly PromotionAcceptanceEvidence[];
|
|
173
194
|
readonly truncatedPromotionPeriods?: readonly string[];
|
|
174
195
|
readonly acceptanceHistoryComplete?: boolean;
|
|
@@ -274,6 +295,20 @@ export interface EvidenceChainHealth {
|
|
|
274
295
|
readonly preChainPrefix: number;
|
|
275
296
|
readonly defects: number;
|
|
276
297
|
readonly defectKinds: readonly string[];
|
|
298
|
+
/**
|
|
299
|
+
* WHERE the defects sit relative to the log's current unbroken run, and HOW MUCH of a run that is.
|
|
300
|
+
* Without this a bare `FAILED` over a log whose damage is entirely historical reads as "today's
|
|
301
|
+
* numbers are garbage", while `dz chain` over the SAME file says "healed … verdicts over those are
|
|
302
|
+
* sound" — MEASURED 2026-09-20 on `.dz/guard-audit.jsonl`: 28 defects, all before the current run,
|
|
303
|
+
* 1095 unbroken records after them; one instrument printed FAILED, the other healed, both exit 0
|
|
304
|
+
* (backlog 79ce6262). Neither was lying; neither named its WINDOW. `event-chain.ts` says it
|
|
305
|
+
* outright: a caller that reports soundness without printing the run size overclaims on its behalf,
|
|
306
|
+
* and the same holds for a caller that reports damage without printing where the damage sits.
|
|
307
|
+
*/
|
|
308
|
+
readonly defectsBeforeRun: number;
|
|
309
|
+
readonly defectsInRun: number;
|
|
310
|
+
/** Records in the current unbroken run — the evidence behind any "sound for today" reading. */
|
|
311
|
+
readonly runRecords: number;
|
|
277
312
|
}
|
|
278
313
|
|
|
279
314
|
export interface InstrumentationHealth {
|
|
@@ -394,6 +429,13 @@ function executionMeasurement(
|
|
|
394
429
|
if (periodAudits.length === 0) {
|
|
395
430
|
return LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`guard-audit-not-recorded:${period}`);
|
|
396
431
|
}
|
|
432
|
+
// Damage that PRECEDES this period's rows says nothing about them; damage that touches them does.
|
|
433
|
+
// A row with no position cannot be placed, and unplaceable is not the same as sound — it refuses.
|
|
434
|
+
const chain = facts.guardAuditChain;
|
|
435
|
+
if (chain !== undefined && chain.defects > 0
|
|
436
|
+
&& periodAudits.some((row) => row.chainLine === undefined || row.chainLine < chain.runFrom)) {
|
|
437
|
+
return LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`guard-audit-chain-damaged:${period}`);
|
|
438
|
+
}
|
|
397
439
|
const audits = periodAudits.filter((row) => row.op === 'publish');
|
|
398
440
|
if (audits.length === 0) {
|
|
399
441
|
return LESSON_TO_RULE_FUNNEL_POLICY.unavailable(`guard-publish-not-recorded:${period}`);
|
|
@@ -594,6 +636,11 @@ export function assembleCompoundingReport(facts: CompoundingFacts): CompoundingR
|
|
|
594
636
|
preChainPrefix: v.preChainPrefix,
|
|
595
637
|
defects: v.defects.length,
|
|
596
638
|
defectKinds: [...new Set(v.defects.map((d) => d.kind))],
|
|
639
|
+
...((age) => ({
|
|
640
|
+
defectsBeforeRun: age.beforeRun.length,
|
|
641
|
+
defectsInRun: age.inRun.length,
|
|
642
|
+
runRecords: age.runRecords,
|
|
643
|
+
}))(classifyChainDefects(v, v.lines)),
|
|
597
644
|
};
|
|
598
645
|
});
|
|
599
646
|
|
|
@@ -623,13 +670,7 @@ export function assembleCompoundingReport(facts: CompoundingFacts): CompoundingR
|
|
|
623
670
|
trajectory.length > 0 ? `guard: ${improvedRules}/${trajectory.length} rules recur less in the later half` : 'guard: not enough history',
|
|
624
671
|
`cold-vs-warm: ${replay.verdict === 'insufficient-data' ? 'INSUFFICIENT DATA (accruing)' : 'READY to measure'}`,
|
|
625
672
|
instrumentation.applyLegLive ? 'apply leg: live' : 'apply leg: STALE — fix the instrumentation before trusting anything above',
|
|
626
|
-
...(chains.length === 0
|
|
627
|
-
? []
|
|
628
|
-
: [
|
|
629
|
-
instrumentation.chainsOk
|
|
630
|
-
? 'evidence chain: verified'
|
|
631
|
-
: 'evidence chain: CORRUPT — the numbers above are computed from a damaged log',
|
|
632
|
-
]),
|
|
673
|
+
...(chains.length === 0 ? [] : [chainHeadline(chains)]),
|
|
633
674
|
].join(' · ');
|
|
634
675
|
|
|
635
676
|
return { pool, guardTrajectory: trajectory, replay, instrumentation, lessonToRuleFunnel, verdict };
|
|
@@ -671,6 +712,68 @@ function renderFunnelPeriodMeasurements(row: LessonToRuleFunnelPeriod): string {
|
|
|
671
712
|
return `${renderPromotionMeasurements(row)} · ${renderFunnelMeasurement('executions', row.executions)}`;
|
|
672
713
|
}
|
|
673
714
|
|
|
715
|
+
/**
|
|
716
|
+
* The HEADLINE verdict over every evidence log — three-valued, because two values lied.
|
|
717
|
+
*
|
|
718
|
+
* MEASURED 2026-09-21 on `.dz/guard-audit.jsonl`: 28 defects, the LAST of them dated 2026-09-05,
|
|
719
|
+
* followed by more than a thousand unbroken records. The old headline read
|
|
720
|
+
* "CORRUPT — the numbers above are computed from a damaged log", which is true of the FILE'S
|
|
721
|
+
* HISTORY and false about the numbers it was printed next to. The distinction already existed one
|
|
722
|
+
* function below, in {@link chainVerdictPhrase}; it simply never reached the line a reader sees
|
|
723
|
+
* first. That is the same defect class this report exists to find: a verdict answering a different
|
|
724
|
+
* question than the one it appears to answer.
|
|
725
|
+
*/
|
|
726
|
+
/**
|
|
727
|
+
* Whether the report's OWN numbers may be trusted, as a value the caller can turn into an exit code.
|
|
728
|
+
*
|
|
729
|
+
* Backlog 79ce6262 named the defect: the report printed "the numbers above are computed from a
|
|
730
|
+
* damaged log" and exited 0 anyway — a tool announcing its own output untrustworthy and reporting
|
|
731
|
+
* success. That record offered two lawful cures and asked which applies. Both do, on different
|
|
732
|
+
* branches, and only the three-valued verdict lets them coexist: damage BEHIND the current run
|
|
733
|
+
* narrows the WORDING (the numbers stand, exit 0), damage INSIDE it makes the numbers genuinely
|
|
734
|
+
* unreliable and must reach the exit code.
|
|
735
|
+
*
|
|
736
|
+
* `'trusted'` ⇒ 0. `'unreliable'` ⇒ a non-zero the caller chooses — the run succeeded, the verdict
|
|
737
|
+
* cannot be relied on, which is this repository's INCONCLUSIVE shape, not its failure shape.
|
|
738
|
+
*/
|
|
739
|
+
export function chainTrust(chains: readonly EvidenceChainHealth[]): 'trusted' | 'unreliable' {
|
|
740
|
+
return chains.some((c) => !c.ok && c.defectsInRun > 0) ? 'unreliable' : 'trusted';
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export function chainHeadline(chains: readonly EvidenceChainHealth[]): string {
|
|
744
|
+
const broken = chains.filter((c) => !c.ok);
|
|
745
|
+
if (broken.length === 0) return 'evidence chain: verified';
|
|
746
|
+
const live = broken.filter((c) => c.defectsInRun > 0);
|
|
747
|
+
if (live.length === 0) {
|
|
748
|
+
const runs = broken.reduce((n, c) => n + c.runRecords, 0);
|
|
749
|
+
const defects = broken.reduce((n, c) => n + c.defects, 0);
|
|
750
|
+
return `evidence chain: damaged EARLIER — ${defects} defect(s), none inside the current run of `
|
|
751
|
+
+ `${runs} unbroken record(s); the numbers above stand, the file's history does not`;
|
|
752
|
+
}
|
|
753
|
+
const inRun = live.reduce((n, c) => n + c.defectsInRun, 0);
|
|
754
|
+
return `evidence chain: CORRUPT — ${inRun} defect(s) INSIDE the current run; the numbers above are `
|
|
755
|
+
+ 'computed from a damaged log';
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* The verdict phrase for one evidence log, with its WINDOW named. A bare `FAILED` over damage that an
|
|
760
|
+
* unbroken run has already followed is true of the FILE and misleading about TODAY — see
|
|
761
|
+
* {@link EvidenceChainHealth.defectsBeforeRun}.
|
|
762
|
+
*/
|
|
763
|
+
export function chainVerdictPhrase(c: EvidenceChainHealth): string {
|
|
764
|
+
if (c.ok) return 'verified';
|
|
765
|
+
const kinds = `[${c.defectKinds.join(', ')}]`;
|
|
766
|
+
if (c.defectsInRun === 0) {
|
|
767
|
+
return `DAMAGED EARLIER — ${c.defects} defect(s) ${kinds}, all BEFORE the current run of `
|
|
768
|
+
+ `${c.runRecords} unbroken record(s); numbers over that run stand, the file's history does not`;
|
|
769
|
+
}
|
|
770
|
+
if (c.defectsBeforeRun === 0) {
|
|
771
|
+
return `FAILED — ${c.defects} defect(s) ${kinds} with NO sound records after them`;
|
|
772
|
+
}
|
|
773
|
+
return `FAILED — ${c.defects} defect(s) ${kinds}: ${c.defectsInRun} inside the current run of `
|
|
774
|
+
+ `${c.runRecords} record(s), ${c.defectsBeforeRun} before it`;
|
|
775
|
+
}
|
|
776
|
+
|
|
674
777
|
export function renderCompoundingReport(r: CompoundingReport): string {
|
|
675
778
|
const out: string[] = [];
|
|
676
779
|
out.push('dz compounding — does the learning loop pay? (honest report: gates without data say so)');
|
|
@@ -695,7 +798,7 @@ export function renderCompoundingReport(r: CompoundingReport): string {
|
|
|
695
798
|
);
|
|
696
799
|
for (const c of r.instrumentation.chains) {
|
|
697
800
|
out.push(
|
|
698
|
-
` EVIDENCE CHAIN ${c.log}: ${
|
|
801
|
+
` EVIDENCE CHAIN ${c.log}: ${chainVerdictPhrase(c)}` +
|
|
699
802
|
` · ${c.chained} chained · ${c.preChainPrefix} pre-chain (uncovered)`,
|
|
700
803
|
);
|
|
701
804
|
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { isAbsolute, relative, sep } from 'node:path';
|
|
2
|
+
|
|
3
|
+
export type InstrumentFreshness = 'same' | 'stale' | 'unknown';
|
|
4
|
+
|
|
5
|
+
export interface InstrumentCheckInput {
|
|
6
|
+
/** realpath of the running binary, or null when it could not be resolved. */
|
|
7
|
+
readonly binPath: string | null;
|
|
8
|
+
/** version from the package.json that owns binPath, or null. */
|
|
9
|
+
readonly binVersion: string | null;
|
|
10
|
+
/** version from packages/@dzhechkov/harness-cli/package.json, or null outside the monorepo. */
|
|
11
|
+
readonly treeVersion: string | null;
|
|
12
|
+
/** absolute, realpath'd project root. */
|
|
13
|
+
readonly projectRoot: string;
|
|
14
|
+
/**
|
|
15
|
+
* Whether `projectRoot` above really IS realpath'd. The caller resolves it and falls back to a
|
|
16
|
+
* plain resolve when that throws; with a symlinked root that fallback compares a realpath'd
|
|
17
|
+
* binary against a non-realpath'd root, and an IN-TREE binary then looks external. Containment is
|
|
18
|
+
* undecidable in that state, so it is answered `unknown` rather than guessed either way.
|
|
19
|
+
* Named by independent review (Claude Sonnet, 2026-09-20).
|
|
20
|
+
*/
|
|
21
|
+
readonly projectRootRealpathed: boolean;
|
|
22
|
+
readonly isMonorepo: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `level` is the WHOLE verdict — the caller renders it and never re-derives one of its own. That is
|
|
27
|
+
* deliberate: the first wiring of this module answered `unknown` with `ok: false` while this decider
|
|
28
|
+
* answered `ok`, and two answers to one question is the defect class this repo pays for most often.
|
|
29
|
+
*
|
|
30
|
+
* Three values, because two would lie: `ok` (the instrument is the tree's, or the check does not
|
|
31
|
+
* apply here), `warn` (measured stale — worth saying loudly, never worth failing a health command
|
|
32
|
+
* that gates other people's CI), `unknown` (the evidence could not be gathered — never rendered as
|
|
33
|
+
* a pass, and never as a failure either, since absence of evidence is not a defect).
|
|
34
|
+
*/
|
|
35
|
+
export interface InstrumentCheckResult {
|
|
36
|
+
readonly freshness: InstrumentFreshness;
|
|
37
|
+
readonly level: 'ok' | 'warn' | 'unknown';
|
|
38
|
+
readonly detail: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface RankingStateCheckInput {
|
|
42
|
+
readonly flagOn: boolean;
|
|
43
|
+
/** absolute path the state was looked for at. */
|
|
44
|
+
readonly statePath: string;
|
|
45
|
+
readonly stateExists: boolean;
|
|
46
|
+
/** resolved binary path, for the detail — null when unknown. */
|
|
47
|
+
readonly binPath: string | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Ranking state has no freshness concept, so it deliberately has its own result type. */
|
|
51
|
+
export interface RankingStateCheckResult {
|
|
52
|
+
readonly level: 'ok' | 'warn';
|
|
53
|
+
readonly detail: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Strip semver build metadata: `0.8.32+a1b2c3` and `0.8.32` are the same release. */
|
|
57
|
+
function withoutBuildMetadata(version: string): string {
|
|
58
|
+
const plus = version.indexOf('+');
|
|
59
|
+
return plus < 0 ? version : version.slice(0, plus);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isInsideRoot(candidate: string, root: string): boolean {
|
|
63
|
+
const fromRoot = relative(root, candidate);
|
|
64
|
+
return fromRoot === '' || (!isAbsolute(fromRoot) && fromRoot !== '..' && !fromRoot.startsWith(`..${sep}`));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Decide whether the executable answering `dz doctor` is the workspace's current instrument.
|
|
69
|
+
*
|
|
70
|
+
* LIMITS NAMED BY INDEPENDENT REVIEW (Claude Sonnet, 2026-09-20), none of them hidden behind a
|
|
71
|
+
* passing test:
|
|
72
|
+
* - Containment is a case-SENSITIVE path comparison. On a case-insensitive filesystem, or where the
|
|
73
|
+
* same location is reachable under two path forms, an in-tree binary can read as external. This
|
|
74
|
+
* repo runs on Linux; the cost of being wrong is one extra `warn` line and never an exit code.
|
|
75
|
+
* - The caller attributes a version by walking up from the binary to the NEAREST `package.json`.
|
|
76
|
+
* A shim in package A that loads package B's code is attributed to A, and a broken install with
|
|
77
|
+
* no own manifest is attributed to whatever ancestor has one. The detail always prints the
|
|
78
|
+
* resolved binary path so a reader can see which file was actually measured.
|
|
79
|
+
*/
|
|
80
|
+
export function checkInstrumentFreshness(input: InstrumentCheckInput): InstrumentCheckResult {
|
|
81
|
+
const binary = input.binPath ?? '(unresolved)';
|
|
82
|
+
const binaryVersion = input.binVersion ?? 'unknown';
|
|
83
|
+
const treeVersion = input.treeVersion ?? 'unknown';
|
|
84
|
+
|
|
85
|
+
if (!input.isMonorepo) {
|
|
86
|
+
return {
|
|
87
|
+
freshness: 'unknown',
|
|
88
|
+
level: 'ok',
|
|
89
|
+
detail: `not applicable in a consumer project: no packages/@dzhechkov tree version to compare; resolved binary ${binary}; binary version ${binaryVersion}; tree version ${treeVersion}`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (input.binPath !== null && input.projectRootRealpathed && isInsideRoot(input.binPath, input.projectRoot)) {
|
|
94
|
+
return {
|
|
95
|
+
freshness: 'same',
|
|
96
|
+
level: 'ok',
|
|
97
|
+
detail: `resolved binary ${input.binPath} is inside project root ${input.projectRoot}; binary version ${binaryVersion}; tree version ${treeVersion}; this binary is the tree instrument`,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (!input.projectRootRealpathed) {
|
|
102
|
+
return {
|
|
103
|
+
freshness: 'unknown',
|
|
104
|
+
level: 'unknown',
|
|
105
|
+
detail: `project root ${input.projectRoot} could not be resolved through its symlinks, so it cannot be told whether the answering binary is the tree's own; version could not be determined safely; resolved binary ${binary}; binary version ${binaryVersion}; tree version ${treeVersion}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (input.binPath === null || input.binVersion === null || input.treeVersion === null) {
|
|
110
|
+
return {
|
|
111
|
+
freshness: 'unknown',
|
|
112
|
+
level: 'unknown',
|
|
113
|
+
detail: `instrument version could not be determined; resolved binary ${binary}; binary version ${binaryVersion}; tree version ${treeVersion}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Semver says build metadata after `+` does not participate in equality, so a pipeline that
|
|
118
|
+
// stamps a commit hash onto the version must not read as a stale instrument.
|
|
119
|
+
if (withoutBuildMetadata(input.binVersion) === withoutBuildMetadata(input.treeVersion)) {
|
|
120
|
+
return {
|
|
121
|
+
freshness: 'same',
|
|
122
|
+
level: 'ok',
|
|
123
|
+
detail: `resolved binary ${input.binPath}; binary version ${input.binVersion}; tree version ${input.treeVersion}; versions match`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
freshness: 'stale',
|
|
129
|
+
level: 'warn',
|
|
130
|
+
detail: `resolved binary ${input.binPath}; binary version ${input.binVersion}; tree version ${input.treeVersion}; the answering instrument is stale`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Decide whether enabled bandit re-ranking has the on-disk state needed to operate. */
|
|
135
|
+
export function checkRankingState(input: RankingStateCheckInput): RankingStateCheckResult {
|
|
136
|
+
const binary = input.binPath ?? '(unresolved)';
|
|
137
|
+
if (!input.flagOn) {
|
|
138
|
+
return {
|
|
139
|
+
level: 'ok',
|
|
140
|
+
detail: `bandit re-ranking feature is off; no state is expected at ${input.statePath}; answering binary ${binary}`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (input.stateExists) {
|
|
144
|
+
return {
|
|
145
|
+
level: 'ok',
|
|
146
|
+
detail: `bandit re-ranking is on and state is present at ${input.statePath}; answering binary ${binary}`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
level: 'warn',
|
|
151
|
+
detail: `bandit re-ranking is on but state is absent at ${input.statePath}; answering binary ${binary}`,
|
|
152
|
+
};
|
|
153
|
+
}
|