@dzhechkov/harness-core 0.3.138 → 0.3.141

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/src/index.ts CHANGED
@@ -96,7 +96,7 @@ export type {
96
96
  export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION } from './setup.js';
97
97
  export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStatePath } from './statusline.js';
98
98
  export type { StatuslineData, FeatureAdrState, WriteFeatureAdrStateInput } from './statusline.js';
99
- export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine } from './agentdb-index.js';
99
+ export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows, bumpAgentdbUses, clearAgentdbQuarantine, deleteAgentdbByDzIds, readAgentdbRowsByTaskType, DZ_OWNED_TASK_TYPES } from './agentdb-index.js';
100
100
  export type { AgentdbSearchHit, AgentdbSearchResult, AgentdbImportRow } from './agentdb-index.js';
101
101
  export { DEFAULT_EMBED_MODEL, LEGACY_EMBED_MODEL, DEFAULT_EMBED_DIM, KNOWN_EMBED_DIMS, resolveEmbedModel, readEmbedManifest, writeEmbedManifest, embedManifestPath, legacyEmbedManifest } from './embedding-config.js';
102
102
  export type { EmbedModelConfig, EmbedModelSource, EmbedManifest } from './embedding-config.js';
@@ -326,3 +326,12 @@ export * from './skills-verify.js';
326
326
  // from darwin-mode; measurements are dz-native and NEVER fake a verdict (INSUFFICIENT_DATA is a
327
327
  // finding, not a pass).
328
328
  export * from './compounding.js';
329
+
330
+ // Run-process scorecard (feature dz-score, Reading C) — scores the DISCIPLINE of a feature-adr run
331
+ // from its artifacts. Descriptive-only, permanently: it never gates.
332
+ export * from './score.js';
333
+
334
+ // Smart Backlog (feature smart-backlog) — goal-directed idea pipeline: capture → semantic dedup
335
+ // against the EXISTING Brain vector engine (ADR-001, no 2nd store) → GoalMap alignment (ADR-003) →
336
+ // weighted roulette (ADR-004) → idea2prd enrich hand-off → stub-first Jira adapter seam (ADR-006).
337
+ export * from './backlog.js';
package/src/score.ts ADDED
@@ -0,0 +1,220 @@
1
+ /**
2
+ * `dz score --slug <feature>` — score a feature-adr RUN's process discipline (feature dz-score,
3
+ * Reading C of `features/dz-score/PROPOSAL.md`, chosen by the user 2026-07-28).
4
+ *
5
+ * It scores the PROCESS, not the code: were the ADR's safety properties given a named test? was
6
+ * discrimination proven? did cross-model QE happen and what did it say? was the work verified live?
7
+ * did the READMEs travel in the same change? did the learning loop run?
8
+ *
9
+ * Readings A (repo dashboard) and B (skill scoring) were rejected in the proposal: A invites
10
+ * Goodharting the gates, B would be a fourth scoring surface. C is hard to game — the only way to
11
+ * score well is to actually run the discipline.
12
+ *
13
+ * DESCRIPTIVE-ONLY, permanently: the command never gates, never exits non-zero on a low score.
14
+ * The health-advisor 1.2.0 run is the reference case: its QE report marked the registration
15
+ * criterion "✅ (mechanism)" with no live evidence — this scorecard exists to make that visible.
16
+ *
17
+ * Discriminators were chosen from a SURVEY of the 132 real runs on disk (34/77 ADRs carry a
18
+ * Confirmation section; 31/75 QE reports carry MEASURED markers) — not guessed.
19
+ *
20
+ * PURE: the CLI reads the artifact files; this module only classifies.
21
+ */
22
+
23
+ export type DisciplineVerdict = 'pass' | 'partial' | 'absent';
24
+
25
+ export interface DisciplineScore {
26
+ readonly id: string;
27
+ readonly title: string;
28
+ readonly verdict: DisciplineVerdict;
29
+ /** The line of evidence the verdict rests on — a scorecard must show its work. */
30
+ readonly evidence: string;
31
+ }
32
+
33
+ export interface RunScorecard {
34
+ readonly slug: string;
35
+ readonly disciplines: readonly DisciplineScore[];
36
+ /** Extracted cross-model grade, when one exists (e.g. "A−", "C"). */
37
+ readonly qeGrade: string | null;
38
+ readonly passed: number;
39
+ readonly total: number;
40
+ readonly summary: string;
41
+ }
42
+
43
+ /** The artifact texts of one run, keyed by RELATIVE path under `features/<slug>/`. */
44
+ export type RunArtifacts = Readonly<Record<string, string>>;
45
+
46
+ function collect(artifacts: RunArtifacts, predicate: (path: string) => boolean): string {
47
+ return Object.entries(artifacts)
48
+ .filter(([p]) => predicate(p))
49
+ .map(([, text]) => text)
50
+ .join('\n');
51
+ }
52
+
53
+ /** First matching line (trimmed, capped) — the evidence a verdict shows. */
54
+ function evidenceLine(text: string, re: RegExp): string | null {
55
+ for (const line of text.split('\n')) {
56
+ if (re.test(line)) return line.trim().slice(0, 140);
57
+ }
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Like {@link evidenceLine}, but a NEGATED mention is not evidence: "Codex was not used" and
63
+ * "no discrimination proof was performed" both satisfied the plain regexes (Codex QE #1, and its
64
+ * heuristic table). A line whose match is preceded by a negation word is skipped. Heuristic — but
65
+ * the failure mode flips from a silent false pass to a visible miss the shown evidence exposes.
66
+ */
67
+ const NEGATION_RE = /\b(no|not|never|without|wasn'?t|isn'?t)\b/i;
68
+ function evidenceLinePositive(text: string, re: RegExp): string | null {
69
+ for (const line of text.split('\n')) {
70
+ if (!re.test(line)) continue;
71
+ // Whole-line negation: "Codex was NOT used" carries its negation AFTER the match, so a
72
+ // before-the-match check missed it. The trade is deliberate: a genuine line that happens to
73
+ // contain a negation is SKIPPED (a visible miss the evidence exposes) rather than a negated
74
+ // line being ACCEPTED (a silent false pass).
75
+ if (NEGATION_RE.test(line)) continue;
76
+ return line.trim().slice(0, 140);
77
+ }
78
+ return null;
79
+ }
80
+
81
+ // Word-bounded on BOTH sides: "upgrade B-tree" fabricated a B- (Codex QE #1). The lookahead also
82
+ // rejects "Grade B-tree" (letter after the dash) while keeping the real "Grade: A−" formats.
83
+ const GRADE_RE = /(?<![A-Za-z])[Gg]rade[d]?:?\s*\*{0,2}\s*([A-F][+−-]?)(?![A-Za-z-])/;
84
+
85
+ export function extractQeGrade(qeText: string): string | null {
86
+ const m = GRADE_RE.exec(qeText);
87
+ return m?.[1] ?? null;
88
+ }
89
+
90
+ export function scoreRun(slug: string, artifacts: RunArtifacts): RunScorecard {
91
+ const adrText = collect(artifacts, (p) => p.startsWith('03_adr/'));
92
+ const qeText = collect(artifacts, (p) => p === '08_qe_report.md' || p === '09_fleet_qe_assessment.md');
93
+ const planText = collect(artifacts, (p) => p === '06_implementation_plan.md' || p === '03.5_ideation_report.md');
94
+ const complexityText = collect(artifacts, (p) => p === '00_complexity_assessment.md');
95
+ const manifestText = collect(artifacts, (p) => p.startsWith('07_code_changes/'));
96
+ const allText = collect(artifacts, () => true);
97
+
98
+ const disciplines: DisciplineScore[] = [];
99
+ const add = (id: string, title: string, verdict: DisciplineVerdict, evidence: string): void => {
100
+ disciplines.push({ id, title, verdict, evidence });
101
+ };
102
+
103
+ // 1. ADR with a Confirmation — a named decision whose load-bearing property names its test.
104
+ if (adrText === '') {
105
+ add('adr-confirmation', 'ADR present, property → named test', 'absent', 'no 03_adr/*.md artifact');
106
+ } else {
107
+ const conf = evidenceLine(adrText, /^##+\s*Confirmation/i);
108
+ add(
109
+ 'adr-confirmation',
110
+ 'ADR present, property → named test',
111
+ conf !== null ? 'pass' : 'partial',
112
+ conf ?? 'ADR exists but has no Confirmation section — the load-bearing property names no test',
113
+ );
114
+ }
115
+
116
+ // 2. Discrimination — proof the test can FAIL (the §42 gate, or an explicit mutation proof).
117
+ const discr =
118
+ evidenceLinePositive(allText, /discrimination|§42/i) ??
119
+ evidenceLinePositive(allText, /mutation[s]?\s.*(prov|kill)|mutant[s]?\s.*(kill|red)|RED on the old|goes? RED|failed as expected/i);
120
+ add(
121
+ 'discrimination',
122
+ 'the property test is proven able to fail',
123
+ discr !== null ? 'pass' : 'absent',
124
+ discr ?? 'no discrimination/§42/mutation-proof evidence in any artifact',
125
+ );
126
+
127
+ // 3. Cross-model QE — an independent family reviewed it, and a grade exists.
128
+ const grade = extractQeGrade(qeText);
129
+ if (qeText === '') {
130
+ add('cross-model-qe', 'independent cross-model review with a grade', 'absent', 'no 08_qe_report.md artifact');
131
+ } else {
132
+ const crossLine = evidenceLinePositive(qeText, /codex|gpt-|cross-model/i);
133
+ const gradeLine = grade !== null ? evidenceLine(qeText, GRADE_RE) : null;
134
+ add(
135
+ 'cross-model-qe',
136
+ 'independent cross-model review with a grade',
137
+ crossLine !== null && grade !== null ? 'pass' : 'partial',
138
+ crossLine !== null
139
+ ? grade !== null
140
+ ? `${crossLine}${gradeLine !== null && gradeLine !== crossLine ? ` | ${gradeLine}` : ''}`.slice(0, 140)
141
+ : `${crossLine} — but NO parseable grade`.slice(0, 140)
142
+ : 'QE report exists but no (non-negated) cross-model reviewer is named (self-review only)',
143
+ );
144
+ }
145
+
146
+ // 4. Live verification — the property was observed, not inferred. The health-advisor 1.2.0 QE
147
+ // report is the cautionary case: "✅ (mechanism)" with no live evidence shipped a dead feature.
148
+ const live = evidenceLine(qeText, /MEASURED|verified live|VERIFIED LIVE|reproducer/i);
149
+ const liveAnywhere = live ?? evidenceLine(allText, /MEASURED|verified live|VERIFIED LIVE|reproducer/i);
150
+ add(
151
+ 'live-verification',
152
+ 'claims verified by running, not by reasoning',
153
+ live !== null ? 'pass' : liveAnywhere !== null ? 'partial' : 'absent',
154
+ live ?? liveAnywhere ?? 'no MEASURED / verified-live / reproducer marker anywhere — every claim is inferred',
155
+ );
156
+
157
+ // 5. README-first — the docs travelled in the same change.
158
+ const readme = evidenceLine(qeText + '\n' + manifestText, /README/);
159
+ add(
160
+ 'readme-first',
161
+ 'READMEs updated in the same change',
162
+ readme !== null ? 'pass' : 'absent',
163
+ readme ?? 'no README mention in the QE report or the change manifest',
164
+ );
165
+
166
+ // 6. The learning loop — Step-0 recall folded in, Step-8 lessons taught.
167
+ const recalled = evidenceLine(complexityText + '\n' + allText, /LEARNED_PATTERNS|dz recall|recalled/i);
168
+ const taught = evidenceLine(allText, /lesson[s]? taught|dz teach|taught \(/i);
169
+ add(
170
+ 'learning-loop',
171
+ 'Step-0 recall used; Step-8 lessons taught',
172
+ recalled !== null && taught !== null ? 'pass' : recalled !== null || taught !== null ? 'partial' : 'absent',
173
+ recalled !== null && taught !== null
174
+ ? `${recalled.slice(0, 68)} | ${taught.slice(0, 68)}`
175
+ : recalled ?? taught ?? 'no recall/teach evidence — the run neither drew on nor fed the learned store',
176
+ );
177
+
178
+ // 7. Amendment confirmation — only when amendments EXIST; their absence is not a failure.
179
+ // Born in the PLAN/ideation: a stray "AM-7" in QE prose (e.g. another feature's test name)
180
+ // must not conjure the discipline (caught by the very first dogfood run).
181
+ const plannedAm = [...new Set(planText.match(/AM-\d+/g) ?? [])].sort();
182
+ if (plannedAm.length > 0) {
183
+ // One stray AM-9 in QE must not satisfy AM-1..AM-2 (Codex QE #3): coverage is id-by-id.
184
+ const covered = plannedAm.filter((id) => qeText.includes(id));
185
+ const missing = plannedAm.filter((id) => !qeText.includes(id));
186
+ add(
187
+ 'amendment-confirmation',
188
+ 'amendments carried into QE with confirmation',
189
+ missing.length === 0 ? 'pass' : covered.length > 0 ? 'partial' : 'partial',
190
+ missing.length === 0
191
+ ? `all planned amendments reach QE: ${plannedAm.join(', ')}`
192
+ : `planned ${plannedAm.join(', ')} — QE never mentions ${missing.join(', ')}`,
193
+ );
194
+ }
195
+
196
+ const passed = disciplines.filter((d) => d.verdict === 'pass').length;
197
+ const total = disciplines.length;
198
+ const worst = disciplines.filter((d) => d.verdict === 'absent').map((d) => d.id);
199
+ const summary =
200
+ `${passed}/${total} disciplines fully evidenced` +
201
+ (grade !== null ? ` · QE grade ${grade}` : ' · no QE grade') +
202
+ (worst.length > 0 ? ` · absent: ${worst.join(', ')}` : '');
203
+
204
+ return { slug, disciplines, qeGrade: grade, passed, total, summary };
205
+ }
206
+
207
+ const MARK: Record<DisciplineVerdict, string> = { pass: '✓', partial: '◐', absent: '✗' };
208
+
209
+ export function renderScorecard(card: RunScorecard): string {
210
+ const out: string[] = [];
211
+ out.push(`dz score — ${card.slug} (process scorecard; descriptive-only, never a gate)`);
212
+ out.push('');
213
+ for (const d of card.disciplines) {
214
+ out.push(` ${MARK[d.verdict]} ${d.title}`);
215
+ out.push(` ${d.evidence}`);
216
+ }
217
+ out.push('');
218
+ out.push(` ${card.summary}`);
219
+ return out.join('\n');
220
+ }
@@ -65,7 +65,13 @@ import {
65
65
  cosineSimilarity,
66
66
  importVectorsToAgentdb,
67
67
  reindexAgentdbRows,
68
+ readAgentdbRowsByTaskType,
69
+ DZ_OWNED_TASK_TYPES,
68
70
  } from './agentdb-index.js';
71
+ // smart-backlog (ADR-001/005 lifecycle): `dz vector reindex` must re-embed dz-backlog rows too, or
72
+ // they rot in a stale embedding space after a model bump. One-directional import — backlog.ts imports
73
+ // agentdb-index/compounding only, never vector-tier, so there is no cycle.
74
+ import { BACKLOG_TASK_TYPE } from './backlog.js';
69
75
  import { currentEmbedManifest, guardEmbedSpace, DEFAULT_EMBED_DIM, resolveEmbedModel, type EmbedModelConfig } from './embedding-config.js';
70
76
  import { applyLearningSignals, applyLearningSignalsWithDelta, resolveLearningBackend, type LearningSignalBackend } from './learning-backend.js';
71
77
 
@@ -278,6 +284,15 @@ export interface ReindexVectorReport {
278
284
  * silent, since a mixed store looks healthy right up until someone widens a query.
279
285
  */
280
286
  readonly staleTaskTypes?: readonly string[];
287
+ /**
288
+ * smart-backlog honest-skip status. Set ONLY on the non-agentdb-engine path: backlog code never touches
289
+ * the shared manifest, so under a non-agentdb engine NOTHING is reindexed — `backlog:'skipped'` +
290
+ * `generic:'skipped'` + a non-empty `error` say so (overall success is never claimed). Unset on the full
291
+ * agentdb reindex path (which owns and re-embeds dz-backlog together with the other types) — behavior for
292
+ * every other caller is unchanged.
293
+ */
294
+ readonly backlog?: 'skipped';
295
+ readonly generic?: 'skipped';
281
296
  }
282
297
 
283
298
  export type TeachGuardResult =
@@ -963,10 +978,31 @@ export async function vectorTierStatus(
963
978
  }
964
979
 
965
980
  export async function reindexVectorStore(projectRoot: string, opts: VectorServiceOptions = {}): Promise<ReindexVectorReport> {
981
+ // ARCHITECTURAL INVARIANT (smart-backlog): backlog code never PARTIALLY advances the SHARED agentdb
982
+ // model manifest — no path re-stamps it to a new model while leaving a sibling task type un-re-embedded.
983
+ // The FIRST backlog write legitimately establishes the manifest when the store is new (correct); a model
984
+ // ADVANCE happens ONLY here, in a full reindex that re-embeds EVERY owned task type together atomically
985
+ // (taskTypes = DZ_OWNED_TASK_TYPES, which owns dz-backlog — HIGH-3). The old backlog-only re-stamp path
986
+ // spawned an endless class of edge cases (missing/corrupt/rollback/scan-error); it is REMOVED.
966
987
  const resolved = pickEngine(projectRoot, opts);
967
- if (resolved.engine === undefined) return { reembedded: 0, error: resolved.reason ?? 'no vector engine available' };
968
- if (resolved.engine.kind !== 'agentdb') {
969
- return { reembedded: 0, error: 'dz vector reindex currently rewrites the agentdb learned-pattern mirror; switch memory.vector.engine to agentdb/auto' };
988
+ if (resolved.engine === undefined || resolved.engine.kind !== 'agentdb') {
989
+ // Non-agentdb (or absent) engine: the generic learned-pattern reindex can't run here, and backlog must
990
+ // not advance the shared manifest partially. Touch NOTHING honest skip with a UNIFORM shape (LOW-I:
991
+ // always report backlog/generic skipped). Backlog's same-model writes already bind vectors to the
992
+ // current model (HIGH-2); a model bump needs a FULL reindex under the agentdb engine.
993
+ // A non-agentdb vector engine is CONFIGURED (rvf resolved, or requested-but-not-installed) ⇒ the
994
+ // manifest-untouched message pointing to the full agentdb reindex. Only a genuinely absent engine
995
+ // (no config, nothing resolvable) falls back to the bare reason.
996
+ const nonAgentdbConfigured = resolved.engine !== undefined || readVectorEngineMode(projectRoot) === 'rvf';
997
+ return {
998
+ reembedded: 0,
999
+ backlog: 'skipped',
1000
+ generic: 'skipped',
1001
+ error: nonAgentdbConfigured
1002
+ ? 'a non-agentdb vector engine (e.g. RVF) is configured; no reindex was performed and the shared model manifest was NOT touched. ' +
1003
+ 'Run a full reindex under memory.vector.engine=agentdb/auto to re-embed every task type (dz-teach/dz-learning/dz-backlog) together.'
1004
+ : resolved.reason ?? 'no vector engine available',
1005
+ };
970
1006
  }
971
1007
  let records: MemoryRecord[];
972
1008
  try {
@@ -984,7 +1020,14 @@ export async function reindexVectorStore(projectRoot: string, opts: VectorServic
984
1020
  ...(r.tags !== undefined ? { tags: r.tags } : {}),
985
1021
  ...(r.metadata !== undefined ? { metadata: r.metadata } : {}),
986
1022
  }));
987
- return reindexAgentdbRows(projectRoot, rows);
1023
+ // HIGH-G: ownership is by STORE-PRESENCE. Read the dz-backlog rows that ACTUALLY EXIST IN THE STORE and
1024
+ // re-embed THOSE (their stored text) — never reconstruct them from ideas.jsonl, or an empty/unreadable
1025
+ // sidecar would advance the manifest while the store's real dz-backlog rows rot in the old model space.
1026
+ // Symmetric with dz-teach/dz-learning (re-embedded from their store of truth). Only widen the owned set
1027
+ // when the store actually holds dz-backlog rows — a store with none reindexes byte-identically to before.
1028
+ const backlogStoreRows = readAgentdbRowsByTaskType(projectRoot, BACKLOG_TASK_TYPE).rows;
1029
+ if (backlogStoreRows.length === 0) return reindexAgentdbRows(projectRoot, rows);
1030
+ return reindexAgentdbRows(projectRoot, [...rows, ...backlogStoreRows], { taskTypes: DZ_OWNED_TASK_TYPES });
988
1031
  }
989
1032
 
990
1033
  /* ------------------------------------------------------------------ */