@duckcodeailabs/dql-agent 1.9.5 → 1.10.0

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 (61) hide show
  1. package/dist/agent-run-engine.d.ts +13 -0
  2. package/dist/agent-run-engine.d.ts.map +1 -1
  3. package/dist/agent-run-engine.js +59 -1
  4. package/dist/agent-run-engine.js.map +1 -1
  5. package/dist/answer-loop.d.ts +34 -1
  6. package/dist/answer-loop.d.ts.map +1 -1
  7. package/dist/answer-loop.js +295 -35
  8. package/dist/answer-loop.js.map +1 -1
  9. package/dist/embeddings/provider.d.ts +1 -1
  10. package/dist/embeddings/provider.d.ts.map +1 -1
  11. package/dist/embeddings/provider.js.map +1 -1
  12. package/dist/governed-relational-compiler.d.ts +138 -0
  13. package/dist/governed-relational-compiler.d.ts.map +1 -0
  14. package/dist/governed-relational-compiler.js +315 -0
  15. package/dist/governed-relational-compiler.js.map +1 -0
  16. package/dist/index.d.ts +13 -5
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +8 -4
  19. package/dist/index.js.map +1 -1
  20. package/dist/intent-controller.d.ts +5 -0
  21. package/dist/intent-controller.d.ts.map +1 -1
  22. package/dist/intent-controller.js.map +1 -1
  23. package/dist/kg/build.d.ts.map +1 -1
  24. package/dist/kg/build.js +81 -1
  25. package/dist/kg/build.js.map +1 -1
  26. package/dist/meaning-resolution.d.ts +4 -0
  27. package/dist/meaning-resolution.d.ts.map +1 -1
  28. package/dist/meaning-resolution.js +14 -4
  29. package/dist/meaning-resolution.js.map +1 -1
  30. package/dist/metadata/analysis-planner.d.ts.map +1 -1
  31. package/dist/metadata/analysis-planner.js +34 -6
  32. package/dist/metadata/analysis-planner.js.map +1 -1
  33. package/dist/metadata/catalog.d.ts +83 -2
  34. package/dist/metadata/catalog.d.ts.map +1 -1
  35. package/dist/metadata/catalog.js +381 -28
  36. package/dist/metadata/catalog.js.map +1 -1
  37. package/dist/metadata/meaning-evidence.d.ts +10 -1
  38. package/dist/metadata/meaning-evidence.d.ts.map +1 -1
  39. package/dist/metadata/meaning-evidence.js +78 -1
  40. package/dist/metadata/meaning-evidence.js.map +1 -1
  41. package/dist/plan-execution-adapter.d.ts +60 -0
  42. package/dist/plan-execution-adapter.d.ts.map +1 -0
  43. package/dist/plan-execution-adapter.js +239 -0
  44. package/dist/plan-execution-adapter.js.map +1 -0
  45. package/dist/research-governance.d.ts +56 -0
  46. package/dist/research-governance.d.ts.map +1 -0
  47. package/dist/research-governance.js +68 -0
  48. package/dist/research-governance.js.map +1 -0
  49. package/dist/research-loop.d.ts +14 -0
  50. package/dist/research-loop.d.ts.map +1 -1
  51. package/dist/research-loop.js +77 -23
  52. package/dist/research-loop.js.map +1 -1
  53. package/dist/resolved-analytical-plan.d.ts +112 -0
  54. package/dist/resolved-analytical-plan.d.ts.map +1 -0
  55. package/dist/resolved-analytical-plan.js +346 -0
  56. package/dist/resolved-analytical-plan.js.map +1 -0
  57. package/dist/router.d.ts +3 -0
  58. package/dist/router.d.ts.map +1 -1
  59. package/dist/router.js +34 -10
  60. package/dist/router.js.map +1 -1
  61. package/package.json +4 -4
@@ -13,6 +13,7 @@ import { dirname, join } from 'node:path';
13
13
  import { brotliCompressSync, brotliDecompressSync, constants as zlibConstants } from 'node:zlib';
14
14
  import { buildManifest, buildManifestKnowledgeGraph, loadDomainPackageRegistry, loadManifestKnowledgeSkills, loadProjectConfig, parseContractRef, resolveDataLexManifestPath, resolveDbtManifestPath, resolveSemanticLayerWithDiagnostics, extractColumnLineage, normalizeDqlArtifactReference, composeEffectiveTrust, } from '@duckcodeailabs/dql-core';
15
15
  import { buildKGFromManifest, buildKGFromSemanticLayer } from '../kg/build.js';
16
+ import { domainContextSearchDomains } from '../domain-context.js';
16
17
  import { buildBlockBusinessFingerprint, buildBlockSqlFingerprints } from './block-fingerprints.js';
17
18
  import { buildAnalysisQuestionPlan, certifiedApplicabilityForObject, scoreAllowedSqlRelationWithAnalysisPlan, scoreMetadataObjectWithAnalysisPlan, sortAllowedSqlContextForAnalysisPlan, } from './analysis-planner.js';
18
19
  import { extractSimpleSelectShape, sourceSqlShapeColumns } from './sql-shape.js';
@@ -20,11 +21,11 @@ import { grainMatches, requestedGrainFromPlan, } from './grain-gate.js';
20
21
  import { certifiedFitAllowsTier1, certifiedTerminationVerdict, evaluateCertifiedBlockFit, } from './block-fit.js';
21
22
  import { retrieveScopedHints } from '../hints/retrieval.js';
22
23
  import { buildFtsMatch } from '../memory/fts-query.js';
23
- import { envEmbeddingProvider } from '../embeddings/provider.js';
24
+ import { cosineSimilarity, envEmbeddingProvider, HashedTokenEmbeddingProvider, } from '../embeddings/provider.js';
24
25
  import { matchExampleParaphrase } from './example-match.js';
25
26
  import { loadSkills, selectRelevantSkills } from '../skills/loader.js';
26
27
  import { buildMeaningEvidencePackage, } from './meaning-evidence.js';
27
- export { toAgentRetrievalEvidence } from './meaning-evidence.js';
28
+ export { applyContextPackCompatibility, toAgentRetrievalEvidence } from './meaning-evidence.js';
28
29
  const require = createRequire(import.meta.url);
29
30
  let databaseCtor = null;
30
31
  function loadDatabase() {
@@ -93,26 +94,36 @@ const OBJECT_PRIORITY = {
93
94
  };
94
95
  const COLUMN_OBJECT_TYPES = new Set(['dbt_column', 'warehouse_column', 'runtime_column', 'runtime_value']);
95
96
  // v3 removes the legacy persisted plaintext runtime-value cache (SEC-003).
96
- const METADATA_INDEX_VERSION = 'metadata-index-v4-content-addressed-skills';
97
+ const METADATA_INDEX_VERSION = 'metadata-index-v5-qualified-vector-lanes';
98
+ const DEFAULT_VECTOR_PROVIDER = new HashedTokenEmbeddingProvider();
97
99
  export function defaultMetadataPath(projectRoot) {
98
100
  return join(projectRoot, '.dql', 'cache', 'metadata.sqlite');
99
101
  }
100
102
  export function metadataSnapshotPath(projectRoot, fingerprint) {
101
- return join(projectRoot, '.dql', 'cache', 'snapshots', `${fingerprint}.sqlite`);
103
+ return join(projectRoot, '.dql', 'cache', 'snapshots', `${METADATA_INDEX_VERSION}-${fingerprint}.sqlite`);
102
104
  }
103
- export function activeMetadataSnapshotPath(projectRoot) {
105
+ function readActiveMetadataSnapshotPointer(projectRoot) {
104
106
  const pointerPath = join(projectRoot, '.dql', 'cache', 'active-snapshot.json');
105
107
  if (!existsSync(pointerPath))
106
108
  return null;
107
109
  try {
108
110
  const parsed = JSON.parse(readFileSync(pointerPath, 'utf8'));
109
- const path = typeof parsed.snapshotPath === 'string' ? parsed.snapshotPath : '';
110
- return path && existsSync(path) ? path : null;
111
+ const snapshotId = typeof parsed.snapshotId === 'string' ? parsed.snapshotId : '';
112
+ const snapshotPath = typeof parsed.snapshotPath === 'string' ? parsed.snapshotPath : '';
113
+ const indexSchemaVersion = typeof parsed.indexSchemaVersion === 'string'
114
+ ? parsed.indexSchemaVersion
115
+ : undefined;
116
+ if (!snapshotId || !snapshotPath || !existsSync(snapshotPath))
117
+ return null;
118
+ return { snapshotId, snapshotPath, indexSchemaVersion };
111
119
  }
112
120
  catch {
113
121
  return null;
114
122
  }
115
123
  }
124
+ export function activeMetadataSnapshotPath(projectRoot) {
125
+ return readActiveMetadataSnapshotPointer(projectRoot)?.snapshotPath ?? null;
126
+ }
116
127
  /** Read the prepared catalog identity without rebuilding the metadata snapshot. */
117
128
  export function currentMetadataFingerprint(projectRoot) {
118
129
  if (!existsSync(defaultMetadataPath(projectRoot)))
@@ -150,6 +161,47 @@ export function openActiveKnowledgeSnapshot(projectRoot) {
150
161
  const path = activeMetadataSnapshotPath(projectRoot) ?? defaultMetadataPath(projectRoot);
151
162
  return new MetadataCatalog(path, { readOnly: path !== defaultMetadataPath(projectRoot) });
152
163
  }
164
+ const activeKnowledgeSnapshotLeases = new Map();
165
+ /**
166
+ * Acquire one exact immutable SQLite file for a request lifetime. The active
167
+ * pointer is read once; later activations cannot redirect this lease to a
168
+ * different catalog. Callers must release in `finally`.
169
+ *
170
+ * Acceptance: CTX-002, CTX-005.
171
+ */
172
+ export function acquireActiveKnowledgeSnapshot(projectRoot) {
173
+ const pointer = readActiveMetadataSnapshotPointer(projectRoot);
174
+ const path = pointer?.snapshotPath ?? defaultMetadataPath(projectRoot);
175
+ const catalog = new MetadataCatalog(path, { readOnly: Boolean(pointer) });
176
+ const catalogFingerprint = catalog.state('fingerprint') ?? 'metadata-unavailable';
177
+ if (pointer && pointer.snapshotId !== catalogFingerprint) {
178
+ catalog.close();
179
+ throw new Error(`Active metadata snapshot pointer mismatch (pointer ${pointer.snapshotId}, catalog ${catalogFingerprint}).`);
180
+ }
181
+ activeKnowledgeSnapshotLeases.set(path, (activeKnowledgeSnapshotLeases.get(path) ?? 0) + 1);
182
+ let released = false;
183
+ return Object.freeze({
184
+ snapshotId: catalogFingerprint,
185
+ path,
186
+ catalog,
187
+ release() {
188
+ if (released)
189
+ return;
190
+ released = true;
191
+ catalog.close();
192
+ const remaining = (activeKnowledgeSnapshotLeases.get(path) ?? 1) - 1;
193
+ if (remaining > 0)
194
+ activeKnowledgeSnapshotLeases.set(path, remaining);
195
+ else
196
+ activeKnowledgeSnapshotLeases.delete(path);
197
+ },
198
+ });
199
+ }
200
+ export function activeKnowledgeSnapshotLeaseCount(path) {
201
+ if (path)
202
+ return activeKnowledgeSnapshotLeases.get(path) ?? 0;
203
+ return Array.from(activeKnowledgeSnapshotLeases.values()).reduce((sum, count) => sum + count, 0);
204
+ }
153
205
  export function readIndexedDomainKnowledge(projectRoot, domainId) {
154
206
  const catalog = openActiveKnowledgeSnapshot(projectRoot);
155
207
  try {
@@ -295,6 +347,92 @@ export function upsertMetadataSnapshot(projectRoot, snapshot) {
295
347
  catalog.close();
296
348
  }
297
349
  }
350
+ /**
351
+ * Run exact, BM25/lexical, vector, and graph candidate generation as
352
+ * independent lanes over one acquired snapshot. Domain/import eligibility is
353
+ * applied in SQL or before graph admission, never as a post-ranking boost.
354
+ *
355
+ * Acceptance: CTX-005, AGT-009, AGT-010.
356
+ */
357
+ export async function retrieveMetadataSnapshotCandidates(catalog, input) {
358
+ const limit = Math.max(1, input.limit ?? 80);
359
+ const domains = domainContextSearchDomains(input.domainContext);
360
+ const isEligible = (object) => (object.objectType !== 'skill'
361
+ && (!input.objectTypes?.length || input.objectTypes.includes(object.objectType))
362
+ && (domains.length === 0 || !object.domain || domains.includes(object.domain)));
363
+ const explicitIdentities = explicitMetadataIdentities(input.question);
364
+ const exactObjects = explicitIdentities.flatMap((identity) => {
365
+ const resolution = catalog.resolveIdentity(identity);
366
+ return resolution.status === 'resolved' && resolution.object && isEligible(resolution.object)
367
+ ? [resolution.object]
368
+ : [];
369
+ });
370
+ const queries = uniqueMetadataSearchQueries(input.searchQueries?.length
371
+ ? input.searchQueries
372
+ : [input.question]);
373
+ const lexicalObjects = mergeObjects(queries.flatMap((query) => catalog.searchObjects({
374
+ query,
375
+ objectTypes: input.objectTypes,
376
+ domains: domains.length > 0 ? domains : undefined,
377
+ limit,
378
+ }))).filter(isEligible).slice(0, limit);
379
+ const vector = await catalog.searchVectorObjects({
380
+ query: input.question,
381
+ objectTypes: input.objectTypes,
382
+ domains: domains.length > 0 ? domains : undefined,
383
+ limit: Math.min(limit, 24),
384
+ provider: input.embeddingProvider,
385
+ });
386
+ const vectorObjects = vector.candidates.filter(isEligible);
387
+ const seedKeys = mergeObjects([...exactObjects, ...lexicalObjects.slice(0, 12), ...vectorObjects.slice(0, 12)])
388
+ .map((object) => object.objectKey);
389
+ const graphEdges = catalog.edgesForKeys(seedKeys, 1);
390
+ const graphObjects = catalog.getObjectsByKeys(Array.from(new Set(graphEdges.flatMap((edge) => [edge.fromKey, edge.toKey])))).filter((object) => isEligible(object) && !seedKeys.includes(object.objectKey)).slice(0, Math.min(limit, 24));
391
+ const lanes = [
392
+ retrievalLane('exact', exactObjects, (object) => explicitIdentities.includes(object.objectKey)
393
+ ? 'exact object-key reference'
394
+ : 'resolved qualified/native/alias reference'),
395
+ retrievalLane('lexical', lexicalObjects, () => 'BM25/lexical candidate from the immutable snapshot'),
396
+ {
397
+ ...retrievalLane('vector', vectorObjects, () => `independent vector candidate from ${vector.providerId}`),
398
+ provider: vector.providerId,
399
+ unavailableReason: vector.unavailableReason,
400
+ },
401
+ retrievalLane('graph', graphObjects, () => 'one-hop neighbor of an eligible exact/lexical/vector seed'),
402
+ ];
403
+ const selected = mergeObjects([...exactObjects, ...lexicalObjects, ...vectorObjects, ...graphObjects])
404
+ .slice(0, limit * 2);
405
+ return {
406
+ snapshotId: catalog.state('fingerprint') ?? 'metadata-unavailable',
407
+ selected,
408
+ lanes,
409
+ };
410
+ }
411
+ function retrievalLane(lane, objects, reason) {
412
+ return {
413
+ lane,
414
+ candidates: objects.map((object, index) => ({
415
+ objectKey: object.objectKey,
416
+ objectType: object.objectType,
417
+ name: object.name,
418
+ rank: index + 1,
419
+ score: Number((object.score ?? (1 / (index + 1))).toFixed(6)),
420
+ reason: reason(object),
421
+ })),
422
+ };
423
+ }
424
+ function explicitMetadataIdentities(question) {
425
+ const identities = [];
426
+ for (const match of question.matchAll(/@(metric|dimension|measure|entity|model|block|term)\(([^)]+)\)/gi)) {
427
+ const identity = match[2]?.trim();
428
+ if (identity)
429
+ identities.push(identity);
430
+ }
431
+ for (const match of question.matchAll(/\b(?:semantic|dql|dbt|skill):[a-z0-9_.:-]+/gi)) {
432
+ identities.push(match[0]);
433
+ }
434
+ return Array.from(new Set(identities));
435
+ }
298
436
  export async function buildLocalContextPack(projectRoot, request) {
299
437
  let prepared = false;
300
438
  if (request.preparedMetadataFingerprint) {
@@ -311,8 +449,9 @@ export async function buildLocalContextPack(projectRoot, request) {
311
449
  // Static governed knowledge is read from the immutable content-addressed
312
450
  // snapshot selected at request start. Mutable run history, runtime schema,
313
451
  // and context packs stay in the working catalog and never alter that view.
314
- const snapshotPath = activeMetadataSnapshotPath(projectRoot) ?? defaultMetadataPath(projectRoot);
315
- const catalog = openActiveKnowledgeSnapshot(projectRoot);
452
+ const snapshotLease = acquireActiveKnowledgeSnapshot(projectRoot);
453
+ const snapshotPath = snapshotLease.path;
454
+ const catalog = snapshotLease.catalog;
316
455
  const runtimeCatalog = openMetadataCatalog(projectRoot);
317
456
  try {
318
457
  const mode = request.mode ?? 'question';
@@ -400,11 +539,14 @@ export async function buildLocalContextPack(projectRoot, request) {
400
539
  // Leaving it in generic FTS results would bypass status/domain/area/exclusion
401
540
  // eligibility simply because a word in its body matched the question.
402
541
  const retrievalObjects = (rows) => scopeObjects(rows).filter((row) => row.objectType !== 'skill');
403
- const searchRows = retrievalObjects(mergeObjects(searchQueries.flatMap((query) => catalog.searchObjects({
404
- query,
542
+ const snapshotRetrieval = await retrieveMetadataSnapshotCandidates(catalog, {
543
+ question: request.question,
544
+ searchQueries,
405
545
  objectTypes: request.objectTypes,
546
+ domainContext: effectiveDomainContext,
406
547
  limit: Math.max(request.limit ?? 80, 20),
407
- }))));
548
+ });
549
+ const searchRows = retrievalObjects(snapshotRetrieval.selected);
408
550
  const schemaShapeCandidates = schemaShapeCandidateObjects(catalog, questionPlan, request, mergeObjects([...runtimeObjects, ...runtimeValueObjects]));
409
551
  const schemaShapeObjects = schemaShapeCandidates.map((candidate) => candidate.object);
410
552
  const exactCandidate = request.focusObjectKey ? catalog.getObject(request.focusObjectKey) : null;
@@ -550,6 +692,7 @@ export async function buildLocalContextPack(projectRoot, request) {
550
692
  hintConflicts: hintResult.conflicts,
551
693
  retrievalDiagnostics: {
552
694
  strategy: usedFullCatalog ? 'full_catalog' : 'sqlite_fts',
695
+ lanes: snapshotRetrieval.lanes,
553
696
  focusedModelAreaId: focusedArea?.id,
554
697
  modelAreaSource: focusedArea?.source,
555
698
  selectedObjects: objects.length,
@@ -588,7 +731,7 @@ export async function buildLocalContextPack(projectRoot, request) {
588
731
  return { ...payload, id };
589
732
  }
590
733
  finally {
591
- catalog.close();
734
+ snapshotLease.release();
592
735
  runtimeCatalog.close();
593
736
  }
594
737
  }
@@ -611,7 +754,10 @@ function buildKnowledgeLens(catalog, context, selectedSkills) {
611
754
  modelAreaId: context?.modelAreaId,
612
755
  purpose: context?.purpose,
613
756
  skillRefs,
614
- snapshotId: context?.snapshotId ?? catalog.state('fingerprint') ?? 'metadata-unavailable',
757
+ // The immutable search snapshot is authoritative. A caller-provided
758
+ // envelope may have been resolved against an earlier manifest signature;
759
+ // it cannot relabel the catalog that actually supplied evidence.
760
+ snapshotId: catalog.state('fingerprint') ?? 'metadata-unavailable',
615
761
  capsuleFingerprint: stringValue(capsule?.payload?.fingerprint)
616
762
  ?? stringValue(capsule?.payload?.sourceFingerprint),
617
763
  skillFingerprints: Object.keys(skillFingerprints).length > 0 ? skillFingerprints : undefined,
@@ -1036,6 +1182,15 @@ export class MetadataCatalog {
1036
1182
  tokenize = 'porter unicode61'
1037
1183
  );
1038
1184
 
1185
+ CREATE TABLE IF NOT EXISTS metadata_vector_index (
1186
+ object_key TEXT PRIMARY KEY,
1187
+ provider_id TEXT NOT NULL,
1188
+ dimensions INTEGER NOT NULL,
1189
+ vector BLOB NOT NULL,
1190
+ text_hash TEXT NOT NULL
1191
+ );
1192
+ CREATE INDEX IF NOT EXISTS idx_metadata_vector_provider ON metadata_vector_index(provider_id);
1193
+
1039
1194
  CREATE TABLE IF NOT EXISTS metadata_edges (
1040
1195
  edge_type TEXT NOT NULL,
1041
1196
  from_key TEXT NOT NULL,
@@ -1192,6 +1347,10 @@ export class MetadataCatalog {
1192
1347
  const insertFts = this.db.prepare(`
1193
1348
  INSERT INTO metadata_fts (object_key, name, full_name, description, domain, owner, payload)
1194
1349
  VALUES (?, ?, ?, ?, ?, ?, ?)
1350
+ `);
1351
+ const insertVector = this.db.prepare(`
1352
+ INSERT INTO metadata_vector_index (object_key, provider_id, dimensions, vector, text_hash)
1353
+ VALUES (?, ?, ?, ?, ?)
1195
1354
  `);
1196
1355
  const insertEdge = this.db.prepare(`
1197
1356
  INSERT OR IGNORE INTO metadata_edges (
@@ -1226,6 +1385,7 @@ export class MetadataCatalog {
1226
1385
  const txn = this.db.transaction(() => {
1227
1386
  this.db.prepare('DELETE FROM metadata_edges').run();
1228
1387
  this.db.prepare('DELETE FROM metadata_fts').run();
1388
+ this.db.prepare('DELETE FROM metadata_vector_index').run();
1229
1389
  this.db.prepare('DELETE FROM metadata_objects').run();
1230
1390
  this.db.prepare('DELETE FROM metadata_diagnostics').run();
1231
1391
  this.db.prepare('DELETE FROM metadata_source_fingerprints').run();
@@ -1235,6 +1395,11 @@ export class MetadataCatalog {
1235
1395
  const payload = object.payload ?? {};
1236
1396
  insertObject.run(object.objectKey, object.objectType, object.name, object.fullName ?? null, object.domain ?? null, object.owner ?? null, object.status ?? null, object.description ?? null, object.sourcePath ?? null, object.sourceSystem ?? null, JSON.stringify(payload), object.updatedAt ?? now);
1237
1397
  insertFts.run(object.objectKey, object.name, object.fullName ?? '', object.description ?? '', object.domain ?? '', object.owner ?? '', searchableMetadataPayload(payload));
1398
+ if (isVectorIndexObject(object)) {
1399
+ const vectorText = metadataVectorText(object);
1400
+ const vector = DEFAULT_VECTOR_PROVIDER.embedOne(vectorText);
1401
+ insertVector.run(object.objectKey, DEFAULT_VECTOR_PROVIDER.id, vector.length, encodeFloat32Vector(vector), sha256(vectorText));
1402
+ }
1238
1403
  }
1239
1404
  for (const edge of snapshot.edges) {
1240
1405
  insertEdge.run(edge.edgeType, edge.fromKey, edge.toKey, edge.confidence ?? 1, JSON.stringify(edge.payload ?? {}), now);
@@ -1262,6 +1427,8 @@ export class MetadataCatalog {
1262
1427
  setState.run('compile_conflicts_json', JSON.stringify(snapshot.compileConflicts ?? []));
1263
1428
  setState.run('manifest_generated_at', snapshot.manifest.generatedAt);
1264
1429
  setState.run('index_version', METADATA_INDEX_VERSION);
1430
+ setState.run('vector_provider', DEFAULT_VECTOR_PROVIDER.id);
1431
+ setState.run('vector_dimensions', String(DEFAULT_VECTOR_PROVIDER.dimensions));
1265
1432
  });
1266
1433
  txn();
1267
1434
  }
@@ -1303,6 +1470,9 @@ export class MetadataCatalog {
1303
1470
  const insertFts = this.db.prepare(`
1304
1471
  INSERT INTO metadata_fts (object_key, name, full_name, description, domain, owner, payload)
1305
1472
  VALUES (?, ?, ?, ?, ?, ?, ?)`);
1473
+ const insertVector = this.db.prepare(`
1474
+ INSERT INTO metadata_vector_index (object_key, provider_id, dimensions, vector, text_hash)
1475
+ VALUES (?, ?, ?, ?, ?)`);
1306
1476
  const insertEdge = this.db.prepare(`
1307
1477
  INSERT OR IGNORE INTO metadata_edges (edge_type, from_key, to_key, confidence, payload_json, created_at)
1308
1478
  VALUES (?, ?, ?, ?, ?, ?)`);
@@ -1324,15 +1494,18 @@ export class MetadataCatalog {
1324
1494
  INSERT OR REPLACE INTO skill_bodies (body_hash, encoding, compressed_body, original_length)
1325
1495
  VALUES (?, 'br', ?, ?)`);
1326
1496
  const deleteFtsForSource = this.db.prepare('DELETE FROM metadata_fts WHERE object_key IN (SELECT object_key FROM metadata_objects WHERE COALESCE(source_path, source_system) = ?)');
1497
+ const deleteVectorsForSource = this.db.prepare('DELETE FROM metadata_vector_index WHERE object_key IN (SELECT object_key FROM metadata_objects WHERE COALESCE(source_path, source_system) = ?)');
1327
1498
  const deleteObjectsForSource = this.db.prepare('DELETE FROM metadata_objects WHERE COALESCE(source_path, source_system) = ?');
1328
1499
  const txn = this.db.transaction(() => {
1329
1500
  // Drop changed + removed sources' rows, and ALL sourceless rows (never
1330
1501
  // fingerprinted, so always refreshed).
1331
1502
  for (const source of [...changedSources, ...removedSources]) {
1332
1503
  deleteFtsForSource.run(source);
1504
+ deleteVectorsForSource.run(source);
1333
1505
  deleteObjectsForSource.run(source);
1334
1506
  }
1335
1507
  this.db.prepare('DELETE FROM metadata_fts WHERE object_key IN (SELECT object_key FROM metadata_objects WHERE source_path IS NULL AND source_system IS NULL)').run();
1508
+ this.db.prepare('DELETE FROM metadata_vector_index WHERE object_key IN (SELECT object_key FROM metadata_objects WHERE source_path IS NULL AND source_system IS NULL)').run();
1336
1509
  this.db.prepare('DELETE FROM metadata_objects WHERE source_path IS NULL AND source_system IS NULL').run();
1337
1510
  // Re-insert only objects from changed sources or sourceless; unchanged
1338
1511
  // sources' rows are left untouched.
@@ -1343,6 +1516,11 @@ export class MetadataCatalog {
1343
1516
  const payload = object.payload ?? {};
1344
1517
  insertObject.run(object.objectKey, object.objectType, object.name, object.fullName ?? null, object.domain ?? null, object.owner ?? null, object.status ?? null, object.description ?? null, object.sourcePath ?? null, object.sourceSystem ?? null, JSON.stringify(payload), object.updatedAt ?? now);
1345
1518
  insertFts.run(object.objectKey, object.name, object.fullName ?? '', object.description ?? '', object.domain ?? '', object.owner ?? '', searchableMetadataPayload(payload));
1519
+ if (isVectorIndexObject(object)) {
1520
+ const vectorText = metadataVectorText(object);
1521
+ const vector = DEFAULT_VECTOR_PROVIDER.embedOne(vectorText);
1522
+ insertVector.run(object.objectKey, DEFAULT_VECTOR_PROVIDER.id, vector.length, encodeFloat32Vector(vector), sha256(vectorText));
1523
+ }
1346
1524
  }
1347
1525
  // Cross-source tables: rebuild fully.
1348
1526
  this.db.prepare('DELETE FROM metadata_edges').run();
@@ -1378,12 +1556,14 @@ export class MetadataCatalog {
1378
1556
  setState.run('compile_conflicts_json', JSON.stringify(snapshot.compileConflicts ?? []));
1379
1557
  setState.run('manifest_generated_at', snapshot.manifest.generatedAt);
1380
1558
  setState.run('index_version', METADATA_INDEX_VERSION);
1559
+ setState.run('vector_provider', DEFAULT_VECTOR_PROVIDER.id);
1560
+ setState.run('vector_dimensions', String(DEFAULT_VECTOR_PROVIDER.dimensions));
1381
1561
  });
1382
1562
  txn();
1383
1563
  return { mode: 'incremental', changedSources: changedSources.size };
1384
1564
  }
1385
1565
  searchObjects(options) {
1386
- const { query, objectTypes, domain, limit = 40 } = options;
1566
+ const { query, objectTypes, domain, domains, limit = 40 } = options;
1387
1567
  const match = buildFtsMatch(query, { prefix: true });
1388
1568
  if (!match.or)
1389
1569
  return this.listObjects({ objectTypes, domain, limit });
@@ -1397,6 +1577,10 @@ export class MetadataCatalog {
1397
1577
  filters.push('o.domain = ?');
1398
1578
  extraParams.push(domain);
1399
1579
  }
1580
+ else if (domains?.length) {
1581
+ filters.push(`(o.domain IS NULL OR o.domain IN (${domains.map(() => '?').join(', ')}))`);
1582
+ extraParams.push(...domains);
1583
+ }
1400
1584
  const whereExtra = filters.length > 0 ? ` AND ${filters.join(' AND ')}` : '';
1401
1585
  const runMatch = (matchExpr) => this.db.prepare(`
1402
1586
  SELECT o.*,
@@ -1429,6 +1613,106 @@ export class MetadataCatalog {
1429
1613
  snippet: row.snip ?? undefined,
1430
1614
  }));
1431
1615
  }
1616
+ /** Replace the snapshot vector lane with an explicitly configured provider. */
1617
+ async rebuildVectorIndex(provider, batchSize = 96) {
1618
+ const objects = [];
1619
+ this.scanObjects({ batchSize: 500 }, (rows) => {
1620
+ objects.push(...rows.filter(isVectorIndexObject));
1621
+ });
1622
+ const encoded = [];
1623
+ let dimensions = 0;
1624
+ for (let offset = 0; offset < objects.length; offset += Math.max(1, batchSize)) {
1625
+ const batch = objects.slice(offset, offset + Math.max(1, batchSize));
1626
+ const texts = batch.map(metadataVectorText);
1627
+ const vectors = await provider.embed(texts);
1628
+ if (vectors.length !== batch.length) {
1629
+ throw new Error(`Embedding provider ${provider.id} returned ${vectors.length} vectors for ${batch.length} objects.`);
1630
+ }
1631
+ for (let index = 0; index < batch.length; index += 1) {
1632
+ const vector = vectors[index] ?? [];
1633
+ if (vector.length === 0)
1634
+ throw new Error(`Embedding provider ${provider.id} returned an empty vector.`);
1635
+ if (dimensions === 0)
1636
+ dimensions = vector.length;
1637
+ if (vector.length !== dimensions) {
1638
+ throw new Error(`Embedding provider ${provider.id} changed dimensions from ${dimensions} to ${vector.length}.`);
1639
+ }
1640
+ encoded.push({
1641
+ objectKey: batch[index].objectKey,
1642
+ dimensions,
1643
+ vector: encodeFloat32Vector(vector),
1644
+ textHash: sha256(texts[index]),
1645
+ });
1646
+ }
1647
+ }
1648
+ const insert = this.db.prepare(`
1649
+ INSERT INTO metadata_vector_index (object_key, provider_id, dimensions, vector, text_hash)
1650
+ VALUES (?, ?, ?, ?, ?)
1651
+ `);
1652
+ const setState = this.db.prepare('INSERT OR REPLACE INTO metadata_state (key, value) VALUES (?, ?)');
1653
+ this.db.transaction(() => {
1654
+ this.db.prepare('DELETE FROM metadata_vector_index').run();
1655
+ for (const item of encoded) {
1656
+ insert.run(item.objectKey, provider.id, item.dimensions, item.vector, item.textHash);
1657
+ }
1658
+ setState.run('vector_provider', provider.id);
1659
+ setState.run('vector_dimensions', String(dimensions));
1660
+ })();
1661
+ }
1662
+ /** Independent vector candidate generation over all eligible snapshot cards. */
1663
+ async searchVectorObjects(options) {
1664
+ const indexedProviderId = this.state('vector_provider') ?? DEFAULT_VECTOR_PROVIDER.id;
1665
+ const indexedDimensions = Number(this.state('vector_dimensions') ?? DEFAULT_VECTOR_PROVIDER.dimensions);
1666
+ const provider = options.provider ?? DEFAULT_VECTOR_PROVIDER;
1667
+ if (provider.id !== indexedProviderId) {
1668
+ return {
1669
+ providerId: indexedProviderId,
1670
+ dimensions: indexedDimensions,
1671
+ candidates: [],
1672
+ unavailableReason: `requested provider ${provider.id} does not match snapshot vector provider ${indexedProviderId}`,
1673
+ };
1674
+ }
1675
+ const [queryVector] = provider === DEFAULT_VECTOR_PROVIDER
1676
+ ? [DEFAULT_VECTOR_PROVIDER.embedOne(options.query)]
1677
+ : await provider.embed([options.query]);
1678
+ if (!queryVector || queryVector.length !== indexedDimensions) {
1679
+ return {
1680
+ providerId: indexedProviderId,
1681
+ dimensions: indexedDimensions,
1682
+ candidates: [],
1683
+ unavailableReason: `query vector dimensions do not match snapshot index (${queryVector?.length ?? 0} vs ${indexedDimensions})`,
1684
+ };
1685
+ }
1686
+ const filters = ['v.provider_id = ?'];
1687
+ const params = [indexedProviderId];
1688
+ if (options.objectTypes?.length) {
1689
+ filters.push(`o.object_type IN (${options.objectTypes.map(() => '?').join(', ')})`);
1690
+ params.push(...options.objectTypes);
1691
+ }
1692
+ if (options.domains?.length) {
1693
+ filters.push(`(o.domain IS NULL OR o.domain IN (${options.domains.map(() => '?').join(', ')}))`);
1694
+ params.push(...options.domains);
1695
+ }
1696
+ const rows = this.db.prepare(`
1697
+ SELECT o.*, v.vector AS vector_blob
1698
+ FROM metadata_vector_index AS v
1699
+ JOIN metadata_objects AS o ON o.object_key = v.object_key
1700
+ WHERE ${filters.join(' AND ')}
1701
+ ORDER BY o.object_key
1702
+ `).all(...params);
1703
+ const candidates = rows
1704
+ .map((row) => {
1705
+ const cosine = cosineSimilarity(queryVector, decodeFloat32Vector(row.vector_blob));
1706
+ return { ...rowToObject(row), score: Number(((cosine + 1) / 2).toFixed(6)) };
1707
+ })
1708
+ // A cosine of zero maps to 0.5 and is not evidence. Requiring positive
1709
+ // separation prevents hash collisions/unrelated cards from flooding the
1710
+ // downstream bounded context while retaining independent vector recall.
1711
+ .filter((row) => (row.score ?? 0) > 0.55)
1712
+ .sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.objectKey.localeCompare(right.objectKey))
1713
+ .slice(0, Math.max(1, options.limit ?? 40));
1714
+ return { providerId: indexedProviderId, dimensions: indexedDimensions, candidates };
1715
+ }
1432
1716
  listObjects(options = {}) {
1433
1717
  const filters = [];
1434
1718
  const params = [];
@@ -1556,13 +1840,49 @@ export class MetadataCatalog {
1556
1840
  return rows;
1557
1841
  }
1558
1842
  findObjectByIdentity(identity) {
1843
+ return this.resolveIdentity(identity).object ?? null;
1844
+ }
1845
+ /** Resolve exact/qualified/native/alias identities without first-match wins. */
1846
+ resolveIdentity(identity) {
1847
+ const normalized = identity.trim();
1848
+ if (!normalized)
1849
+ return { identity, status: 'missing', candidates: [] };
1850
+ const exact = this.db.prepare(`
1851
+ SELECT * FROM metadata_objects WHERE object_key = ?
1852
+ `).get(normalized);
1853
+ if (exact) {
1854
+ const object = rowToObject(exact);
1855
+ return { identity, status: 'resolved', matchedBy: 'object_key', object, candidates: [object] };
1856
+ }
1559
1857
  const rows = this.db.prepare(`
1560
- SELECT * FROM metadata_objects
1561
- WHERE object_key = ? OR full_name = ?
1562
- ORDER BY CASE WHEN object_key = ? THEN 0 ELSE 1 END, object_key
1563
- LIMIT 2
1564
- `).all(identity, identity, identity);
1565
- return rows.length === 1 ? rowToObject(rows[0]) : rows[0] ? rowToObject(rows[0]) : null;
1858
+ SELECT DISTINCT o.*
1859
+ FROM metadata_objects AS o
1860
+ WHERE o.full_name = ?
1861
+ OR json_extract(o.payload_json, '$.sourceNativeId') = ?
1862
+ OR EXISTS (
1863
+ SELECT 1
1864
+ FROM json_each(COALESCE(json_extract(o.payload_json, '$.aliases'), '[]')) AS alias
1865
+ WHERE alias.value = ?
1866
+ )
1867
+ ORDER BY o.object_key
1868
+ LIMIT 21
1869
+ `).all(normalized, normalized, normalized);
1870
+ const candidates = rows.map(rowToObject);
1871
+ if (candidates.length === 0)
1872
+ return { identity, status: 'missing', candidates: [] };
1873
+ if (candidates.length > 1)
1874
+ return { identity, status: 'ambiguous', candidates };
1875
+ const object = candidates[0];
1876
+ const payload = object.payload ?? {};
1877
+ const aliases = metadataStringArray(payload.aliases);
1878
+ const matchedBy = object.fullName === normalized
1879
+ ? 'qualified_id'
1880
+ : stringValue(payload.sourceNativeId) === normalized
1881
+ ? 'source_native_id'
1882
+ : aliases.includes(normalized)
1883
+ ? 'alias'
1884
+ : 'qualified_id';
1885
+ return { identity, status: 'resolved', matchedBy, object, candidates };
1566
1886
  }
1567
1887
  edgesForKeys(keys, hops = 1) {
1568
1888
  let frontier = new Set(keys.filter(Boolean));
@@ -1944,11 +2264,11 @@ function objectFromKGNode(node) {
1944
2264
  reviewCadence: node.reviewCadence,
1945
2265
  pattern: node.pattern,
1946
2266
  grain: node.grain,
1947
- entities: node.entities ?? [],
2267
+ entities: node.entities ?? node.payload?.entities ?? [],
1948
2268
  declaredOutputs: node.declaredOutputs ?? [],
1949
2269
  outputs: node.outputs ?? [],
1950
2270
  outputContract: node.outputContract ?? [],
1951
- dimensions: node.dimensions ?? [],
2271
+ dimensions: node.dimensions ?? node.payload?.dimensions ?? [],
1952
2272
  allowedFilters: node.allowedFilters ?? [],
1953
2273
  parameterPolicy: node.parameterPolicy ?? [],
1954
2274
  parameters: node.parameters ?? [],
@@ -3190,7 +3510,7 @@ async function planContextPackRoute(input) {
3190
3510
  plan: input.questionPlan,
3191
3511
  block: object,
3192
3512
  exactExampleMatch: hasExactExampleQuestion(input.request.question, object),
3193
- definitionLookup: intent === 'definition_lookup',
3513
+ definitionLookup: intent === 'definition_lookup' && objectNameInQuestion(input.request.question, object),
3194
3514
  }),
3195
3515
  applicabilityScore: applicabilityByKey.get(object.objectKey)?.score ?? 0,
3196
3516
  }))
@@ -3238,7 +3558,7 @@ async function planContextPackRoute(input) {
3238
3558
  : undefined;
3239
3559
  const exactExampleMatch = exact ? hasExactExampleQuestion(input.request.question, exact) : false;
3240
3560
  const directCertifiedBypass = Boolean(exact && (exactExampleMatch
3241
- || intent === 'definition_lookup'
3561
+ || (intent === 'definition_lookup' && objectNameInQuestion(input.request.question, exact))
3242
3562
  || objectNameInQuestion(input.request.question, exact)));
3243
3563
  const blockFitObject = exact
3244
3564
  ?? (contextApplicability ? input.objects.find((object) => object.objectKey === contextApplicability.objectKey) : undefined);
@@ -3248,7 +3568,8 @@ async function planContextPackRoute(input) {
3248
3568
  plan: input.questionPlan,
3249
3569
  block: blockFitObject,
3250
3570
  exactExampleMatch: exact ? exactExampleMatch : false,
3251
- definitionLookup: Boolean(exact && intent === 'definition_lookup'),
3571
+ definitionLookup: Boolean(exact && intent === 'definition_lookup'
3572
+ && objectNameInQuestion(input.request.question, blockFitObject)),
3252
3573
  })
3253
3574
  : undefined;
3254
3575
  const blockFit = rawBlockFit && blockFitObject
@@ -3635,8 +3956,14 @@ function isGeneratedMetadataIntent(intent) {
3635
3956
  function findExactCertifiedObject(question, intent, objects) {
3636
3957
  const candidates = objects.filter((object) => isCertifiedMetadataObject(object));
3637
3958
  if (intent === 'definition_lookup') {
3638
- return candidates.find((object) => object.objectType === 'dql_term' || object.objectType === 'business_view')
3639
- ?? candidates.find((object) => object.objectType === 'dql_block' && objectNameInQuestion(question, object));
3959
+ // A definition-shaped sentence is not itself evidence of a match. Small
3960
+ // catalogs often return every certified term; selecting the first one made
3961
+ // unrelated questions (for example weather) look governed. Prefer an
3962
+ // explicitly named/example block, then require the term/view name itself.
3963
+ return candidates.find((object) => object.objectType === 'dql_block'
3964
+ && (hasExactExampleQuestion(question, object) || objectNameInQuestion(question, object)))
3965
+ ?? candidates.find((object) => (object.objectType === 'dql_term' || object.objectType === 'business_view')
3966
+ && objectNameInQuestion(question, object));
3640
3967
  }
3641
3968
  const namedExact = candidates.find((object) => object.objectType === 'dql_block' &&
3642
3969
  objectNameInQuestion(question, object) &&
@@ -5555,6 +5882,32 @@ function searchableMetadataPayload(payload) {
5555
5882
  visit(payload, 0);
5556
5883
  return tokens.join(' ');
5557
5884
  }
5885
+ const VECTOR_EXCLUDED_OBJECT_TYPES = new Set([
5886
+ ...COLUMN_OBJECT_TYPES,
5887
+ 'skill',
5888
+ 'runtime_value',
5889
+ ]);
5890
+ function isVectorIndexObject(object) {
5891
+ return !VECTOR_EXCLUDED_OBJECT_TYPES.has(object.objectType);
5892
+ }
5893
+ function metadataVectorText(object) {
5894
+ return [
5895
+ object.name,
5896
+ object.fullName ?? '',
5897
+ object.description ?? '',
5898
+ object.domain ?? '',
5899
+ object.owner ?? '',
5900
+ searchableMetadataPayload(object.payload ?? {}),
5901
+ ].filter(Boolean).join('\n').slice(0, 16_000);
5902
+ }
5903
+ function encodeFloat32Vector(vector) {
5904
+ const values = Float32Array.from(vector);
5905
+ return Buffer.from(values.buffer, values.byteOffset, values.byteLength);
5906
+ }
5907
+ function decodeFloat32Vector(blob) {
5908
+ const bytes = blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength);
5909
+ return new Float32Array(bytes);
5910
+ }
5558
5911
  function rowToObject(row) {
5559
5912
  return {
5560
5913
  objectKey: row.object_key,