@dzhechkov/harness-core 0.3.150 → 0.4.2

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 (111) hide show
  1. package/.dz-manifest.json +410 -62
  2. package/README.md +81 -3
  3. package/dist/agentdb-index.d.ts.map +1 -1
  4. package/dist/agentdb-index.js +10 -2
  5. package/dist/agentdb-index.js.map +1 -1
  6. package/dist/backlog-embed.d.ts +94 -0
  7. package/dist/backlog-embed.d.ts.map +1 -0
  8. package/dist/backlog-embed.js +138 -0
  9. package/dist/backlog-embed.js.map +1 -0
  10. package/dist/backlog.d.ts +180 -7
  11. package/dist/backlog.d.ts.map +1 -1
  12. package/dist/backlog.js +429 -26
  13. package/dist/backlog.js.map +1 -1
  14. package/dist/challenge-panel.d.ts +3 -0
  15. package/dist/challenge-panel.d.ts.map +1 -1
  16. package/dist/challenge-panel.js +3 -0
  17. package/dist/challenge-panel.js.map +1 -1
  18. package/dist/export-holdout.d.ts +149 -0
  19. package/dist/export-holdout.d.ts.map +1 -0
  20. package/dist/export-holdout.js +198 -0
  21. package/dist/export-holdout.js.map +1 -0
  22. package/dist/feature-adr-checkpoints.d.ts +127 -0
  23. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  24. package/dist/feature-adr-checkpoints.js +199 -1
  25. package/dist/feature-adr-checkpoints.js.map +1 -1
  26. package/dist/feature-adr-routing.d.ts +3 -0
  27. package/dist/feature-adr-routing.d.ts.map +1 -1
  28. package/dist/feature-adr-routing.js +3 -0
  29. package/dist/feature-adr-routing.js.map +1 -1
  30. package/dist/guard.d.ts +42 -0
  31. package/dist/guard.d.ts.map +1 -1
  32. package/dist/guard.js +73 -1
  33. package/dist/guard.js.map +1 -1
  34. package/dist/index.d.ts +16 -3
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +28 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/loop-blobs.generated.d.ts +33 -0
  39. package/dist/loop-blobs.generated.d.ts.map +1 -0
  40. package/dist/loop-blobs.generated.js +101 -0
  41. package/dist/loop-blobs.generated.js.map +1 -0
  42. package/dist/loop-lint.d.ts +63 -0
  43. package/dist/loop-lint.d.ts.map +1 -0
  44. package/dist/loop-lint.js +606 -0
  45. package/dist/loop-lint.js.map +1 -0
  46. package/dist/loop-plan.d.ts +416 -0
  47. package/dist/loop-plan.d.ts.map +1 -0
  48. package/dist/loop-plan.js +1151 -0
  49. package/dist/loop-plan.js.map +1 -0
  50. package/dist/loop-render.d.ts +104 -0
  51. package/dist/loop-render.d.ts.map +1 -0
  52. package/dist/loop-render.js +1068 -0
  53. package/dist/loop-render.js.map +1 -0
  54. package/dist/loop-trace.d.ts +229 -0
  55. package/dist/loop-trace.d.ts.map +1 -0
  56. package/dist/loop-trace.js +614 -0
  57. package/dist/loop-trace.js.map +1 -0
  58. package/dist/mutation-gate.d.ts +247 -0
  59. package/dist/mutation-gate.d.ts.map +1 -0
  60. package/dist/mutation-gate.js +535 -0
  61. package/dist/mutation-gate.js.map +1 -0
  62. package/dist/no-stubs.d.ts +53 -0
  63. package/dist/no-stubs.d.ts.map +1 -0
  64. package/dist/no-stubs.js +190 -0
  65. package/dist/no-stubs.js.map +1 -0
  66. package/dist/package-skill-layouts.d.ts +67 -0
  67. package/dist/package-skill-layouts.d.ts.map +1 -0
  68. package/dist/package-skill-layouts.js +81 -0
  69. package/dist/package-skill-layouts.js.map +1 -0
  70. package/dist/patterns.d.ts.map +1 -1
  71. package/dist/patterns.js +156 -75
  72. package/dist/patterns.js.map +1 -1
  73. package/dist/recall-domain-boost.d.ts.map +1 -1
  74. package/dist/recall-domain-boost.js +6 -0
  75. package/dist/recall-domain-boost.js.map +1 -1
  76. package/dist/statusline.d.ts +10 -2
  77. package/dist/statusline.d.ts.map +1 -1
  78. package/dist/statusline.js +122 -36
  79. package/dist/statusline.js.map +1 -1
  80. package/dist/store-lock.d.ts +108 -0
  81. package/dist/store-lock.d.ts.map +1 -0
  82. package/dist/store-lock.js +231 -0
  83. package/dist/store-lock.js.map +1 -0
  84. package/dist/workflows.d.ts +16 -22
  85. package/dist/workflows.d.ts.map +1 -1
  86. package/dist/workflows.js +17 -98
  87. package/dist/workflows.js.map +1 -1
  88. package/package.json +6 -4
  89. package/sbom.json +1073 -203
  90. package/src/agentdb-index.ts +10 -1
  91. package/src/backlog-embed.ts +156 -0
  92. package/src/backlog.ts +536 -28
  93. package/src/challenge-panel.ts +4 -0
  94. package/src/export-holdout.ts +235 -0
  95. package/src/feature-adr-checkpoints.ts +291 -1
  96. package/src/feature-adr-routing.ts +4 -0
  97. package/src/guard.ts +106 -1
  98. package/src/index.ts +62 -2
  99. package/src/loop-blobs.generated.ts +114 -0
  100. package/src/loop-lint.ts +643 -0
  101. package/src/loop-plan.ts +1419 -0
  102. package/src/loop-render.ts +1126 -0
  103. package/src/loop-trace.ts +727 -0
  104. package/src/mutation-gate.ts +701 -0
  105. package/src/no-stubs.ts +204 -0
  106. package/src/package-skill-layouts.ts +107 -0
  107. package/src/patterns.ts +135 -60
  108. package/src/recall-domain-boost.ts +6 -0
  109. package/src/statusline.ts +117 -30
  110. package/src/store-lock.ts +258 -0
  111. package/src/workflows.ts +18 -117
@@ -16,6 +16,9 @@ import { existsSync, mkdirSync } from 'node:fs';
16
16
  import { join, dirname } from 'node:path';
17
17
  import { pathToFileURL } from 'node:url';
18
18
  import { createRequire } from 'node:module';
19
+ // The backlog dedup embed form (PURE, zero-dep — no cycle): dz-backlog rows must be embedded in the
20
+ // SAME bounded form the dedup query uses, including through the reindex path.
21
+ import { BACKLOG_TASK_TYPE, dedupEmbedText } from './backlog-embed.js';
19
22
  import {
20
23
  currentEmbedManifest,
21
24
  guardEmbedSpace,
@@ -129,7 +132,13 @@ export async function indexPatternsToAgentdb(
129
132
  // ALL first, then commit the writes atomically (QE P2: a mid-loop failure must not leave a
130
133
  // partial batch reported as indexed:0).
131
134
  const prepared: Array<{ row: AgentdbRow; vec: Float32Array }> = [];
132
- for (const row of rows) prepared.push({ row, vec: await emb.embed(`${row.taskType}: ${row.text}`) });
135
+ // dz-backlog rows use the BOUNDED dedup embed form (backlog-embed.ts) — the same form the
136
+ // dedup query and `mirrorIdeaVector` use, so a `dz vector reindex` lands backlog rows in the
137
+ // space they are queried in. Every other task type keeps the classic `${taskType}: ${text}`.
138
+ for (const row of rows) {
139
+ const embedText = row.taskType === BACKLOG_TASK_TYPE ? dedupEmbedText(row.text) : `${row.taskType}: ${row.text}`;
140
+ prepared.push({ row, vec: await emb.embed(embedText) });
141
+ }
133
142
  const commit = db.transaction(() => {
134
143
  for (const { row, vec } of prepared) {
135
144
  const r = insPattern.run(
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Backlog dedup EMBED FORM + lexical corroboration (the 2026-08-11 register-inflation fix).
3
+ *
4
+ * Zero-dep PURE module: imported by BOTH `backlog.ts` (query/mirror/harmonize) and
5
+ * `agentdb-index.ts` (the reindex/index write path), so every dz-backlog vector — fresh mirror,
6
+ * reindex, or query — is built from the SAME text form. A second definition of this form is exactly
7
+ * the query-vs-row space split this module exists to prevent.
8
+ *
9
+ * WHY the bounded excerpt (all numbers MEASURED 2026-08-11 on the real 105-idea store, reproducer:
10
+ * scratch pairwise-cosine harness over `EmbeddingService` / Xenova-paraphrase-multilingual-MiniLM-L12-v2,
11
+ * the same model + `${taskType}: ${text}` form the tool uses):
12
+ *
13
+ * - Full-length embeddings INVERT the duplicate signal on long texts. Genuine reworded paraphrases
14
+ * of 1073–1509-char ideas scored 0.35–0.61 against their own sources, while topically DISJOINT
15
+ * long-RU pairs scored up to 0.9195 (genai-tweets x dz-cost) — i.e. above every genuine long
16
+ * paraphrase. No threshold can separate classes that are ordered the wrong way round.
17
+ * - The inflation is a register/length effect, not topic: long-RU x long-RU pairs mean cosine
18
+ * 0.6467 vs short-EN x short-EN 0.3929 (+0.25) over the same store; the 2026-08-05 incident
19
+ * (an agentdb zombie-process idea absorbed as DUPLICATE of a dz-guard publish-gate rule at
20
+ * cosine 0.941) shared only the register — правило/проверка/гейт/уровень/измерено.
21
+ * - Bounded to the first 400 chars, the same pairs separate: paraphrase-vs-source 0.73–0.95 with
22
+ * the source as top-1 in 8/8 probes, store-wide max non-duplicate pair 0.9181, and the
23
+ * patient-values long-form scored 0.80 against its own short reword (was 0.26 at full length,
24
+ * with an unrelated record at 0.89 on top).
25
+ *
26
+ * WHY the lexical corroboration: cosine alone still cannot tell a same-register pair from a true
27
+ * paraphrase near the threshold. A TRUE re-capture shares the idea's DISTINCTIVE vocabulary
28
+ * (subject nouns, identifiers); a register-only pair shares function words. Measured containment on
29
+ * the labeled set: true duplicates 0.33–0.97 (>= 0.538 for every pair that also cleared the cosine
30
+ * threshold); register-only false-positive pairs <= 0.171.
31
+ */
32
+
33
+ /** The vector-store namespace that isolates ideas from lessons (ADR-001/005). */
34
+ export const BACKLOG_TASK_TYPE = 'dz-backlog';
35
+
36
+ /**
37
+ * Version of the dedup embed FORM (not the model). v1 = `dz-backlog: <full text>`; v2 = bounded
38
+ * excerpt (this module). Stored vectors written under an older form are in a DIFFERENT space than
39
+ * v2 queries for texts longer than the cap — `ensureBacklogEmbedForm` re-mirrors them once.
40
+ */
41
+ export const DEDUP_EMBED_FORM_VERSION = 2;
42
+
43
+ /**
44
+ * Embed-input cap in UTF-16 units. NOT config: the cap is part of the embed form — two stores (or a
45
+ * query and a row) built with different caps silently live in different spaces. 400 chosen over 300
46
+ * by measurement: same store-wide max (0.9181) with higher paraphrase-vs-source cosines (0.88 vs
47
+ * 0.84 on the hardest long-RU probe).
48
+ */
49
+ export const DEDUP_EMBED_CAP = 400;
50
+
51
+ /**
52
+ * The DISTINCTIVE excerpt of an idea for embedding: whitespace-collapsed, capped at
53
+ * {@link DEDUP_EMBED_CAP}. Short texts pass through untouched (byte-identical semantics to v1 for
54
+ * them). The cut never splits a surrogate pair.
55
+ */
56
+ export function dedupExcerpt(text: string): string {
57
+ const collapsed = text.replace(/\s+/g, ' ').trim();
58
+ if (collapsed.length <= DEDUP_EMBED_CAP) return collapsed;
59
+ let cut = collapsed.slice(0, DEDUP_EMBED_CAP);
60
+ // A high surrogate at the cut would make the excerpt an invalid string — drop the half char.
61
+ const lastCode = cut.charCodeAt(cut.length - 1);
62
+ if (lastCode >= 0xd800 && lastCode <= 0xdbff) cut = cut.slice(0, -1);
63
+ return cut;
64
+ }
65
+
66
+ /** The ONE dedup embed form (v2) — used by the query, the mirror write, and the reindex write. */
67
+ export function dedupEmbedText(text: string): string {
68
+ return `${BACKLOG_TASK_TYPE}: ${dedupExcerpt(text)}`;
69
+ }
70
+
71
+ /**
72
+ * Register/function words excluded from the DISTINCTIVE token set — the shared technical register
73
+ * that inflated the 0.941 false duplicate (правило/проверка/гейт/уровень/измерено + generic RU/EN
74
+ * function words). Deliberately small: the containment cut (0.30) sits far from both measured
75
+ * classes (<= 0.171 vs >= 0.538), so the list does not need to be complete to discriminate.
76
+ */
77
+ const REGISTER_STOPWORDS = new Set(
78
+ (
79
+ 'правило проверка гейт уровень измерено замерено идея задача контекст исследование урок вердикт ' +
80
+ 'цель форма выход проблема источник материалы сегодня реально нужно нельзя должен должна должно ' +
81
+ 'обязан обязана обязано который которая которое только через после перед против всего этого чтобы ' +
82
+ 'если иначе когда где притом причем поэтому пока пусть надо есть нет был была было будут быть ' +
83
+ 'может можно станет стало сразу самый самая самое наш наша наше свой своя своё туда сюда здесь ' +
84
+ 'там опять снова очень давно потом чужой чужая every never always must should could would about ' +
85
+ 'there their these those with from into over under this that what when where which while откуда ' +
86
+ 'почему зачем такой такая такое весь вся всё они оный дать даёт дают взять берет берут'
87
+ ).split(/\s+/),
88
+ );
89
+
90
+ /**
91
+ * The distinctive-token set of an idea text: lowercased word-ish tokens (letters/digits, allowing
92
+ * inner `_ . - /` so identifiers and paths survive whole), length >= 4, register stopwords removed,
93
+ * Cyrillic words longer than 6 chars folded to their 6-char prefix (a crude but MEASURED-adequate
94
+ * stem that matches RU inflections: подкрепление/подкрепления → подкре).
95
+ */
96
+ export function distinctiveTokens(text: string): Set<string> {
97
+ const out = new Set<string>();
98
+ for (const m of text.toLowerCase().matchAll(/[\p{L}\p{N}][\p{L}\p{N}_.\-/]*/gu)) {
99
+ let w = (m[0] ?? '').replace(/[.\-/]+$/, '');
100
+ if (w.length < 4 || REGISTER_STOPWORDS.has(w)) continue;
101
+ if (/[а-яё]/.test(w) && w.length > 6) w = w.slice(0, 6);
102
+ out.add(w);
103
+ }
104
+ return out;
105
+ }
106
+
107
+ /**
108
+ * Lexical containment in [0,1]: |A ∩ B| / min(|A|, |B|) over distinctive tokens. `min` (containment,
109
+ * not Jaccard) so a SHORT re-capture of a LONG idea — whose vocabulary is a subset — still scores
110
+ * high; that asymmetry is exactly the length-moved-the-verdict incident. Empty token sets score 0
111
+ * (no evidence is not corroboration).
112
+ */
113
+ export function lexicalContainment(a: string, b: string): number {
114
+ const ta = distinctiveTokens(a);
115
+ const tb = distinctiveTokens(b);
116
+ if (ta.size === 0 || tb.size === 0) return 0;
117
+ let inter = 0;
118
+ for (const w of ta) if (tb.has(w)) inter++;
119
+ return inter / Math.min(ta.size, tb.size);
120
+ }
121
+
122
+ /** The corroboration knobs of the two-signal decision (defaults in `readBacklogConfig`). */
123
+ export interface DedupBandConfig {
124
+ readonly duplicateThreshold: number;
125
+ readonly relatednessFloor: number;
126
+ /** DUPLICATE-by-cosine additionally requires containment >= this (undefined containment waives). */
127
+ readonly corroborationFloor: number;
128
+ /** Containment >= this + cosine >= subsetCosineFloor promotes to duplicate BELOW the threshold. */
129
+ readonly subsetContainment: number;
130
+ readonly subsetCosineFloor: number;
131
+ }
132
+
133
+ export type DedupPairBand = 'duplicate' | 'demoted' | 'subset-duplicate' | 'below';
134
+
135
+ /**
136
+ * THE two-signal pair decision — shared by `classifyDedup` (capture) and `harmonizeBacklog` (batch)
137
+ * so the two paths can never disagree about what a duplicate is.
138
+ *
139
+ * duplicate cosine >= duplicateThreshold AND (containment unknown OR >= corroborationFloor)
140
+ * demoted cosine >= duplicateThreshold but containment < corroborationFloor — the
141
+ * register-only false positive (zombie x publish-gate @ 0.941, containment 0.077):
142
+ * high cosine with DISJOINT subject vocabulary is not a duplicate.
143
+ * subset-duplicate cosine in [subsetCosineFloor, duplicateThreshold) AND containment >=
144
+ * subsetContainment — the same idea re-captured at a different LENGTH (measured
145
+ * long-vs-short patient-values: cosine 0.8019, containment 0.971).
146
+ * below everything else (related/new banding is the caller's job).
147
+ */
148
+ export function dedupPairBand(cosine: number, containment: number | undefined, cfg: DedupBandConfig): DedupPairBand {
149
+ if (cosine >= cfg.duplicateThreshold) {
150
+ return containment === undefined || containment >= cfg.corroborationFloor ? 'duplicate' : 'demoted';
151
+ }
152
+ if (containment !== undefined && containment >= cfg.subsetContainment && cosine >= cfg.subsetCosineFloor) {
153
+ return 'subset-duplicate';
154
+ }
155
+ return 'below';
156
+ }