@dzhechkov/harness-core 0.7.12 → 0.8.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 (105) hide show
  1. package/.dz-manifest.json +210 -70
  2. package/README.md +34 -1
  3. package/dist/book-kb.d.ts.map +1 -1
  4. package/dist/book-kb.js +17 -0
  5. package/dist/book-kb.js.map +1 -1
  6. package/dist/feature-adr-checkpoints.d.ts +61 -0
  7. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  8. package/dist/feature-adr-checkpoints.js +116 -2
  9. package/dist/feature-adr-checkpoints.js.map +1 -1
  10. package/dist/index.d.ts +16 -2
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +13 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/loop-blobs.generated.js +2 -2
  15. package/dist/loop-blobs.generated.js.map +1 -1
  16. package/dist/mutation-gate.d.ts +31 -8
  17. package/dist/mutation-gate.d.ts.map +1 -1
  18. package/dist/mutation-gate.js +57 -16
  19. package/dist/mutation-gate.js.map +1 -1
  20. package/dist/named-lock.d.ts.map +1 -1
  21. package/dist/named-lock.js +11 -9
  22. package/dist/named-lock.js.map +1 -1
  23. package/dist/native-dep-probe.d.ts +11 -0
  24. package/dist/native-dep-probe.d.ts.map +1 -0
  25. package/dist/native-dep-probe.js +100 -0
  26. package/dist/native-dep-probe.js.map +1 -0
  27. package/dist/operations.d.ts.map +1 -1
  28. package/dist/operations.js +82 -1
  29. package/dist/operations.js.map +1 -1
  30. package/dist/parser-safe-region.d.ts +47 -0
  31. package/dist/parser-safe-region.d.ts.map +1 -0
  32. package/dist/parser-safe-region.js +47 -0
  33. package/dist/parser-safe-region.js.map +1 -0
  34. package/dist/patterns.d.ts.map +1 -1
  35. package/dist/patterns.js +10 -1
  36. package/dist/patterns.js.map +1 -1
  37. package/dist/profile.d.ts +202 -0
  38. package/dist/profile.d.ts.map +1 -0
  39. package/dist/profile.js +536 -0
  40. package/dist/profile.js.map +1 -0
  41. package/dist/publish.d.ts +5 -3
  42. package/dist/publish.d.ts.map +1 -1
  43. package/dist/publish.js +46 -8
  44. package/dist/publish.js.map +1 -1
  45. package/dist/qe-rounds.d.ts +75 -0
  46. package/dist/qe-rounds.d.ts.map +1 -0
  47. package/dist/qe-rounds.js +174 -0
  48. package/dist/qe-rounds.js.map +1 -0
  49. package/dist/registry.d.ts.map +1 -1
  50. package/dist/registry.js +5 -1
  51. package/dist/registry.js.map +1 -1
  52. package/dist/run-records.d.ts.map +1 -1
  53. package/dist/run-records.js +8 -1
  54. package/dist/run-records.js.map +1 -1
  55. package/dist/setup.d.ts +35 -4
  56. package/dist/setup.d.ts.map +1 -1
  57. package/dist/setup.js +86 -23
  58. package/dist/setup.js.map +1 -1
  59. package/dist/sign.d.ts +29 -24
  60. package/dist/sign.d.ts.map +1 -1
  61. package/dist/sign.js +183 -61
  62. package/dist/sign.js.map +1 -1
  63. package/dist/store-location.d.ts +56 -0
  64. package/dist/store-location.d.ts.map +1 -0
  65. package/dist/store-location.js +56 -0
  66. package/dist/store-location.js.map +1 -0
  67. package/dist/store-lock.d.ts +1 -0
  68. package/dist/store-lock.d.ts.map +1 -1
  69. package/dist/store-lock.js +7 -1
  70. package/dist/store-lock.js.map +1 -1
  71. package/dist/store-merge.d.ts +55 -0
  72. package/dist/store-merge.d.ts.map +1 -0
  73. package/dist/store-merge.js +77 -0
  74. package/dist/store-merge.js.map +1 -0
  75. package/dist/teach-target.d.ts +61 -0
  76. package/dist/teach-target.d.ts.map +1 -0
  77. package/dist/teach-target.js +105 -0
  78. package/dist/teach-target.js.map +1 -0
  79. package/dist/vector-tier.d.ts.map +1 -1
  80. package/dist/vector-tier.js +63 -20
  81. package/dist/vector-tier.js.map +1 -1
  82. package/package.json +13 -13
  83. package/sbom.json +424 -70
  84. package/src/book-kb.ts +17 -0
  85. package/src/feature-adr-checkpoints.ts +111 -2
  86. package/src/index.ts +39 -1
  87. package/src/loop-blobs.generated.ts +2 -2
  88. package/src/mutation-gate.ts +74 -17
  89. package/src/named-lock.ts +10 -8
  90. package/src/native-dep-probe.ts +118 -0
  91. package/src/operations.ts +77 -1
  92. package/src/parser-safe-region.ts +65 -0
  93. package/src/patterns.ts +10 -1
  94. package/src/profile.ts +593 -0
  95. package/src/publish.ts +43 -7
  96. package/src/qe-rounds.ts +226 -0
  97. package/src/registry.ts +5 -1
  98. package/src/run-records.ts +9 -1
  99. package/src/setup.ts +87 -23
  100. package/src/sign.ts +180 -60
  101. package/src/store-location.ts +76 -0
  102. package/src/store-lock.ts +8 -1
  103. package/src/store-merge.ts +91 -0
  104. package/src/teach-target.ts +126 -0
  105. package/src/vector-tier.ts +58 -9
@@ -0,0 +1,126 @@
1
+ /**
2
+ * teach-target — which store does THIS lesson belong to, and who decided.
3
+ *
4
+ * The owner asked for a per-session choice: "in this session accumulate per project; in another,
5
+ * across all projects." For a CLI every invocation is a fresh process, so there is no session
6
+ * object to hold a mode. An environment variable IS a shell session — set once, governs every
7
+ * command in that terminal, dies with it. A persisted file would outlive what the user meant by
8
+ * "this session" and become a mode nobody remembers setting.
9
+ *
10
+ * This increment is third on purpose. A mode set once and forgotten lies silently, and it is safe
11
+ * here only because `learning-store-says-where` already makes every write announce its destination.
12
+ * This adds the other half of that sentence: not only WHERE the lesson landed, but WHY that store
13
+ * was chosen. A fact that is not stated cannot be checked.
14
+ */
15
+
16
+ import { readFileSync } from 'node:fs';
17
+ import { join } from 'node:path';
18
+
19
+ /** Where a lesson is written. */
20
+ export type TeachStore = 'project' | 'global';
21
+
22
+ /** Who decided — the reason is printed, so a forgotten mode cannot stay invisible. */
23
+ export type TeachReason = 'flag' | 'environment' | 'config' | 'default';
24
+
25
+ export interface TeachTarget {
26
+ readonly store: TeachStore;
27
+ readonly reason: TeachReason;
28
+ }
29
+
30
+ /** Refusal, never a silent fallback. */
31
+ export class TeachTargetError extends Error {}
32
+
33
+ export const TEACH_STORES: readonly TeachStore[] = ['project', 'global'];
34
+
35
+ const VALID = new Set<string>(TEACH_STORES);
36
+
37
+ /**
38
+ * One level of input, validated.
39
+ *
40
+ * An unknown value is REFUSED and the source is named, because the cure differs: a bad flag is a
41
+ * typo on this command line, a bad environment variable was exported hours ago in another window,
42
+ * and a bad config is committed and affects everyone on the project.
43
+ *
44
+ * `DZ_LEARN=globl` quietly writing to the project store would be the invisible mislabel this whole
45
+ * mode exists to prevent, arriving through the mode itself.
46
+ */
47
+ function validate(value: string | undefined, source: string): TeachStore | undefined {
48
+ if (value === undefined) return undefined;
49
+ const v = value.trim();
50
+ if (v === '') return undefined; // an empty export is not a choice
51
+ if (!VALID.has(v)) {
52
+ throw new TeachTargetError(
53
+ `${source}: unknown store ${JSON.stringify(v)} — expected one of: ${TEACH_STORES.join(', ')}`,
54
+ );
55
+ }
56
+ return v as TeachStore;
57
+ }
58
+
59
+ /**
60
+ * Resolve the destination.
61
+ *
62
+ * Precedence, most specific first: `--to` → `DZ_LEARN` → project config → `project`.
63
+ * That ordering is the only one a user can predict without reading the source.
64
+ *
65
+ * **The default does not move.** MEASURED: 361 records in this repo's own store were written under
66
+ * today's behaviour, and every other user's store is the same. Redirecting an un-flagged `dz teach`
67
+ * would silently change every existing workflow, and the user would find out when a lesson was not
68
+ * where they expected it.
69
+ */
70
+ export function resolveTeachTarget(input: {
71
+ flag?: string | undefined;
72
+ env?: string | undefined;
73
+ config?: string | undefined;
74
+ }): TeachTarget {
75
+ const flag = validate(input.flag, '--to');
76
+ if (flag !== undefined) return { store: flag, reason: 'flag' };
77
+
78
+ const env = validate(input.env, 'DZ_LEARN');
79
+ if (env !== undefined) return { store: env, reason: 'environment' };
80
+
81
+ const cfg = validate(input.config, '.dz/config.json learning.teachTo');
82
+ if (cfg !== undefined) return { store: cfg, reason: 'config' };
83
+
84
+ return { store: 'project', reason: 'default' };
85
+ }
86
+
87
+ /**
88
+ * How the reason reads in the store line.
89
+ *
90
+ * Each phrase names WHERE the decision came from, so a user who set `DZ_LEARN=global` three hours
91
+ * ago and forgot has something to recognise. `default` deliberately says nothing extra: adding a
92
+ * phrase there would change the line for every user who set nothing, breaking the byte-identity
93
+ * this feature promises them.
94
+ */
95
+ export function teachReasonPhrase(reason: TeachReason): string {
96
+ switch (reason) {
97
+ case 'flag': return 'chosen by --to';
98
+ case 'environment': return 'chosen by DZ_LEARN in this shell';
99
+ case 'config': return 'chosen by .dz/config.json';
100
+ case 'default': return '';
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Read the project default from `.dz/config.json` → `learning.teachTo`.
106
+ *
107
+ * Returns the RAW string, not a validated store: an unreadable file is "no opinion", but a file
108
+ * that says `teachTo: "globl"` HAS an opinion and must be refused by `resolveTeachTarget` rather
109
+ * than swallowed here. Those two cases are different and the caller can only tell them apart if
110
+ * this function keeps them apart.
111
+ */
112
+ export function readTeachToConfig(projectRoot: string): string | undefined {
113
+ try {
114
+ const parsed = JSON.parse(
115
+ readFileSync(join(projectRoot, '.dz', 'config.json'), 'utf-8'),
116
+ ) as { learning?: { teachTo?: unknown } };
117
+ const raw = parsed.learning?.teachTo;
118
+ if (raw === undefined || raw === null) return undefined; // no key is no opinion
119
+ // A present key of the WRONG TYPE (`teachTo: true`) is still an opinion, and a wrong one.
120
+ // Returning undefined here would fall through to the project default silently — the exact
121
+ // fail-open the resolver refuses everywhere else (cross-family QE, 2026-08-27).
122
+ return typeof raw === 'string' ? raw : JSON.stringify(raw);
123
+ } catch {
124
+ return undefined; // missing or unparseable config is no opinion at all
125
+ }
126
+ }
@@ -75,6 +75,12 @@ import {
75
75
  import { BACKLOG_TASK_TYPE } from './backlog.js';
76
76
  import { currentEmbedManifest, guardEmbedSpace, DEFAULT_EMBED_DIM, resolveEmbedModel, type EmbedModelConfig } from './embedding-config.js';
77
77
  import { applyLearningSignals, applyLearningSignalsWithDelta, applyLearningSignalsWithTerms, resolveLearningBackend, type LearningSignalBackend, type RerankTerm } from './learning-backend.js';
78
+ import {
79
+ describeNativeDep,
80
+ exerciseSqliteOpen,
81
+ probeNativeDep,
82
+ type NativeDepVerdict,
83
+ } from './native-dep-probe.js';
78
84
  // lesson-bandit-rerank: the payoff axis. ONE-DIRECTIONAL — lesson-payoff imports patterns.js and
79
85
  // nothing from here, so there is no cycle.
80
86
  import {
@@ -634,8 +640,11 @@ export function mirrorWriterExplanation(state: MirrorWriterState): string {
634
640
  }
635
641
 
636
642
  /**
637
- * Engine selection cascade: config mode → `require.resolve` probe (NO module load a
638
- * lexical-only project pays zero engine cost, NFR-5) adapter or an honest reason.
643
+ * Engine selection cascade: config mode → project-local package gate native usability probe
644
+ * adapter or an honest reason. A lexical-only project still pays nothing because
645
+ * {@link isPackageInstalled} gates the probe; a project with the dependency installed pays one
646
+ * require + one in-memory open per process (measured: agentdb 53.6 ms, better-sqlite3 8.6 ms,
647
+ * 62.2 ms total, versus about 0.008 ms for the old existsSync-only check).
639
648
  * Never throws. `auto` prefers agentdb (it reads the vectors consolidate already wrote),
640
649
  * falling through to rvf.
641
650
  */
@@ -659,19 +668,43 @@ function isPackageInstalled(projectRoot: string, pkgName: string): boolean {
659
668
  export function resolveVectorEngine(projectRoot: string): ResolvedVectorEngine {
660
669
  const mode = readVectorEngineMode(projectRoot);
661
670
  if (mode === 'off') return { reason: 'vector tier disabled (memory.vector.engine = "off")' };
662
- const canResolve = (id: string): boolean => isPackageInstalled(projectRoot, id);
671
+ let autoAgentdbFailure: string | undefined;
672
+ const probeIfInstalled = (id: string): NativeDepVerdict =>
673
+ isPackageInstalled(projectRoot, id)
674
+ ? id === 'better-sqlite3'
675
+ ? probeNativeDep(projectRoot, id, exerciseSqliteOpen)
676
+ : probeNativeDep(projectRoot, id)
677
+ : { state: 'absent', pkg: id };
663
678
  if (mode === 'agentdb' || mode === 'auto') {
664
- if (canResolve('agentdb') && canResolve('better-sqlite3')) return { engine: agentdbVectorEngine(projectRoot) };
679
+ const agentdb = probeIfInstalled('agentdb');
680
+ if (agentdb.state === 'unusable') {
681
+ const reason = describeNativeDep(agentdb);
682
+ if (mode === 'agentdb') return { reason };
683
+ autoAgentdbFailure = reason;
684
+ }
685
+ const sqlite = agentdb.state === 'usable' ? probeIfInstalled('better-sqlite3') : undefined;
686
+ if (sqlite?.state === 'unusable') return { reason: describeNativeDep(sqlite) };
687
+ if (agentdb.state === 'usable' && sqlite?.state === 'usable') {
688
+ return { engine: agentdbVectorEngine(projectRoot) };
689
+ }
665
690
  if (mode === 'agentdb') {
666
691
  return { reason: 'agentdb/better-sqlite3 not installed in project (run: dz setup --memory agentdb)' };
667
692
  }
668
693
  }
669
694
  if (mode === 'rvf' || mode === 'auto') {
670
- if (canResolve('@ruvector/rvf')) return { engine: rvfVectorEngine(projectRoot) };
695
+ const rvf = probeIfInstalled('@ruvector/rvf');
696
+ if (rvf.state === 'unusable') {
697
+ const reason = describeNativeDep(rvf);
698
+ return { reason: autoAgentdbFailure === undefined ? reason : `${autoAgentdbFailure}; ${reason}` };
699
+ }
700
+ if (rvf.state === 'usable') return { engine: rvfVectorEngine(projectRoot) };
671
701
  if (mode === 'rvf') {
672
702
  return { reason: '@ruvector/rvf not installed in project (npm i @ruvector/rvf) — vector tier inactive' };
673
703
  }
674
704
  }
705
+ if (autoAgentdbFailure !== undefined) {
706
+ return { reason: `${autoAgentdbFailure}; @ruvector/rvf not installed in project (npm i @ruvector/rvf) — vector tier inactive` };
707
+ }
675
708
  return { reason: 'no vector engine available: agentdb/better-sqlite3 not installed in project (run: dz setup --memory agentdb)' };
676
709
  }
677
710
 
@@ -1199,7 +1232,23 @@ export async function recallHybrid(
1199
1232
  semanticRanked: 0,
1200
1233
  ...extra,
1201
1234
  ...(banditReport !== undefined ? { bandit: banditReport } : {}),
1202
- ...(banditReport !== undefined && opts.deferExposures === true
1235
+ ...deferredCommit(),
1236
+ };
1237
+ };
1238
+
1239
+ /**
1240
+ * The deferred-exposure commit, built ONCE and attached by EVERY return path.
1241
+ *
1242
+ * The first draft attached it only inside `lexicalOnly()` — the FALLBACK. On the main hybrid path
1243
+ * (a vector engine present, i.e. the normal case) the field was absent, so `markRecallHits` had
1244
+ * already returned early on `deferred: true` and NOTHING ever committed the exposures. MEASURED
1245
+ * the same day: an armed recall printed `bandit payoff: … state absent` while `.dz/lesson-bandit/`
1246
+ * was never created — the feature reported itself running while its reward feed was severed.
1247
+ * A ranking feature that quietly stops ranking looks exactly like one that works; that sentence is
1248
+ * in the vendored engine's own header, and the failure recurred one layer down anyway.
1249
+ */
1250
+ function deferredCommit(): { commitExposures?: (shownDzIds: readonly string[]) => void } {
1251
+ return banditReport !== undefined && opts.deferExposures === true
1203
1252
  ? {
1204
1253
  /** Record exposures for the hits the caller actually PRINTED. Everything it needs is in
1205
1254
  * the report plus the shown ids — no closure over the pre-cut hit list, so there is no
@@ -1217,9 +1266,8 @@ export async function recallHybrid(
1217
1266
  } catch { /* a derived index never blocks the recall return (NFR-5) */ }
1218
1267
  },
1219
1268
  }
1220
- : {}),
1221
- };
1222
- };
1269
+ : {};
1270
+ }
1223
1271
 
1224
1272
  if (mode === 'lexical') {
1225
1273
  const out = lexicalOnly({});
@@ -1295,6 +1343,7 @@ export async function recallHybrid(
1295
1343
  semanticCandidates: sr.hits.length,
1296
1344
  semanticRanked: semantic.length,
1297
1345
  ...(banditReport !== undefined ? { bandit: banditReport } : {}),
1346
+ ...deferredCommit(),
1298
1347
  };
1299
1348
  }
1300
1349