@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.
- package/dist/agentdb-index.d.ts +16 -2
- package/dist/agentdb-index.d.ts.map +1 -1
- package/dist/agentdb-index.js +129 -22
- package/dist/agentdb-index.js.map +1 -1
- package/dist/brain.d.ts +23 -0
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +205 -9
- package/dist/brain.js.map +1 -1
- package/dist/embedding-config.d.ts +39 -0
- package/dist/embedding-config.d.ts.map +1 -0
- package/dist/embedding-config.js +93 -0
- package/dist/embedding-config.js.map +1 -0
- package/dist/feature-adr-routing.d.ts +125 -0
- package/dist/feature-adr-routing.d.ts.map +1 -0
- package/dist/feature-adr-routing.js +190 -0
- package/dist/feature-adr-routing.js.map +1 -0
- package/dist/index.d.ts +8 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/setup.js +1 -1
- package/dist/setup.js.map +1 -1
- package/dist/vector-tier.d.ts +9 -0
- package/dist/vector-tier.d.ts.map +1 -1
- package/dist/vector-tier.js +75 -16
- package/dist/vector-tier.js.map +1 -1
- package/package.json +4 -4
- package/src/agentdb-index.ts +132 -23
- package/src/brain.ts +200 -8
- package/src/embedding-config.ts +117 -0
- package/src/feature-adr-routing.ts +220 -0
- package/src/index.ts +20 -1
- package/src/setup.ts +1 -1
- package/src/vector-tier.ts +78 -16
|
@@ -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/
|
|
274
|
+
embeddingModel: 'Xenova/paraphrase-multilingual-MiniLM-L12-v2',
|
|
275
275
|
sessionHookWrites: true,
|
|
276
276
|
} : undefined,
|
|
277
277
|
},
|
package/src/vector-tier.ts
CHANGED
|
@@ -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
|
-
|
|
1143
|
-
|
|
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
|
|
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
|
-
|
|
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),
|
|
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),
|
|
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),
|
|
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)}` };
|