@dzhechkov/harness-core 0.3.84 → 0.3.88
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/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/setup.js +1 -1
- package/dist/setup.js.map +1 -1
- package/dist/skill-drift.d.ts +38 -11
- package/dist/skill-drift.d.ts.map +1 -1
- package/dist/skill-drift.js +101 -36
- package/dist/skill-drift.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 +3 -3
- package/src/agentdb-index.ts +132 -23
- package/src/brain.ts +200 -8
- package/src/embedding-config.ts +117 -0
- package/src/index.ts +7 -1
- package/src/setup.ts +1 -1
- package/src/skill-drift.ts +131 -40
- package/src/vector-tier.ts +78 -16
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,
|
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/skill-drift.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
24
|
-
import { join, relative, dirname, basename } from 'node:path';
|
|
24
|
+
import { join, relative, resolve, dirname, basename } from 'node:path';
|
|
25
25
|
import { createHash } from 'node:crypto';
|
|
26
26
|
|
|
27
27
|
/** One shared skill whose copies byte-differ (`driftFiles > 0`). */
|
|
@@ -65,27 +65,46 @@ export interface SweepResult {
|
|
|
65
65
|
readonly allowlisted: readonly DriftedSkill[];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* How the canonical was resolved for a {@link syncCanonicalSkill} run.
|
|
70
|
+
* - `from` — an explicit `--from <dir>` was supplied.
|
|
71
|
+
* - `skills-meta` — no `--from`; `skills-meta/<skill>` exists and was used.
|
|
72
|
+
* - `auto` — no explicit canonical; the WRITE path auto-detected the most-complete copy
|
|
73
|
+
* (opt-in `--auto` ONLY — never a bare default).
|
|
74
|
+
* - `none` — no canonical could be resolved. `--check` still runs a canonical-free peer
|
|
75
|
+
* comparison; a bare write refuses (safe-by-default).
|
|
76
|
+
*/
|
|
77
|
+
export type CanonicalSource = 'from' | 'skills-meta' | 'auto' | 'none';
|
|
78
|
+
|
|
68
79
|
/** Options for {@link syncCanonicalSkill}. */
|
|
69
80
|
export interface SyncCanonicalOptions {
|
|
70
81
|
/** Report drift only, write NOTHING (CI mode). */
|
|
71
82
|
readonly check?: boolean;
|
|
72
83
|
/** Override the canonical source dir (default: `skills-meta/<skill>`). */
|
|
73
84
|
readonly from?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Opt-in for the WRITE path ONLY: when no `--from`/`skills-meta` canonical exists, auto-detect the
|
|
87
|
+
* most-complete copy as canonical instead of refusing. A HEURISTIC — the CLI prints a loud warning
|
|
88
|
+
* naming the pick + the exact overwrite list. Never affects the read-only `check` path.
|
|
89
|
+
*/
|
|
90
|
+
readonly auto?: boolean;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
|
-
/** Result of a canonical-wins sync (or a `check:true` dry-run). */
|
|
93
|
+
/** Result of a canonical-wins sync (or a `check:true` dry-run / canonical-free peer check). */
|
|
77
94
|
export interface SyncResult {
|
|
78
|
-
/** Resolved canonical dir (abs). */
|
|
95
|
+
/** Resolved canonical dir (abs). Empty `''` in canonical-free peer / refuse modes. */
|
|
79
96
|
readonly canonical: string;
|
|
80
|
-
/** Whether
|
|
97
|
+
/** Whether a canonical source dir was resolved. `false` ⇒ `resolvedFrom === 'none'`. */
|
|
81
98
|
readonly canonicalExists: boolean;
|
|
82
|
-
/**
|
|
99
|
+
/** How the canonical resolved: `from` | `skills-meta` | `auto` | `none`. */
|
|
100
|
+
readonly resolvedFrom: CanonicalSource;
|
|
101
|
+
/** # copies compared — non-canonical copies in resolved modes; ALL peers in canonical-free mode. */
|
|
83
102
|
readonly copies: number;
|
|
84
|
-
/** # copies overwritten (0 when `check:true
|
|
103
|
+
/** # copies overwritten (0 when `check:true`, in peer mode, or when a bare write refuses). */
|
|
85
104
|
readonly synced: number;
|
|
86
|
-
/** # copies that differ from canonical. */
|
|
105
|
+
/** # copies/files that differ (from canonical in resolved modes; between peers in canonical-free mode). */
|
|
87
106
|
readonly drifted: number;
|
|
88
|
-
/** Abs paths of copies written (empty when `check:true` — proves no writes). */
|
|
107
|
+
/** Abs paths of copies written (empty when `check:true` / peer / refuse — proves no writes). */
|
|
89
108
|
readonly wrote: readonly string[];
|
|
90
109
|
}
|
|
91
110
|
|
|
@@ -115,6 +134,69 @@ function walk(dir: string): string[] {
|
|
|
115
134
|
return out;
|
|
116
135
|
}
|
|
117
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Byte-compare a set of skill copies against EACH OTHER (canonical-free). Builds the union of
|
|
139
|
+
* relative file paths across every copy, hashes each `(copy × rel)`, and counts how many relative
|
|
140
|
+
* paths disagree (`driftFiles`) and how many `(copy × rel)` pairs are absent from a copy
|
|
141
|
+
* (`missingFiles`). `driftFiles > 0` ⇔ the copies are NOT byte-identical to one another.
|
|
142
|
+
*
|
|
143
|
+
* This is the exact per-skill comparison {@link sweepSkillDrift} performs; it is extracted verbatim
|
|
144
|
+
* so BOTH the CI sweep AND the canonical-free `sync-canonical --check` share one implementation and
|
|
145
|
+
* yield the same verdict. Behavior-preserving refactor — no numbers change.
|
|
146
|
+
*/
|
|
147
|
+
function comparePeers(copies: readonly string[]): { driftFiles: number; missingFiles: number; totalFiles: number } {
|
|
148
|
+
const relFiles = new Set<string>();
|
|
149
|
+
for (const c of copies) for (const f of walk(c)) relFiles.add(relative(c, f));
|
|
150
|
+
|
|
151
|
+
let driftFiles = 0;
|
|
152
|
+
let missingFiles = 0;
|
|
153
|
+
for (const rel of relFiles) {
|
|
154
|
+
const hashes = new Set<string>();
|
|
155
|
+
for (const c of copies) {
|
|
156
|
+
const p = join(c, rel);
|
|
157
|
+
if (existsSync(p)) hashes.add(md5(p));
|
|
158
|
+
else {
|
|
159
|
+
missingFiles++;
|
|
160
|
+
hashes.add('__MISSING__');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (hashes.size > 1) driftFiles++;
|
|
164
|
+
}
|
|
165
|
+
return { driftFiles, missingFiles, totalFiles: relFiles.size };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Deterministic auto-pick of a canonical from a set of copies: the copy with the MOST files wins,
|
|
170
|
+
* tie-broken lexicographically on sorted path (so the same drift always auto-picks the same
|
|
171
|
+
* canonical, across runs and machines). "Most complete" is a HEURISTIC, not a correctness oracle —
|
|
172
|
+
* hence it is only ever reached behind an explicit `--auto` opt-in plus a loud warning.
|
|
173
|
+
*/
|
|
174
|
+
function pickMostComplete(copies: readonly string[]): string {
|
|
175
|
+
return [...copies].sort().reduce((best, c) => (walk(c).length > walk(best).length ? c : best));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolve WHICH dir is canonical for a sync/check run, and HOW it resolved. Precedence is identical
|
|
180
|
+
* for read and write paths:
|
|
181
|
+
* 1. `opts.from` → `'from'`
|
|
182
|
+
* 2. `skills-meta/<skill>` → `'skills-meta'`
|
|
183
|
+
* 3. `opts.auto` (write) → `'auto'` (most-complete copy)
|
|
184
|
+
* 4. otherwise → `'none'` (no canonical — `--check` compares copies to each other;
|
|
185
|
+
* a bare write refuses)
|
|
186
|
+
*/
|
|
187
|
+
function resolveCanonical(
|
|
188
|
+
root: string,
|
|
189
|
+
skill: string,
|
|
190
|
+
copies: readonly string[],
|
|
191
|
+
opts: SyncCanonicalOptions,
|
|
192
|
+
): { canonical: string | null; resolvedFrom: CanonicalSource } {
|
|
193
|
+
if (opts.from !== undefined) return { canonical: resolve(opts.from), resolvedFrom: 'from' };
|
|
194
|
+
const meta = join(root, 'packages/@dzhechkov/skills-meta', skill);
|
|
195
|
+
if (existsSync(meta)) return { canonical: meta, resolvedFrom: 'skills-meta' };
|
|
196
|
+
if (opts.auto === true && copies.length >= 1) return { canonical: pickMostComplete(copies), resolvedFrom: 'auto' };
|
|
197
|
+
return { canonical: null, resolvedFrom: 'none' };
|
|
198
|
+
}
|
|
199
|
+
|
|
118
200
|
/**
|
|
119
201
|
* Every skill dir (a dir containing `SKILL.md`) under `packages/` + `.claude/skills`, excluding
|
|
120
202
|
* `node_modules` / `__pycache__`. Ported verbatim from `scripts/drift-sweep-skills.mjs`.
|
|
@@ -131,6 +213,10 @@ function findSkillDirs(root: string, scope: 'packages' | 'all' = 'all'): string[
|
|
|
131
213
|
} catch {
|
|
132
214
|
continue;
|
|
133
215
|
}
|
|
216
|
+
// Drift-guard scope = SKILL.md-bearing dirs ONLY. `templates/docs/<name>/` mirrors carry
|
|
217
|
+
// skill-shaped NAMES but are rendered/derived documentation (no SKILL.md) and are therefore
|
|
218
|
+
// intentionally excluded — they are not skill definitions, so there is nothing to keep in sync.
|
|
219
|
+
// (Verified: no `packages/**/templates/docs/**` dir carries a SKILL.md. See ADR-001, D1.)
|
|
134
220
|
if (entries.includes(SKILL_MANIFEST)) dirs.push(d);
|
|
135
221
|
for (const e of entries) {
|
|
136
222
|
if (IGNORED_ENTRIES.has(e)) continue;
|
|
@@ -173,31 +259,16 @@ export function sweepSkillDrift(root: string, opts: SweepOptions = {}): SweepRes
|
|
|
173
259
|
duplicated++;
|
|
174
260
|
const copies = [...unsorted].sort();
|
|
175
261
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
let driftFiles = 0;
|
|
181
|
-
let missingFiles = 0;
|
|
182
|
-
for (const rel of relFiles) {
|
|
183
|
-
const hashes = new Set<string>();
|
|
184
|
-
for (const c of copies) {
|
|
185
|
-
const p = join(c, rel);
|
|
186
|
-
if (existsSync(p)) hashes.add(md5(p));
|
|
187
|
-
else {
|
|
188
|
-
missingFiles++;
|
|
189
|
-
hashes.add('__MISSING__');
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (hashes.size > 1) driftFiles++;
|
|
193
|
-
}
|
|
262
|
+
// Per-skill byte-comparison of every copy against each other (extracted to `comparePeers`
|
|
263
|
+
// so the canonical-free `sync-canonical --check` reuses the EXACT same logic).
|
|
264
|
+
const { driftFiles, missingFiles, totalFiles } = comparePeers(copies);
|
|
194
265
|
|
|
195
266
|
if (driftFiles > 0) {
|
|
196
267
|
const entry: DriftedSkill = {
|
|
197
268
|
name,
|
|
198
269
|
copies: copies.length,
|
|
199
270
|
driftFiles,
|
|
200
|
-
totalFiles
|
|
271
|
+
totalFiles,
|
|
201
272
|
missingFiles,
|
|
202
273
|
locations: copies,
|
|
203
274
|
};
|
|
@@ -212,31 +283,51 @@ export function sweepSkillDrift(root: string, opts: SweepOptions = {}): SweepRes
|
|
|
212
283
|
}
|
|
213
284
|
|
|
214
285
|
/**
|
|
215
|
-
* Heal one skill: treat `
|
|
216
|
-
*
|
|
286
|
+
* Heal one skill: treat the resolved canonical (`--from` → `skills-meta/<skill>` → `--auto`
|
|
287
|
+
* most-complete copy) as authoritative and overwrite every other copy in the monorepo, proving
|
|
288
|
+
* byte-identity. Pure port of `scripts/sync-canonical-skill.mjs`, extended with a canonical-free path.
|
|
217
289
|
*
|
|
218
290
|
* `check:true` writes NOTHING (`wrote` stays empty) and only reports the drift count.
|
|
219
291
|
* Default overwrites drifting copies; a subsequent {@link sweepSkillDrift} then reports 0 drift.
|
|
220
|
-
*
|
|
221
|
-
*
|
|
292
|
+
*
|
|
293
|
+
* When NO canonical resolves (`resolvedFrom === 'none'` — no `--from`, no `skills-meta`, no `--auto`):
|
|
294
|
+
* • `check:true` → CANONICAL-FREE peer check: `drifted` = # files that differ ACROSS the copies
|
|
295
|
+
* (byte-identical copies ⇒ `drifted === 0`). Reuses {@link comparePeers} — the exact sweep logic.
|
|
296
|
+
* • write (bare) → REFUSES: returns `wrote:[]`, `synced:0`, mutates NOTHING. Safe-by-default: the
|
|
297
|
+
* tool never guesses a canonical for a write, because a wrong pick destroys the good copy. The
|
|
298
|
+
* operator must pass `--from`, run `--check`, or opt in to `--auto` (which the CLI announces).
|
|
299
|
+
*
|
|
300
|
+
* This function never throws / never `process.exit`s — the CLI owns exit codes, printing, and the
|
|
301
|
+
* loud `--auto` warning.
|
|
222
302
|
*/
|
|
223
303
|
export function syncCanonicalSkill(root: string, skill: string, opts: SyncCanonicalOptions = {}): SyncResult {
|
|
224
304
|
const check = opts.check === true;
|
|
225
|
-
const canonical = opts.from ?? join(root, 'packages/@dzhechkov/skills-meta', skill);
|
|
226
305
|
|
|
227
|
-
|
|
228
|
-
|
|
306
|
+
// The healer/checker operates on EXACTLY what the detector sees (same roots via findSkillDirs) —
|
|
307
|
+
// otherwise a copy could be silently healed but never gated, or vice-versa.
|
|
308
|
+
const allCopies = findSkillDirs(root, 'all')
|
|
309
|
+
.filter((d) => basename(d) === skill)
|
|
310
|
+
.sort();
|
|
311
|
+
|
|
312
|
+
const { canonical, resolvedFrom } = resolveCanonical(root, skill, allCopies, opts);
|
|
313
|
+
|
|
314
|
+
// No canonical resolved → asymmetric read/write handling (ADR-001 D3/D4).
|
|
315
|
+
if (canonical === null) {
|
|
316
|
+
if (check) {
|
|
317
|
+
// CANONICAL-FREE peer check: are the copies byte-identical to EACH OTHER? (<2 ⇒ vacuously so.)
|
|
318
|
+
const drifted = allCopies.length < 2 ? 0 : comparePeers(allCopies).driftFiles;
|
|
319
|
+
return { canonical: '', canonicalExists: false, resolvedFrom, copies: allCopies.length, synced: 0, drifted, wrote: [] };
|
|
320
|
+
}
|
|
321
|
+
// Bare WRITE with no resolvable canonical → REFUSE. Mutates nothing (`wrote:[]` proves it).
|
|
322
|
+
return { canonical: '', canonicalExists: false, resolvedFrom, copies: allCopies.length, synced: 0, drifted: 0, wrote: [] };
|
|
229
323
|
}
|
|
230
324
|
|
|
231
325
|
const canonFiles = walk(canonical)
|
|
232
326
|
.map((p) => relative(canonical, p))
|
|
233
327
|
.sort();
|
|
234
328
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
const copies = findSkillDirs(root, 'all')
|
|
238
|
-
.filter((d) => basename(d) === skill && relative(canonical, d) !== '')
|
|
239
|
-
.sort();
|
|
329
|
+
// Every <skill>/ dir except the canonical itself.
|
|
330
|
+
const copies = allCopies.filter((d) => relative(canonical, d) !== '');
|
|
240
331
|
|
|
241
332
|
let drifted = 0;
|
|
242
333
|
let synced = 0;
|
|
@@ -269,5 +360,5 @@ export function syncCanonicalSkill(root: string, skill: string, opts: SyncCanoni
|
|
|
269
360
|
wrote.push(copy);
|
|
270
361
|
}
|
|
271
362
|
|
|
272
|
-
return { canonical, canonicalExists: true, copies: copies.length, synced, drifted, wrote };
|
|
363
|
+
return { canonical, canonicalExists: true, resolvedFrom, copies: copies.length, synced, drifted, wrote };
|
|
273
364
|
}
|
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)}` };
|