@dzhechkov/harness-core 0.3.86 → 0.3.90

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.
@@ -0,0 +1,220 @@
1
+ /**
2
+ * feature-adr per-stage model routing — the pure resolution core.
3
+ *
4
+ * This is the TESTABLE mirror of the routing block inlined into the
5
+ * `.claude/workflows/feature-adr.js` workflow (and its byte-identical
6
+ * skills-feature-adr template copy). The workflow is a top-level-`await`
7
+ * SCRIPT, not an importable module — the pipeline runs on import — so the
8
+ * pure helpers cannot be `import`-ed from it directly. Instead the workflow
9
+ * INLINES a byte-equivalent copy of the function bodies below, and
10
+ * `feature-adr-model-routing.test.ts` asserts the inline copy is
11
+ * string-equivalent to this module (a drift guard tying the tested code to
12
+ * the shipped code), then exercises these exports for the load-bearing
13
+ * behavioral assertions (LB-A/B/C, AC-1…AC-6).
14
+ *
15
+ * The load-bearing property: a model that WRITES code must not also SELF-QE.
16
+ * When `args.models.qe` is unset, the QE stage is auto-routed to the OTHER
17
+ * family than the resolved coder (codex-coder → Claude `opus`; Claude-coder →
18
+ * `codex:<top>:high`, or `opus` if codex is unavailable — never a block).
19
+ *
20
+ * DESIGN CONSTRAINT — the Workflow parser is STRICTER than `node --check`
21
+ * (no nested template literals, no inline `cond ? agent() : null` in arrays).
22
+ * The function BODIES here are written parser-safe (string `+` concat, explicit
23
+ * `if`/return, object-literal data tables) so the same source can be inlined
24
+ * verbatim into the workflow. Keep this module and the workflow's inline block
25
+ * in lock-step.
26
+ *
27
+ * @packageDocumentation
28
+ */
29
+
30
+ /** A resolved `agent()` opts fragment: either a Claude `{model}` or a codex spec. */
31
+ export interface StageOpts {
32
+ readonly model?: string;
33
+ readonly agentType?: string;
34
+ readonly codexModel?: string;
35
+ readonly _reasoning?: string;
36
+ }
37
+
38
+ /** The knobs `resolveStageModel` closes over — passed in from the workflow. */
39
+ export interface RoutingEnv {
40
+ /** `args.models` — the per-stage override map (may be empty). */
41
+ readonly MODELS: Record<string, string | null | undefined>;
42
+ /** `args.codexModel` default id (default `'auto'`). */
43
+ readonly CODEX_MODEL: string;
44
+ /** Resolved legacy coder knob: `'claude' | 'codex' | 'codex-fallback'`. */
45
+ readonly CODER: string;
46
+ /** Resolved legacy qeReviewer knob: `'claude' | 'codex' | 'codex-fallback'`. */
47
+ readonly QE_REVIEWER: string;
48
+ /** `args.planner` shortcut (only `'codex'` is meaningful). */
49
+ readonly PLANNER?: string;
50
+ /**
51
+ * Whether codex is available at qe-resolution time. Defaults true; the caller
52
+ * passes `false` (from a pre-flight probe / `A.codexAvailable===false`) to force
53
+ * the cross-model QE default to fall back to a Claude reviewer instead of codex.
54
+ */
55
+ readonly codexAvailable?: boolean;
56
+ /** Optional log sink (the workflow passes its `log`); defaults to a no-op. */
57
+ readonly log?: (msg: string) => void;
58
+ }
59
+
60
+ // ── Data tables (data-only extensibility — gpt-5.6-ready) ───────────────────
61
+
62
+ /** Known codex ids. Adding a new id (e.g. `'gpt-5.7'`) is a DATA-ONLY change. */
63
+ export const KNOWN_CODEX: Record<string, number> = { auto: 1, 'gpt-5.5': 1, 'gpt-5.6': 1 };
64
+
65
+ /** The Claude model names the Workflow runtime accepts as `agent()` `model`. */
66
+ export const CLAUDE_NAMES: Record<string, number> = { fable: 1, opus: 1, sonnet: 1, haiku: 1 };
67
+
68
+ /**
69
+ * The proven DEFAULT TABLE, applied only when the user opts into routing.
70
+ * `code`/`qe` are `null` SENTINELS: their defaults are DERIVED (the coder knob /
71
+ * the cross-model rule), not fixed model names.
72
+ */
73
+ export const DEFAULT_MODELS: Record<string, string | null> = {
74
+ router: 'fable',
75
+ requirements: 'sonnet',
76
+ research: 'sonnet',
77
+ adr: 'opus',
78
+ ideation: 'sonnet',
79
+ ddd: 'opus',
80
+ architecture: 'opus',
81
+ plan: 'sonnet',
82
+ code: null,
83
+ qe: null,
84
+ fleet: 'sonnet',
85
+ };
86
+
87
+ // ── Pure resolvers (byte-equivalent to the workflow's inline block) ──────────
88
+
89
+ /**
90
+ * Turn a compact model SPEC into `agent()` opts.
91
+ * - falsy → `{}` (session-inherited — the BC path)
92
+ * - `'codex[:id[:r]]'` → `{agentType:'codex:codex-rescue', codexModel, _reasoning}`
93
+ * - `'fable'|'opus'|…` → `{model}`
94
+ * - unknown → warn + `{}` (Claude) / `CODEX_MODEL` (codex id)
95
+ */
96
+ export function specToOpts(spec: string | null | undefined, env: RoutingEnv): StageOpts {
97
+ const log = env.log || function () {};
98
+ if (!spec) return {};
99
+ const parts = String(spec).split(':');
100
+ const head = parts[0] || '';
101
+ if (head === 'codex') {
102
+ let id = parts[1] || env.CODEX_MODEL;
103
+ if (id !== 'auto' && !KNOWN_CODEX[id]) {
104
+ log('models: unknown codex id ' + id + ' — using ' + env.CODEX_MODEL);
105
+ id = env.CODEX_MODEL;
106
+ }
107
+ const reasoning = parts[2] || 'high';
108
+ return { agentType: 'codex:codex-rescue', codexModel: id, _reasoning: reasoning };
109
+ }
110
+ if (CLAUDE_NAMES[head]) return { model: head };
111
+ log('models: unknown spec ' + spec + ' — session-inherited');
112
+ return {};
113
+ }
114
+
115
+ /**
116
+ * Fold the legacy `coder` knob into a spec:
117
+ * `codex`/`codex-fallback` → `'codex:' + CODEX_MODEL + ':high'`; else `'opus'`.
118
+ * NOTE: a DIRECT `MODELS.code` spec bypasses this (handled in `resolveStageModel`);
119
+ * this only maps the KNOB, which is why `coderIsCodex` (below) also inspects `MODELS.code`.
120
+ */
121
+ export function resolveCoderSpec(env: RoutingEnv): string {
122
+ if (env.CODER === 'codex' || env.CODER === 'codex-fallback') return 'codex:' + env.CODEX_MODEL + ':high';
123
+ return 'opus';
124
+ }
125
+
126
+ /**
127
+ * Whether the RESOLVED coder is the codex family — true when the coder knob is
128
+ * codex/codex-fallback OR a direct `MODELS.code` spec is a codex spec. The
129
+ * cross-model QE default derives from THIS (not the knob alone), so a direct
130
+ * `MODELS.code='codex'` still routes QE to Claude (never codex-self-QE).
131
+ */
132
+ export function coderIsCodex(env: RoutingEnv): boolean {
133
+ if (env.CODER === 'codex' || env.CODER === 'codex-fallback') return true;
134
+ const codeSpec = env.MODELS.code;
135
+ if (codeSpec && String(codeSpec).split(':')[0] === 'codex') return true;
136
+ return false;
137
+ }
138
+
139
+ /**
140
+ * The CROSS-MODEL QE default (load-bearing). Called only when `MODELS.qe` is
141
+ * unset. Resolves to the OTHER family than the coder:
142
+ * - coder is codex → `'opus'` (Claude reviews codex's code)
143
+ * - coder is Claude → codex-available ? `'codex:<top>:high'` : `'opus'` (never block)
144
+ * `<top>` = `CODEX_MODEL` when pinned (≠'auto'), else the top `KNOWN_CODEX` id.
145
+ */
146
+ export function resolveQeSpec(env: RoutingEnv): string {
147
+ if (coderIsCodex(env)) return 'opus';
148
+ const CODEX_AVAILABLE = env.codexAvailable !== false;
149
+ if (!CODEX_AVAILABLE) return 'opus';
150
+ let top = env.CODEX_MODEL;
151
+ if (top === 'auto') {
152
+ const ids = Object.keys(KNOWN_CODEX);
153
+ for (let i = 0; i < ids.length; i++) {
154
+ if (ids[i] !== 'auto') top = ids[i] || top;
155
+ }
156
+ }
157
+ return 'codex:' + top + ':high';
158
+ }
159
+
160
+ /**
161
+ * Whether the caller opted into routing at all. When FALSE (no `args.models`,
162
+ * no codex knobs), every Claude stage resolves to `{}` → byte-identical to today.
163
+ */
164
+ export function routingRequested(env: RoutingEnv): boolean {
165
+ return (
166
+ Object.keys(env.MODELS).length > 0 ||
167
+ env.PLANNER === 'codex' ||
168
+ env.CODER === 'codex' ||
169
+ env.CODER === 'codex-fallback' ||
170
+ env.QE_REVIEWER === 'codex' ||
171
+ env.QE_REVIEWER === 'codex-fallback'
172
+ );
173
+ }
174
+
175
+ /**
176
+ * Whether the Step-8 QE reviewer should be CODEX. This is the load-bearing cross-model gate — it must
177
+ * NEVER let the model that wrote the code also self-QE. Order:
178
+ * 1. an explicit `MODELS.qe` spec wins outright (user opt-in — even a codex qe against a codex coder).
179
+ * 2. else the legacy `QE_REVIEWER==='codex'` knob asks for codex, but is HONORED ONLY when the coder is
180
+ * NOT codex — a codex coder + `qeReviewer:'codex'` would be codex-self-QE (the anti-pattern this whole
181
+ * feature exists to prevent), so cross-model wins and QE stays Claude.
182
+ * 3. else fall to the cross-model DEFAULT (`resolveQeSpec`): codex iff the coder is Claude & codex is
183
+ * available & routing was opted into. No routing → false → today's Claude QE (byte-identical BC).
184
+ * NOTE: the previous inline gate re-added `QE_REVIEWER==='codex'` AFTER the safe resolver, so a codex
185
+ * coder with the legacy knob silently self-QE'd. This function is the single tested source of that truth.
186
+ */
187
+ export function qeShouldUseCodex(env: RoutingEnv): boolean {
188
+ const explicit = env.MODELS.qe;
189
+ if (explicit !== undefined && explicit !== null) {
190
+ return String(explicit).split(':')[0] === 'codex';
191
+ }
192
+ if (env.QE_REVIEWER === 'codex') return !coderIsCodex(env);
193
+ return routingRequested(env) && resolveQeSpec(env).split(':')[0] === 'codex';
194
+ }
195
+
196
+ /**
197
+ * Resolve a stage to its `agent()` opts fragment.
198
+ * 1. explicit `MODELS[stage]` wins
199
+ * 2. else if the user did NOT opt into routing → `{}` (byte-identical BC path)
200
+ * 3. else the DEFAULT TABLE fills the gap
201
+ * 4. `code`/`qe` `null` sentinels resolve via the coder / cross-model rules
202
+ */
203
+ export function resolveStageModel(stage: string, env: RoutingEnv): StageOpts {
204
+ let spec = env.MODELS[stage];
205
+ if (spec === undefined) {
206
+ if (!routingRequested(env)) return {};
207
+ spec = DEFAULT_MODELS[stage];
208
+ }
209
+ if (stage === 'code' && (spec === null || spec === undefined)) return specToOpts(resolveCoderSpec(env), env);
210
+ if (stage === 'qe' && (spec === null || spec === undefined)) return specToOpts(resolveQeSpec(env), env);
211
+ return specToOpts(spec, env);
212
+ }
213
+
214
+ /** Spread-merge a resolved opts fragment onto a call's base opts (extra wins). */
215
+ export function mergeOpts<B extends object, E extends object>(base: B, extra: E): B & E {
216
+ const out: Record<string, unknown> = {};
217
+ for (const k in base) out[k] = (base as Record<string, unknown>)[k];
218
+ for (const k in extra) out[k] = (extra as Record<string, unknown>)[k];
219
+ return out as B & E;
220
+ }
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ export {
47
47
  mergeHybridHits,
48
48
  recallHybrid,
49
49
  vectorTierStatus,
50
+ reindexVectorStore,
50
51
  harmonizeVectorStore,
51
52
  selectClusterKeeper,
52
53
  importRvfCheckpoint,
@@ -72,12 +73,15 @@ export type {
72
73
  HarmonizeOptions,
73
74
  ImportReport,
74
75
  ImportOptions,
76
+ ReindexVectorReport,
75
77
  } from './vector-tier.js';
76
78
  export { runSetup, generateHooksConfig, generateAgentdbWriter, writerVersionOf, AGENTDB_WRITER_VERSION } from './setup.js';
77
79
  export { statuslineData, readFeatureAdrState, writeFeatureAdrState, featureAdrStatePath } from './statusline.js';
78
80
  export type { StatuslineData, FeatureAdrState, WriteFeatureAdrStateInput } from './statusline.js';
79
- export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb } from './agentdb-index.js';
81
+ export { indexPatternsToAgentdb, resolveAgentdbPath, searchAgentdbPatterns, listAgentdbDzIds, resolveAgentdbEmbedder, cosineSimilarity, importVectorsToAgentdb, reindexAgentdbRows } from './agentdb-index.js';
80
82
  export type { AgentdbSearchHit, AgentdbSearchResult, AgentdbImportRow } from './agentdb-index.js';
83
+ export { DEFAULT_EMBED_MODEL, LEGACY_EMBED_MODEL, DEFAULT_EMBED_DIM, KNOWN_EMBED_DIMS, resolveEmbedModel, readEmbedManifest, writeEmbedManifest, embedManifestPath, legacyEmbedManifest } from './embedding-config.js';
84
+ export type { EmbedModelConfig, EmbedModelSource, EmbedManifest } from './embedding-config.js';
81
85
  export { putBookKnowledge, queryBookKnowledge, bookKbPath } from './book-kb.js';
82
86
  export type { BookKU, BookKUHit } from './book-kb.js';
83
87
  export {
@@ -91,6 +95,8 @@ export {
91
95
  promoteProjectToBrain,
92
96
  updateBrainSource,
93
97
  queryBrain,
98
+ searchBrainVectors,
99
+ reindexBrainVectors,
94
100
  rerankHits,
95
101
  groundPrompt,
96
102
  buildPrimer,
@@ -161,3 +167,16 @@ export type {
161
167
  } from './mcp-scan.js';
162
168
  export type { RegistryEntry, Registry } from './registry.js';
163
169
  export type { BenchmarkCheck, BenchmarkScore, BenchmarkReport, CompareResult } from './benchmark.js';
170
+ export {
171
+ specToOpts,
172
+ resolveStageModel,
173
+ resolveCoderSpec,
174
+ coderIsCodex,
175
+ resolveQeSpec,
176
+ routingRequested,
177
+ mergeOpts,
178
+ DEFAULT_MODELS,
179
+ KNOWN_CODEX,
180
+ CLAUDE_NAMES,
181
+ } from './feature-adr-routing.js';
182
+ export type { StageOpts, RoutingEnv } from './feature-adr-routing.js';
package/src/setup.ts CHANGED
@@ -271,7 +271,7 @@ function generateDzConfig(target: string, preset: string | undefined, backend: M
271
271
  mcpServer: 'agentdb',
272
272
  // Both the hook writer and the MCP server read/write THIS path (env AGENTDB_PATH).
273
273
  storePath: '.dz/agentdb.db',
274
- embeddingModel: 'Xenova/all-MiniLM-L6-v2',
274
+ embeddingModel: 'Xenova/paraphrase-multilingual-MiniLM-L12-v2',
275
275
  sessionHookWrites: true,
276
276
  } : undefined,
277
277
  },
@@ -59,7 +59,9 @@ import {
59
59
  resolveAgentdbEmbedder,
60
60
  cosineSimilarity,
61
61
  importVectorsToAgentdb,
62
+ reindexAgentdbRows,
62
63
  } from './agentdb-index.js';
64
+ import { currentEmbedManifest, guardEmbedSpace, DEFAULT_EMBED_DIM, resolveEmbedModel, type EmbedModelConfig } from './embedding-config.js';
63
65
 
64
66
  /* ------------------------------------------------------------------ */
65
67
  /* Types (04_domain_model §3.4 / §4.1) */
@@ -165,6 +167,7 @@ export interface VectorTierStatus {
165
167
  readonly kind?: VectorEngineKind | undefined;
166
168
  readonly available: boolean;
167
169
  readonly reason?: string | undefined;
170
+ readonly embeddingModel?: string | undefined;
168
171
  readonly lexicalTotal: number;
169
172
  readonly lexicalMirrorable: number;
170
173
  readonly mirrored?: number | undefined;
@@ -174,10 +177,6 @@ export interface VectorTierStatus {
174
177
  /** Wall-time bound applied to EVERY engine call, read and write legs alike (ADR R1 + NC1). */
175
178
  export const DEFAULT_VECTOR_TIMEOUT_MS = 10_000;
176
179
 
177
- /** The pinned local embedding model + dimension (agentdb's `EmbeddingService`; the RVF manifest space). */
178
- const LOCAL_EMBED_MODEL = 'Xenova/all-MiniLM-L6-v2';
179
- const LOCAL_EMBED_DIM = 384;
180
-
181
180
  /** Default cosine cutoff for near-duplicate clustering (`--threshold` / config overrides). */
182
181
  export const DEFAULT_HARMONIZE_THRESHOLD = 0.92;
183
182
 
@@ -255,6 +254,14 @@ export interface ImportOptions extends VectorServiceOptions {
255
254
  readonly embed?: ((text: string) => Promise<Float32Array>) | undefined;
256
255
  }
257
256
 
257
+ export interface ReindexVectorReport {
258
+ readonly reembedded: number;
259
+ readonly model?: string;
260
+ readonly version?: number;
261
+ readonly backupPath?: string;
262
+ readonly error?: string;
263
+ }
264
+
258
265
  /* ------------------------------------------------------------------ */
259
266
  /* Timeout wrapper (both legs — NC1/QR-1) */
260
267
  /* ------------------------------------------------------------------ */
@@ -817,6 +824,7 @@ export async function vectorTierStatus(
817
824
  opts: VectorServiceOptions = {},
818
825
  ): Promise<VectorTierStatus> {
819
826
  const mode = readVectorEngineMode(projectRoot);
827
+ const model = resolveEmbedModel(projectRoot);
820
828
  let records: MemoryRecord[];
821
829
  try {
822
830
  records = loadStoreRecords(projectRoot);
@@ -831,6 +839,7 @@ export async function vectorTierStatus(
831
839
  mode,
832
840
  available: false,
833
841
  ...(resolved.reason !== undefined ? { reason: resolved.reason } : {}),
842
+ ...(!('error' in model) ? { embeddingModel: model.model } : {}),
834
843
  lexicalTotal: records.length,
835
844
  lexicalMirrorable,
836
845
  pending,
@@ -847,6 +856,7 @@ export async function vectorTierStatus(
847
856
  kind: engine.kind,
848
857
  available: true,
849
858
  ...(listed.error !== undefined ? { reason: listed.error } : {}),
859
+ ...(!('error' in model) ? { embeddingModel: model.model } : {}),
850
860
  lexicalTotal: records.length,
851
861
  lexicalMirrorable,
852
862
  mirrored: listed.error === undefined ? listed.ids.length : undefined,
@@ -854,6 +864,31 @@ export async function vectorTierStatus(
854
864
  };
855
865
  }
856
866
 
867
+ export async function reindexVectorStore(projectRoot: string, opts: VectorServiceOptions = {}): Promise<ReindexVectorReport> {
868
+ const resolved = pickEngine(projectRoot, opts);
869
+ if (resolved.engine === undefined) return { reembedded: 0, error: resolved.reason ?? 'no vector engine available' };
870
+ if (resolved.engine.kind !== 'agentdb') {
871
+ return { reembedded: 0, error: 'dz vector reindex currently rewrites the agentdb learned-pattern mirror; switch memory.vector.engine to agentdb/auto' };
872
+ }
873
+ let records: MemoryRecord[];
874
+ try {
875
+ records = loadStoreRecords(projectRoot);
876
+ } catch {
877
+ records = [];
878
+ }
879
+ const rows = records
880
+ .map(memoryRecordVectorEntry)
881
+ .filter((r): r is VectorEntry => r !== undefined)
882
+ .map((r) => ({
883
+ taskType: r.taskType,
884
+ text: r.text,
885
+ score: r.score,
886
+ ...(r.tags !== undefined ? { tags: r.tags } : {}),
887
+ ...(r.metadata !== undefined ? { metadata: r.metadata } : {}),
888
+ }));
889
+ return reindexAgentdbRows(projectRoot, rows);
890
+ }
891
+
857
892
  /* ------------------------------------------------------------------ */
858
893
  /* Harmonize — SEMANTIC dedup of the lexical store (05 §2.1) */
859
894
  /* ------------------------------------------------------------------ */
@@ -1139,8 +1174,15 @@ export async function importRvfCheckpoint(projectRoot: string, source: string, o
1139
1174
  if (existsSync(manifestPath)) {
1140
1175
  try {
1141
1176
  const m = JSON.parse(readFileSync(manifestPath, 'utf-8')) as { model?: unknown; dim?: unknown };
1142
- if ((typeof m.model === 'string' && m.model !== LOCAL_EMBED_MODEL) || (typeof m.dim === 'number' && m.dim !== LOCAL_EMBED_DIM)) {
1143
- return fail(`manifest mismatch: checkpoint (${String(m.model)}/${String(m.dim)}) local (${LOCAL_EMBED_MODEL}/${LOCAL_EMBED_DIM}) — refusing a cross-embedding-space merge`);
1177
+ const configured = resolveEmbedModel(projectRoot);
1178
+ if ('error' in configured) return fail(configured.error);
1179
+ const manifest = {
1180
+ model: typeof m.model === 'string' ? m.model : configured.model,
1181
+ dim: typeof m.dim === 'number' ? m.dim : configured.dim,
1182
+ version: 1,
1183
+ };
1184
+ if (manifest.model !== configured.model || manifest.dim !== configured.dim) {
1185
+ return fail(`manifest mismatch: checkpoint (${String(manifest.model)}/${String(manifest.dim)}) ≠ local (${configured.model}/${configured.dim}) — run dz vector reindex; refusing a cross-embedding-space merge`);
1144
1186
  }
1145
1187
  } catch { /* unreadable manifest — tolerate; the idmap is the authority */ }
1146
1188
  }
@@ -1276,12 +1318,26 @@ function readRvfIdmap(projectRoot: string): RvfIdmap {
1276
1318
  }
1277
1319
  }
1278
1320
 
1279
- function writeRvfSidecars(projectRoot: string, idmap: RvfIdmap): void {
1321
+ function guardRvfEmbedSpace(projectRoot: string, idmap: RvfIdmap): { ok: true; configured: EmbedModelConfig; version: number } | { ok: false; error: string } {
1322
+ const configured = resolveEmbedModel(projectRoot);
1323
+ if ('error' in configured) return { ok: false, error: configured.error };
1324
+ const guard = guardEmbedSpace({
1325
+ storePath: rvfBase(projectRoot),
1326
+ configured,
1327
+ hasRows: Object.keys(idmap.slots).length > 0,
1328
+ reindexHint: 'dz vector reindex',
1329
+ });
1330
+ if (!guard.ok) return { ok: false, error: guard.error };
1331
+ return { ok: true, configured, version: guard.manifest.version };
1332
+ }
1333
+
1334
+ function writeRvfSidecars(projectRoot: string, idmap: RvfIdmap, configured: EmbedModelConfig, version: number): void {
1280
1335
  const base = rvfBase(projectRoot);
1281
1336
  mkdirSync(dirname(base), { recursive: true });
1282
1337
  writeFileSync(`${base}.idmap.json`, JSON.stringify(idmap, null, 2));
1338
+ const manifest = currentEmbedManifest(configured, version, '@ruvector/rvf');
1283
1339
  writeFileSync(`${base}.manifest.json`, JSON.stringify(
1284
- { model: 'Xenova/all-MiniLM-L6-v2', dim: 384, engine: '@ruvector/rvf', version: 1 },
1340
+ manifest,
1285
1341
  null,
1286
1342
  2,
1287
1343
  ));
@@ -1356,14 +1412,16 @@ function rvfVectorEngine(projectRoot: string): VectorEngine {
1356
1412
  return {
1357
1413
  kind: 'rvf',
1358
1414
  async upsert(entries) {
1415
+ const idmap = readRvfIdmap(projectRoot);
1416
+ const guard = guardRvfEmbedSpace(projectRoot, idmap);
1417
+ if (!guard.ok) return { indexed: 0, error: guard.error };
1359
1418
  const emb = await resolveAgentdbEmbedder(projectRoot);
1360
1419
  if ('error' in emb) return { indexed: 0, error: noEmbedder };
1361
1420
  const loaded = await loadRvfModule(projectRoot);
1362
1421
  if (!loaded.ok) return { indexed: 0, error: loaded.error };
1363
- const store = await openRvfStore(loaded.mod, rvfBase(projectRoot), 384);
1422
+ const store = await openRvfStore(loaded.mod, rvfBase(projectRoot), DEFAULT_EMBED_DIM);
1364
1423
  if ('error' in store) return { indexed: 0, error: store.error };
1365
1424
  try {
1366
- const idmap = readRvfIdmap(projectRoot);
1367
1425
  let indexed = 0;
1368
1426
  for (const e of entries) {
1369
1427
  const vec = await emb.embed(`${e.taskType}: ${e.text}`);
@@ -1372,7 +1430,7 @@ function rvfVectorEngine(projectRoot: string): VectorEngine {
1372
1430
  indexed += 1;
1373
1431
  }
1374
1432
  await store.close?.();
1375
- writeRvfSidecars(projectRoot, idmap);
1433
+ writeRvfSidecars(projectRoot, idmap, guard.configured, guard.version);
1376
1434
  return { indexed };
1377
1435
  } catch (err) {
1378
1436
  return { indexed: 0, error: `rvf upsert failed: ${err instanceof Error ? err.message : String(err)}` };
@@ -1381,12 +1439,14 @@ function rvfVectorEngine(projectRoot: string): VectorEngine {
1381
1439
  async search(query, limit) {
1382
1440
  const emb = await resolveAgentdbEmbedder(projectRoot);
1383
1441
  if ('error' in emb) return { hits: [], error: noEmbedder };
1442
+ const idmap = readRvfIdmap(projectRoot);
1443
+ const guard = guardRvfEmbedSpace(projectRoot, idmap);
1444
+ if (!guard.ok) return { hits: [], error: guard.error };
1384
1445
  const loaded = await loadRvfModule(projectRoot);
1385
1446
  if (!loaded.ok) return { hits: [], error: loaded.error };
1386
- const store = await openRvfStore(loaded.mod, rvfBase(projectRoot), 384);
1447
+ const store = await openRvfStore(loaded.mod, rvfBase(projectRoot), DEFAULT_EMBED_DIM);
1387
1448
  if ('error' in store) return { hits: [], error: store.error };
1388
1449
  try {
1389
- const idmap = readRvfIdmap(projectRoot);
1390
1450
  const raw = await store.query(await emb.embed(query), limit);
1391
1451
  await store.close?.();
1392
1452
  const hits: VectorHit[] = [];
@@ -1410,12 +1470,14 @@ function rvfVectorEngine(projectRoot: string): VectorEngine {
1410
1470
  return { ids: [...new Set(Object.values(readRvfIdmap(projectRoot).slots))] };
1411
1471
  },
1412
1472
  async importVectors(rows) {
1473
+ const idmap = readRvfIdmap(projectRoot);
1474
+ const guard = guardRvfEmbedSpace(projectRoot, idmap);
1475
+ if (!guard.ok) return { imported: 0, error: guard.error };
1413
1476
  const loaded = await loadRvfModule(projectRoot);
1414
1477
  if (!loaded.ok) return { imported: 0, error: loaded.error };
1415
- const store = await openRvfStore(loaded.mod, rvfBase(projectRoot), 384);
1478
+ const store = await openRvfStore(loaded.mod, rvfBase(projectRoot), DEFAULT_EMBED_DIM);
1416
1479
  if ('error' in store) return { imported: 0, error: store.error };
1417
1480
  try {
1418
- const idmap = readRvfIdmap(projectRoot);
1419
1481
  let imported = 0;
1420
1482
  for (const r of rows) {
1421
1483
  await store.ingest(r.dzId, r.vector); // RVF ingest is upsert-by-id (id = dzId) — no duplicates
@@ -1423,7 +1485,7 @@ function rvfVectorEngine(projectRoot: string): VectorEngine {
1423
1485
  imported += 1;
1424
1486
  }
1425
1487
  await store.close?.();
1426
- writeRvfSidecars(projectRoot, idmap);
1488
+ writeRvfSidecars(projectRoot, idmap, guard.configured, guard.version);
1427
1489
  return { imported };
1428
1490
  } catch (err) {
1429
1491
  return { imported: 0, error: `rvf import failed: ${err instanceof Error ? err.message : String(err)}` };