@dzhechkov/harness-core 0.7.2 → 0.7.4

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 (62) hide show
  1. package/.dz-manifest.json +121 -41
  2. package/README.md +1 -1
  3. package/dist/cli-flag-notice.d.ts +50 -0
  4. package/dist/cli-flag-notice.d.ts.map +1 -0
  5. package/dist/cli-flag-notice.js +106 -0
  6. package/dist/cli-flag-notice.js.map +1 -0
  7. package/dist/event-chain.d.ts +50 -0
  8. package/dist/event-chain.d.ts.map +1 -1
  9. package/dist/event-chain.js +31 -0
  10. package/dist/event-chain.js.map +1 -1
  11. package/dist/index.d.ts +9 -5
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +7 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/name-check.d.ts +98 -0
  16. package/dist/name-check.d.ts.map +1 -0
  17. package/dist/name-check.js +333 -0
  18. package/dist/name-check.js.map +1 -0
  19. package/dist/operations.d.ts.map +1 -1
  20. package/dist/operations.js +33 -5
  21. package/dist/operations.js.map +1 -1
  22. package/dist/provenance.d.ts +100 -92
  23. package/dist/provenance.d.ts.map +1 -1
  24. package/dist/provenance.js +122 -122
  25. package/dist/provenance.js.map +1 -1
  26. package/dist/recall-domain-boost.d.ts +10 -3
  27. package/dist/recall-domain-boost.d.ts.map +1 -1
  28. package/dist/recall-domain-boost.js +7 -0
  29. package/dist/recall-domain-boost.js.map +1 -1
  30. package/dist/recall-hook-policy.d.ts +15 -0
  31. package/dist/recall-hook-policy.d.ts.map +1 -1
  32. package/dist/recall-hook-policy.js +59 -0
  33. package/dist/recall-hook-policy.js.map +1 -1
  34. package/dist/recap.d.ts +146 -0
  35. package/dist/recap.d.ts.map +1 -0
  36. package/dist/recap.js +346 -0
  37. package/dist/recap.js.map +1 -0
  38. package/dist/retro.d.ts +131 -0
  39. package/dist/retro.d.ts.map +1 -0
  40. package/dist/retro.js +207 -0
  41. package/dist/retro.js.map +1 -0
  42. package/dist/score.d.ts +21 -0
  43. package/dist/score.d.ts.map +1 -1
  44. package/dist/score.js +44 -3
  45. package/dist/score.js.map +1 -1
  46. package/dist/vector-tier.d.ts +54 -0
  47. package/dist/vector-tier.d.ts.map +1 -1
  48. package/dist/vector-tier.js +69 -8
  49. package/dist/vector-tier.js.map +1 -1
  50. package/package.json +8 -8
  51. package/sbom.json +240 -40
  52. package/src/cli-flag-notice.ts +114 -0
  53. package/src/event-chain.ts +64 -0
  54. package/src/index.ts +13 -0
  55. package/src/name-check.ts +331 -0
  56. package/src/operations.ts +34 -6
  57. package/src/provenance.ts +217 -0
  58. package/src/recall-domain-boost.ts +10 -3
  59. package/src/recall-hook-policy.ts +60 -0
  60. package/src/recap.ts +462 -0
  61. package/src/score.ts +53 -3
  62. package/src/vector-tier.ts +109 -10
@@ -133,8 +133,21 @@ export interface ImportVectorRow {
133
133
  }
134
134
 
135
135
  /** The engine PORT — both adapters implement exactly this surface (04 §4.1). */
136
+ /**
137
+ * Is this engine's `similarity` a real cosine, or only good enough to RANK by?
138
+ *
139
+ * `agentdb` computes a magnitude-normalised cosine over the stored vectors, so its number is
140
+ * comparable and lives in the space the recall floors were calibrated in. The `rvf` adapter returns
141
+ * `-distance` — unbounded, and the metric is not established — so its number may only order rows.
142
+ * Printing it beside a 0.38 cosine floor would hand the reader a figure that looks calibrated and is
143
+ * not; that is the one lie this feature must not tell (ADR, features/recall-true-closeness).
144
+ */
145
+ export type SimilarityKind = 'cosine' | 'rank-only';
146
+
136
147
  export interface VectorEngine {
137
148
  readonly kind: VectorEngineKind;
149
+ /** Absent means `rank-only`: an engine must SAY its number is a cosine to have it shown as one. */
150
+ readonly similarityKind?: SimilarityKind;
138
151
  upsert(entries: readonly VectorEntry[]): Promise<{ indexed: number; error?: string | undefined }>;
139
152
  search(query: string, limit: number): Promise<{ hits: VectorHit[]; error?: string | undefined }>;
140
153
  listIds(): Promise<{ ids: string[]; error?: string | undefined }>;
@@ -173,6 +186,14 @@ export interface HybridHit {
173
186
  readonly score: number;
174
187
  /** lesson-quarantine: set only for a quarantined hit — display marks ⚠q, ranking was damped. */
175
188
  readonly quarantined?: boolean;
189
+ /**
190
+ * Raw closeness from the semantic leg, when the engine reports a genuine cosine. ABSENT for a
191
+ * lexical-only hit and for an engine whose score is not a cosine — the honest display there is a
192
+ * dash, never a substitute number. On a `both` hit this is the SEMANTIC leg's cosine, which
193
+ * explains less of the ordering than it may appear to: the list is ranked by RRF plus learning
194
+ * signals, not by this.
195
+ */
196
+ readonly similarity?: number;
176
197
  }
177
198
 
178
199
  /** Outcome of {@link recallHybrid}. With no engine this is content-identical to `recallPatterns`. */
@@ -243,6 +264,8 @@ export interface VectorTierStatus {
243
264
  * project print a fully-healthy status line over a dead writer (ADR-001).
244
265
  */
245
266
  readonly mirrorWriterEnabled: boolean;
267
+ /** WHY it is on or off — so the surface printing it cannot invent a cause (see mirrorWriterReason). */
268
+ readonly mirrorWriterState: MirrorWriterState;
246
269
  /**
247
270
  * Vectors of OTHER dz-owned task types (today: `dz-backlog`). Reported separately so a store of N
248
271
  * vectors can be fully accounted for, and never folded into `mirrored`, which must stay comparable
@@ -521,15 +544,63 @@ export function readHarmonizeThreshold(projectRoot: string): number {
521
544
  * `false`, so its `dz teach` output stays byte-identical to the pre-feature baseline (AC-1).
522
545
  */
523
546
  export function vectorMirrorEnabled(projectRoot: string): boolean {
547
+ return mirrorWriterReason(projectRoot).enabled;
548
+ }
549
+
550
+ /** Why the mirror writer is on or off — a CLOSED set, so a caller cannot invent a cause. */
551
+ export type MirrorWriterState =
552
+ | 'on'
553
+ /** No `.dz/config.json` at all — an unconfigured project, which is the normal quiet case. */
554
+ | 'no-config'
555
+ /** The config exists but could not be read or parsed. */
556
+ | 'config-unreadable'
557
+ /** The config is readable and simply does not enable a mirror. */
558
+ | 'not-enabled'
559
+ /** The config explicitly turns the vector tier off. */
560
+ | 'engine-off';
561
+
562
+ /**
563
+ * The mirror writer's state AND its real cause.
564
+ *
565
+ * Every failure used to collapse into `false`, and the one message printed above it named ONE
566
+ * specific cause: "no memory.backend=agentdb". MEASURED 2026-08-24 with a config that sets
567
+ * `memory.vector.engine: "off"` — the status line correctly said OFF and then blamed a setting the
568
+ * config did not mention. A diagnosis that names the wrong cause sends the reader to fix something
569
+ * that is not broken, which is worse than saying nothing.
570
+ */
571
+ export function mirrorWriterReason(projectRoot: string): { enabled: boolean; state: MirrorWriterState } {
572
+ const path = join(projectRoot, '.dz', 'config.json');
573
+ if (!existsSync(path)) return { enabled: false, state: 'no-config' };
574
+ let cfg: { memory?: { backend?: string; vector?: { engine?: string } } };
524
575
  try {
525
- const cfg = JSON.parse(readFileSync(join(projectRoot, '.dz', 'config.json'), 'utf-8')) as {
526
- memory?: { backend?: string; vector?: { engine?: string } };
527
- };
528
- if (cfg.memory?.backend === 'agentdb') return true;
529
- const engine = cfg.memory?.vector?.engine;
530
- return engine === 'agentdb' || engine === 'rvf';
576
+ cfg = JSON.parse(readFileSync(path, 'utf-8')) as typeof cfg;
531
577
  } catch {
532
- return false;
578
+ return { enabled: false, state: 'config-unreadable' };
579
+ }
580
+ const engine = cfg.memory?.vector?.engine;
581
+ // An explicit `off` WINS over `memory.backend`, because that is what the engine resolution itself
582
+ // does. With `{"memory":{"backend":"agentdb","vector":{"engine":"off"}}}` the same status output
583
+ // printed `Engine: none — vector tier disabled` and `Mirror writer: ON` one line apart
584
+ // (cross-family review, codex `gpt-5.6-sol`, 2026-08-24). A report that contradicts itself inside
585
+ // one screen is worse than either half alone, and the half that was wrong is this one: nothing can
586
+ // queue to a tier that is off.
587
+ if (engine === 'off') return { enabled: false, state: 'engine-off' };
588
+ if (cfg.memory?.backend === 'agentdb') return { enabled: true, state: 'on' };
589
+ if (engine === 'agentdb' || engine === 'rvf') return { enabled: true, state: 'on' };
590
+ return { enabled: false, state: 'not-enabled' };
591
+ }
592
+
593
+ /** The sentence a reader can act on, for each state. */
594
+ export function mirrorWriterExplanation(state: MirrorWriterState): string {
595
+ switch (state) {
596
+ case 'on': return 'teach is queueing to the mirror';
597
+ // "has never configured" is a claim about HISTORY from an observation about the PRESENT: the
598
+ // file may be tracked in git and merely deleted from the working tree (cross-family review,
599
+ // codex gpt-5.6-sol, 2026-08-24). Absence proves only absence.
600
+ case 'no-config': return 'no .dz/config.json here — memory is not configured in this working tree, and nothing is queueing';
601
+ case 'config-unreadable': return '.dz/config.json exists but could not be read or parsed — fix the file, not the settings';
602
+ case 'engine-off': return '.dz/config.json sets memory.vector.engine = "off" — the tier is deliberately disabled';
603
+ case 'not-enabled': return '.dz/config.json enables no mirror (needs memory.backend=agentdb, or memory.vector.engine=agentdb|rvf) — teach is NOT queueing';
533
604
  }
534
605
  }
535
606
 
@@ -806,6 +877,13 @@ export interface RankedPattern {
806
877
  readonly id: string;
807
878
  readonly pattern: PatternRecord;
808
879
  readonly backend: RecallHit['backend'];
880
+ /**
881
+ * The semantic leg's raw closeness, when the engine reports a real cosine. Rides ALONGSIDE the RRF
882
+ * score and never enters the ranking maths — four things depend on RRF magnitude (the reinforce
883
+ * cap, quarantine damping, the learning uplift, and an ADR-level note in backlog.ts), so this is a
884
+ * sibling field, never a repurposing.
885
+ */
886
+ readonly similarity?: number;
809
887
  }
810
888
 
811
889
  const RRF_K = 60;
@@ -827,7 +905,7 @@ export function mergeHybridHits(
827
905
  opts: { readonly limit: number; readonly semanticWeight?: number | undefined },
828
906
  ): HybridHit[] {
829
907
  const weight = opts.semanticWeight ?? 1;
830
- interface Acc { pattern: PatternRecord; lex?: RecallHit['backend']; sem: boolean; score: number }
908
+ interface Acc { pattern: PatternRecord; lex?: RecallHit['backend']; sem: boolean; score: number; similarity?: number }
831
909
  const acc = new Map<string, Acc>();
832
910
  lexical.forEach((h, rank) => {
833
911
  const cur = acc.get(h.id) ?? { pattern: h.pattern, sem: false, score: 0 };
@@ -839,6 +917,10 @@ export function mergeHybridHits(
839
917
  const cur = acc.get(h.id) ?? { pattern: h.pattern, sem: false, score: 0 };
840
918
  cur.sem = true;
841
919
  cur.score += weight / (RRF_K + rank + 1);
920
+ // The cosine rides along untouched by the ranking maths. On a hit both legs found, this is the
921
+ // SEMANTIC leg's number — stated in the field's own doc, because it explains less of the order
922
+ // than it looks like it does.
923
+ if (h.similarity !== undefined) cur.similarity = h.similarity;
842
924
  acc.set(h.id, cur);
843
925
  });
844
926
  // Ties break by EVIDENCE, not by the id alphabet: a hit both legs found outranks one only a single
@@ -853,6 +935,7 @@ export function mergeHybridHits(
853
935
  pattern: v.pattern,
854
936
  backend: v.lex !== undefined && v.sem ? ('both' as const) : v.lex ?? ('vector' as const),
855
937
  score: v.score,
938
+ ...(v.similarity === undefined ? {} : { similarity: v.similarity }),
856
939
  });
857
940
  // `slice(0, -1)` drops the LAST element instead of returning nothing, so a negative limit used to
858
941
  // return almost the whole list (MEASURED: limit -1 over 5 candidates returned 4). `dz recall`
@@ -1016,6 +1099,8 @@ export async function recallHybrid(
1016
1099
  const p = recordToPattern(r);
1017
1100
  idToPattern.set(r.id, p);
1018
1101
  }
1102
+ // Only a declared cosine is allowed to travel as one (ADR: per-engine honesty gate).
1103
+ const cosineEngine = engine.similarityKind === 'cosine';
1019
1104
  const semantic: RankedPattern[] = [];
1020
1105
  const seen = new Set<string>();
1021
1106
  for (const h of sr.hits) {
@@ -1023,7 +1108,13 @@ export async function recallHybrid(
1023
1108
  const p = idToPattern.get(h.dzId);
1024
1109
  if (p === undefined) continue; // vector-only orphan — the store pruned/expired it; NEVER resurrect
1025
1110
  seen.add(h.dzId);
1026
- semantic.push({ id: h.dzId, pattern: p, backend: 'vector' });
1111
+ // The cosine was computed by the engine and then thrown away here — `h.similarity` was read
1112
+ // nowhere in this function, and `relevance` in `--json` has been the RRF rank surrogate ever
1113
+ // since (MEASURED: a nonsense query and a meaningful one both score 1/61 at the top). It now
1114
+ // travels with the hit. Only a genuine cosine travels: an engine whose score is a negated
1115
+ // distance reports `similarityKind: 'rank-only'` and contributes nothing here.
1116
+ semantic.push({ id: h.dzId, pattern: p, backend: 'vector',
1117
+ ...(cosineEngine && Number.isFinite(h.similarity) ? { similarity: h.similarity } : {}) });
1027
1118
  }
1028
1119
  const lex: RankedPattern[] = lexical.map((h) => ({
1029
1120
  id: identityToId.get(patternIdentityOf(h.pattern)) ?? patternRecordId(h.pattern),
@@ -1085,7 +1176,8 @@ export async function vectorTierStatus(
1085
1176
  const lexicalMirrorable = mirrorableRecords.length;
1086
1177
  const pendingEntries = readVectorPending(projectRoot);
1087
1178
  const pending = pendingEntries.length;
1088
- const mirrorWriterEnabled = vectorMirrorEnabled(projectRoot);
1179
+ const mirror = mirrorWriterReason(projectRoot);
1180
+ const mirrorWriterEnabled = mirror.enabled;
1089
1181
  const resolved = pickEngine(projectRoot, opts);
1090
1182
  if (resolved.engine === undefined) {
1091
1183
  // No engine to ask ⇒ the debt is UNKNOWN. Reporting 0 here is precisely the defect this feature
@@ -1099,6 +1191,7 @@ export async function vectorTierStatus(
1099
1191
  lexicalMirrorable,
1100
1192
  pending,
1101
1193
  mirrorWriterEnabled,
1194
+ mirrorWriterState: mirror.state,
1102
1195
  };
1103
1196
  }
1104
1197
  const engine = resolved.engine;
@@ -1157,6 +1250,7 @@ export async function vectorTierStatus(
1157
1250
  mirrored,
1158
1251
  pending,
1159
1252
  mirrorWriterEnabled,
1253
+ mirrorWriterState: mirror.state,
1160
1254
  ...(unmirrored !== undefined ? { unmirrored } : {}),
1161
1255
  ...(mirroredOther !== undefined ? { mirroredOther } : {}),
1162
1256
  ...(orphaned !== undefined ? { orphaned } : {}),
@@ -1613,6 +1707,9 @@ export async function importRvfCheckpoint(projectRoot: string, source: string, o
1613
1707
  function agentdbVectorEngine(projectRoot: string): VectorEngine {
1614
1708
  return {
1615
1709
  kind: 'agentdb',
1710
+ // A magnitude-normalising cosine over the stored vectors — comparable, and in the same space the
1711
+ // recall floors were calibrated in.
1712
+ similarityKind: 'cosine',
1616
1713
  async upsert(entries) {
1617
1714
  const r = await indexPatternsToAgentdb(
1618
1715
  projectRoot,
@@ -1775,6 +1872,8 @@ function rvfVectorEngine(projectRoot: string): VectorEngine {
1775
1872
  const noEmbedder = 'rvf engine present but no embedder — install agentdb (dz setup --memory agentdb)';
1776
1873
  return {
1777
1874
  kind: 'rvf',
1875
+ // `-distance`, unbounded, metric not established: good for ORDER, never for a threshold.
1876
+ similarityKind: 'rank-only',
1778
1877
  async upsert(entries) {
1779
1878
  const idmap = readRvfIdmap(projectRoot);
1780
1879
  const guard = guardRvfEmbedSpace(projectRoot, idmap);