@duckcodeailabs/dql-agent 1.13.5 → 1.14.1
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/agent-run-engine.d.ts +79 -4
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +457 -39
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +13 -0
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-store.d.ts.map +1 -1
- package/dist/agent-run-store.js +42 -25
- package/dist/agent-run-store.js.map +1 -1
- package/dist/agentic/analyst-loop.d.ts +91 -0
- package/dist/agentic/analyst-loop.d.ts.map +1 -0
- package/dist/agentic/analyst-loop.js +311 -0
- package/dist/agentic/analyst-loop.js.map +1 -0
- package/dist/agentic/assumptions.d.ts +62 -0
- package/dist/agentic/assumptions.d.ts.map +1 -0
- package/dist/agentic/assumptions.js +73 -0
- package/dist/agentic/assumptions.js.map +1 -0
- package/dist/agentic/business-explanation.d.ts +56 -0
- package/dist/agentic/business-explanation.d.ts.map +1 -0
- package/dist/agentic/business-explanation.js +130 -0
- package/dist/agentic/business-explanation.js.map +1 -0
- package/dist/agentic/identifier-ledger.d.ts +74 -0
- package/dist/agentic/identifier-ledger.d.ts.map +1 -0
- package/dist/agentic/identifier-ledger.js +151 -0
- package/dist/agentic/identifier-ledger.js.map +1 -0
- package/dist/agentic/ledger-tools.d.ts +68 -0
- package/dist/agentic/ledger-tools.d.ts.map +1 -0
- package/dist/agentic/ledger-tools.js +198 -0
- package/dist/agentic/ledger-tools.js.map +1 -0
- package/dist/agentic/orchestrator-policy.d.ts +60 -0
- package/dist/agentic/orchestrator-policy.d.ts.map +1 -0
- package/dist/agentic/orchestrator-policy.js +73 -0
- package/dist/agentic/orchestrator-policy.js.map +1 -0
- package/dist/agentic/orchestrator.d.ts +54 -0
- package/dist/agentic/orchestrator.d.ts.map +1 -0
- package/dist/agentic/orchestrator.js +67 -0
- package/dist/agentic/orchestrator.js.map +1 -0
- package/dist/agentic/preview-tool.d.ts +36 -0
- package/dist/agentic/preview-tool.d.ts.map +1 -0
- package/dist/agentic/preview-tool.js +71 -0
- package/dist/agentic/preview-tool.js.map +1 -0
- package/dist/agentic/rerank.d.ts +50 -0
- package/dist/agentic/rerank.d.ts.map +1 -0
- package/dist/agentic/rerank.js +140 -0
- package/dist/agentic/rerank.js.map +1 -0
- package/dist/agentic/research/hypothesis.d.ts +110 -0
- package/dist/agentic/research/hypothesis.d.ts.map +1 -0
- package/dist/agentic/research/hypothesis.js +125 -0
- package/dist/agentic/research/hypothesis.js.map +1 -0
- package/dist/agentic/research/synthesis.d.ts +32 -0
- package/dist/agentic/research/synthesis.d.ts.map +1 -0
- package/dist/agentic/research/synthesis.js +94 -0
- package/dist/agentic/research/synthesis.js.map +1 -0
- package/dist/agentic/research-agent.d.ts +54 -0
- package/dist/agentic/research-agent.d.ts.map +1 -0
- package/dist/agentic/research-agent.js +176 -0
- package/dist/agentic/research-agent.js.map +1 -0
- package/dist/agentic/sql-authorization.d.ts +131 -0
- package/dist/agentic/sql-authorization.d.ts.map +1 -0
- package/dist/agentic/sql-authorization.js +232 -0
- package/dist/agentic/sql-authorization.js.map +1 -0
- package/dist/agentic/tool-loop.d.ts +29 -0
- package/dist/agentic/tool-loop.d.ts.map +1 -1
- package/dist/agentic/tool-loop.js +129 -52
- package/dist/agentic/tool-loop.js.map +1 -1
- package/dist/agentic/toolset.d.ts.map +1 -1
- package/dist/agentic/toolset.js +155 -0
- package/dist/agentic/toolset.js.map +1 -1
- package/dist/agentic/turn-plan.d.ts +35 -0
- package/dist/agentic/turn-plan.d.ts.map +1 -0
- package/dist/agentic/turn-plan.js +108 -0
- package/dist/agentic/turn-plan.js.map +1 -0
- package/dist/agentic/value-probe.d.ts +89 -0
- package/dist/agentic/value-probe.d.ts.map +1 -0
- package/dist/agentic/value-probe.js +304 -0
- package/dist/agentic/value-probe.js.map +1 -0
- package/dist/analytical-orchestration.d.ts +336 -0
- package/dist/analytical-orchestration.d.ts.map +1 -0
- package/dist/analytical-orchestration.js +631 -0
- package/dist/analytical-orchestration.js.map +1 -0
- package/dist/analytical-request-policy.d.ts +26 -0
- package/dist/analytical-request-policy.d.ts.map +1 -0
- package/dist/analytical-request-policy.js +82 -0
- package/dist/analytical-request-policy.js.map +1 -0
- package/dist/analytical-result-facts.d.ts +14 -1
- package/dist/analytical-result-facts.d.ts.map +1 -1
- package/dist/analytical-result-facts.js +76 -1
- package/dist/analytical-result-facts.js.map +1 -1
- package/dist/answer-loop.d.ts +61 -16
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +256 -36
- package/dist/answer-loop.js.map +1 -1
- package/dist/cascade/budgets.d.ts.map +1 -1
- package/dist/cascade/budgets.js +8 -1
- package/dist/cascade/budgets.js.map +1 -1
- package/dist/cascade/packer.d.ts +88 -0
- package/dist/cascade/packer.d.ts.map +1 -0
- package/dist/cascade/packer.js +106 -0
- package/dist/cascade/packer.js.map +1 -0
- package/dist/conversation/result-ops.d.ts +18 -3
- package/dist/conversation/result-ops.d.ts.map +1 -1
- package/dist/conversation/result-ops.js +77 -5
- package/dist/conversation/result-ops.js.map +1 -1
- package/dist/conversation/rolling-summary.d.ts.map +1 -1
- package/dist/conversation/rolling-summary.js +7 -2
- package/dist/conversation/rolling-summary.js.map +1 -1
- package/dist/conversation/session-store.d.ts +3 -0
- package/dist/conversation/session-store.d.ts.map +1 -1
- package/dist/conversation/session-store.js +8 -4
- package/dist/conversation/session-store.js.map +1 -1
- package/dist/conversation/snapshot.d.ts.map +1 -1
- package/dist/conversation/snapshot.js +13 -1
- package/dist/conversation/snapshot.js.map +1 -1
- package/dist/conversation/turn-trust.d.ts.map +1 -1
- package/dist/conversation/turn-trust.js +6 -0
- package/dist/conversation/turn-trust.js.map +1 -1
- package/dist/hints/store.d.ts.map +1 -1
- package/dist/hints/store.js +15 -5
- package/dist/hints/store.js.map +1 -1
- package/dist/index.d.ts +27 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +28 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +8 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +65 -0
- package/dist/intent-controller.js.map +1 -1
- package/dist/meaning-resolution.d.ts +7 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/block-fit.d.ts +18 -0
- package/dist/metadata/block-fit.d.ts.map +1 -1
- package/dist/metadata/block-fit.js +41 -1
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts +44 -23
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +363 -53
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +101 -8
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/metadata/metric-match.d.ts +32 -0
- package/dist/metadata/metric-match.d.ts.map +1 -1
- package/dist/metadata/metric-match.js +120 -9
- package/dist/metadata/metric-match.js.map +1 -1
- package/dist/research-loop.d.ts +33 -2
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +44 -4
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts.map +1 -1
- package/dist/resolved-analytical-plan.js +92 -5
- package/dist/resolved-analytical-plan.js.map +1 -1
- package/dist/router.d.ts +36 -16
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +604 -25
- package/dist/router.js.map +1 -1
- package/package.json +4 -4
package/dist/metadata/catalog.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyRerank } from '../agentic/rerank.js';
|
|
1
2
|
/**
|
|
2
3
|
* Project-local metadata catalog for OSS agentic analytics.
|
|
3
4
|
*
|
|
@@ -27,6 +28,7 @@ import { projectEmbeddingProvider } from '../embeddings/project-embeddings.js';
|
|
|
27
28
|
import { matchExampleParaphrase } from './example-match.js';
|
|
28
29
|
import { loadSkills, selectRelevantSkills } from '../skills/loader.js';
|
|
29
30
|
import { buildMeaningEvidencePackage, } from './meaning-evidence.js';
|
|
31
|
+
import { fuseContextCandidates, retrieveContextLanes } from '../analytical-orchestration.js';
|
|
30
32
|
import { hasGeneratedAppSourceOrigin, isExplicitlyReusableAppSource, } from '../app-source-policy.js';
|
|
31
33
|
export { applyContextPackCompatibility, toAgentRetrievalEvidence } from './meaning-evidence.js';
|
|
32
34
|
const require = createRequire(import.meta.url);
|
|
@@ -105,8 +107,20 @@ const DEFAULT_VECTOR_PROVIDER = new HashedTokenEmbeddingProvider();
|
|
|
105
107
|
export function defaultMetadataPath(projectRoot) {
|
|
106
108
|
return join(projectRoot, '.dql', 'cache', 'metadata.sqlite');
|
|
107
109
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Snapshots are immutable and addressed by content fingerprint, so
|
|
112
|
+
* `exportSnapshot` refuses to overwrite an existing file. The embedded VECTORS
|
|
113
|
+
* are part of what a snapshot contains but are NOT part of the content
|
|
114
|
+
* fingerprint, so two different encodings of identical content would otherwise
|
|
115
|
+
* collide on one path — and switching `ai.embeddings` would silently keep
|
|
116
|
+
* serving the hashed vectors forever. Qualify the filename with the provider so
|
|
117
|
+
* they are the distinct artifacts they actually are.
|
|
118
|
+
*/
|
|
119
|
+
export function metadataSnapshotPath(projectRoot, fingerprint, vectorProviderId) {
|
|
120
|
+
const suffix = vectorProviderId && !vectorProviderId.startsWith('hashed-token')
|
|
121
|
+
? `-${vectorProviderId.replace(/[^a-z0-9]+/gi, '_').slice(0, 48)}`
|
|
122
|
+
: '';
|
|
123
|
+
return join(projectRoot, '.dql', 'cache', 'snapshots', `${METADATA_INDEX_VERSION}-${fingerprint}${suffix}.sqlite`);
|
|
110
124
|
}
|
|
111
125
|
function readActiveMetadataSnapshotPointer(projectRoot) {
|
|
112
126
|
const pointerPath = join(projectRoot, '.dql', 'cache', 'active-snapshot.json');
|
|
@@ -143,7 +157,7 @@ export function currentMetadataFingerprint(projectRoot) {
|
|
|
143
157
|
}
|
|
144
158
|
}
|
|
145
159
|
function activateMetadataSnapshot(projectRoot, fingerprint, catalog) {
|
|
146
|
-
const snapshotPath = metadataSnapshotPath(projectRoot, fingerprint);
|
|
160
|
+
const snapshotPath = metadataSnapshotPath(projectRoot, fingerprint, catalog.state('vector_provider') ?? undefined);
|
|
147
161
|
catalog.exportSnapshot(snapshotPath);
|
|
148
162
|
const cacheDir = join(projectRoot, '.dql', 'cache');
|
|
149
163
|
const pointerPath = join(cacheDir, 'active-snapshot.json');
|
|
@@ -314,15 +328,22 @@ export async function awaitVectorIndexUpgrade(projectRoot) {
|
|
|
314
328
|
* runs or if it fails. Best-effort by design: an unreachable Ollama must not
|
|
315
329
|
* break indexing or answering.
|
|
316
330
|
*/
|
|
317
|
-
|
|
331
|
+
/** How long a cold re-embed may hold up indexing before we ship the hashed index. */
|
|
332
|
+
const VECTOR_UPGRADE_BUDGET_MS = 8_000;
|
|
333
|
+
/**
|
|
334
|
+
* Re-embed an ALREADY-OPEN catalog. Split out from `upgradeVectorIndexForProject`
|
|
335
|
+
* so `ensureMetadataCatalogFresh` can upgrade the same catalog handle it is about
|
|
336
|
+
* to export — the vectors have to be in place BEFORE `activateMetadataSnapshot`,
|
|
337
|
+
* because retrieval reads the exported snapshot, not this working database.
|
|
338
|
+
*/
|
|
339
|
+
async function upgradeVectorIndexOnCatalog(catalog, provider) {
|
|
318
340
|
if (provider.id.startsWith('hashed-token')) {
|
|
319
341
|
return { upgraded: false, providerId: provider.id, reason: 'project uses the offline hashed embedder' };
|
|
320
342
|
}
|
|
321
|
-
|
|
343
|
+
if (catalog.state('vector_provider') === provider.id) {
|
|
344
|
+
return { upgraded: false, providerId: provider.id, reason: 'index already embedded with this provider' };
|
|
345
|
+
}
|
|
322
346
|
try {
|
|
323
|
-
if (catalog.state('vector_provider') === provider.id) {
|
|
324
|
-
return { upgraded: false, providerId: provider.id, reason: 'index already embedded with this provider' };
|
|
325
|
-
}
|
|
326
347
|
await catalog.rebuildVectorIndex(provider);
|
|
327
348
|
return { upgraded: true, providerId: provider.id };
|
|
328
349
|
}
|
|
@@ -333,6 +354,22 @@ export async function upgradeVectorIndexForProject(projectRoot, provider) {
|
|
|
333
354
|
reason: error instanceof Error ? error.message : String(error),
|
|
334
355
|
};
|
|
335
356
|
}
|
|
357
|
+
}
|
|
358
|
+
export async function upgradeVectorIndexForProject(projectRoot, provider) {
|
|
359
|
+
const catalog = openMetadataCatalog(projectRoot);
|
|
360
|
+
try {
|
|
361
|
+
const result = await upgradeVectorIndexOnCatalog(catalog, provider);
|
|
362
|
+
// Re-export so the ACTIVE snapshot carries the new vectors. Without this the
|
|
363
|
+
// upgrade only ever touched the mutable working database, which the answer
|
|
364
|
+
// path never reads — so `POST /api/settings/embeddings/reindex` reported
|
|
365
|
+
// success while retrieval kept serving the hashed index.
|
|
366
|
+
if (result.upgraded) {
|
|
367
|
+
const fingerprint = catalog.state('fingerprint');
|
|
368
|
+
if (fingerprint)
|
|
369
|
+
activateMetadataSnapshot(projectRoot, fingerprint, catalog);
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
336
373
|
finally {
|
|
337
374
|
catalog.close();
|
|
338
375
|
}
|
|
@@ -348,7 +385,30 @@ export async function ensureMetadataCatalogFresh(projectRoot, options = {}) {
|
|
|
348
385
|
try {
|
|
349
386
|
const existing = catalog.state('fingerprint');
|
|
350
387
|
const compatibleIndex = catalog.state('index_version') === METADATA_INDEX_VERSION;
|
|
388
|
+
// The snapshot write embeds with the sync hashed provider (better-sqlite3
|
|
389
|
+
// transactions cannot await), so a configured Ollama/OpenAI embedder can only
|
|
390
|
+
// be applied afterwards — and it must land BEFORE the export, since retrieval
|
|
391
|
+
// reads the exported snapshot. `searchVectorObjects` returns ZERO candidates
|
|
392
|
+
// when the requested provider id does not match the indexed one, so skipping
|
|
393
|
+
// this makes configuring a real embedder strictly worse than not configuring
|
|
394
|
+
// one: an empty vector lane instead of a weak one.
|
|
395
|
+
const vectorProvider = options.embeddingProvider ?? projectEmbeddingProvider(projectRoot);
|
|
396
|
+
const upgradeVectors = async () => {
|
|
397
|
+
const upgrade = upgradeVectorIndexOnCatalog(catalog, vectorProvider);
|
|
398
|
+
pendingVectorUpgrades.set(projectRoot, upgrade.then(() => undefined, () => undefined));
|
|
399
|
+
// Never let a cold or unreachable embedder block indexing: the hashed index
|
|
400
|
+
// is already in place, so timing out simply ships it and the next refresh
|
|
401
|
+
// retries.
|
|
402
|
+
await Promise.race([
|
|
403
|
+
upgrade,
|
|
404
|
+
new Promise((resolve) => setTimeout(resolve, VECTOR_UPGRADE_BUDGET_MS)),
|
|
405
|
+
]);
|
|
406
|
+
};
|
|
351
407
|
if (!options.force && compatibleIndex && existing === snapshot.fingerprint) {
|
|
408
|
+
// The project content is unchanged but the CONFIGURED EMBEDDER may not be.
|
|
409
|
+
// Without this, switching `ai.embeddings` never takes effect until an
|
|
410
|
+
// unrelated dbt change happens to move the fingerprint.
|
|
411
|
+
await upgradeVectors();
|
|
352
412
|
const snapshotPath = activateMetadataSnapshot(projectRoot, snapshot.fingerprint, catalog);
|
|
353
413
|
return {
|
|
354
414
|
path: defaultMetadataPath(projectRoot),
|
|
@@ -369,6 +429,7 @@ export async function ensureMetadataCatalogFresh(projectRoot, options = {}) {
|
|
|
369
429
|
else {
|
|
370
430
|
catalog.rebuildIncremental(snapshot);
|
|
371
431
|
}
|
|
432
|
+
await upgradeVectors();
|
|
372
433
|
const snapshotPath = activateMetadataSnapshot(projectRoot, snapshot.fingerprint, catalog);
|
|
373
434
|
return {
|
|
374
435
|
path: defaultMetadataPath(projectRoot),
|
|
@@ -417,57 +478,148 @@ export async function retrieveMetadataSnapshotCandidates(catalog, input) {
|
|
|
417
478
|
&& (!input.objectTypes?.length || input.objectTypes.includes(object.objectType))
|
|
418
479
|
&& (domains.length === 0 || !object.domain || domains.includes(object.domain)));
|
|
419
480
|
const explicitIdentities = explicitMetadataIdentities(input.question);
|
|
420
|
-
const exactObjects = explicitIdentities.flatMap((identity) => {
|
|
421
|
-
const resolution = catalog.resolveIdentity(identity);
|
|
422
|
-
return resolution.status === 'resolved' && resolution.object && isEligible(resolution.object)
|
|
423
|
-
? [resolution.object]
|
|
424
|
-
: [];
|
|
425
|
-
});
|
|
426
481
|
const queries = uniqueMetadataSearchQueries(input.searchQueries?.length
|
|
427
482
|
? input.searchQueries
|
|
428
483
|
: [input.question]);
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
484
|
+
// Context lanes are genuinely independent and bounded. Their fan-out is
|
|
485
|
+
// deliberately before graph expansion: graph admission depends on the
|
|
486
|
+
// successful seed lanes, while an embedding failure must not block lexical
|
|
487
|
+
// or exact retrieval (CTX-007/PERF-003).
|
|
488
|
+
let vector;
|
|
489
|
+
let exactObjects = [];
|
|
490
|
+
let lexicalObjects = [];
|
|
491
|
+
let vectorObjects = [];
|
|
492
|
+
const laneCards = await retrieveContextLanes({
|
|
493
|
+
exact: async () => {
|
|
494
|
+
exactObjects = explicitIdentities.flatMap((identity) => {
|
|
495
|
+
const resolution = catalog.resolveIdentity(identity);
|
|
496
|
+
return resolution.status === 'resolved' && resolution.object && isEligible(resolution.object)
|
|
497
|
+
? [resolution.object]
|
|
498
|
+
: [];
|
|
499
|
+
});
|
|
500
|
+
return exactObjects.map((object) => ({
|
|
501
|
+
id: object.objectKey,
|
|
502
|
+
lane: 'exact',
|
|
503
|
+
relevance: 1,
|
|
504
|
+
summary: object.name,
|
|
505
|
+
}));
|
|
506
|
+
},
|
|
507
|
+
lexical: async () => {
|
|
508
|
+
lexicalObjects = mergeObjects(queries.flatMap((query) => catalog.searchObjects({
|
|
509
|
+
query,
|
|
510
|
+
objectTypes: input.objectTypes,
|
|
511
|
+
domains: domains.length > 0 ? domains : undefined,
|
|
512
|
+
limit,
|
|
513
|
+
}))).filter(isEligible).slice(0, limit);
|
|
514
|
+
return lexicalObjects.map((object) => ({
|
|
515
|
+
id: object.objectKey,
|
|
516
|
+
lane: 'lexical',
|
|
517
|
+
relevance: object.score ?? 0,
|
|
518
|
+
summary: object.name,
|
|
519
|
+
}));
|
|
520
|
+
},
|
|
521
|
+
vector: async () => {
|
|
522
|
+
vector = await catalog.searchVectorObjects({
|
|
523
|
+
query: input.question,
|
|
524
|
+
objectTypes: input.objectTypes,
|
|
525
|
+
domains: domains.length > 0 ? domains : undefined,
|
|
526
|
+
limit: Math.min(limit, 24),
|
|
527
|
+
provider: input.embeddingProvider
|
|
528
|
+
?? (input.projectRoot ? projectEmbeddingProvider(input.projectRoot) : undefined),
|
|
529
|
+
});
|
|
530
|
+
vectorObjects = vector.candidates.filter(isEligible);
|
|
531
|
+
return vectorObjects.map((object) => ({
|
|
532
|
+
id: object.objectKey,
|
|
533
|
+
lane: 'vector',
|
|
534
|
+
relevance: object.score ?? 0,
|
|
535
|
+
summary: object.name,
|
|
536
|
+
}));
|
|
537
|
+
},
|
|
538
|
+
}, Math.min(limit * 2, 100));
|
|
449
539
|
const seedKeys = mergeObjects([...exactObjects, ...lexicalObjects.slice(0, 12), ...vectorObjects.slice(0, 12)])
|
|
450
540
|
.map((object) => object.objectKey);
|
|
451
|
-
const
|
|
452
|
-
|
|
541
|
+
const graphStartedAt = Date.now();
|
|
542
|
+
let graphError;
|
|
543
|
+
let graphEdges = [];
|
|
544
|
+
try {
|
|
545
|
+
graphEdges = catalog.edgesForKeys(seedKeys, 1);
|
|
546
|
+
}
|
|
547
|
+
catch (error) {
|
|
548
|
+
graphError = error instanceof Error ? error.message : String(error);
|
|
549
|
+
}
|
|
550
|
+
let graphObjects = [];
|
|
551
|
+
if (!graphError) {
|
|
552
|
+
try {
|
|
553
|
+
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));
|
|
554
|
+
}
|
|
555
|
+
catch (error) {
|
|
556
|
+
// Graph expansion is advisory. Preserve exact/lexical/vector candidates
|
|
557
|
+
// and expose the typed graph failure in the lane diagnostics.
|
|
558
|
+
graphError = error instanceof Error ? error.message : String(error);
|
|
559
|
+
graphObjects = [];
|
|
560
|
+
}
|
|
561
|
+
}
|
|
453
562
|
const lanes = [
|
|
454
563
|
retrievalLane('exact', exactObjects, (object) => explicitIdentities.includes(object.objectKey)
|
|
455
564
|
? 'exact object-key reference'
|
|
456
565
|
: 'resolved qualified/native/alias reference'),
|
|
457
566
|
retrievalLane('lexical', lexicalObjects, () => 'BM25/lexical candidate from the immutable snapshot'),
|
|
458
567
|
{
|
|
459
|
-
...retrievalLane('vector', vectorObjects, () => `independent vector candidate from ${vector
|
|
460
|
-
provider: vector.providerId,
|
|
461
|
-
unavailableReason: vector.unavailableReason,
|
|
568
|
+
...retrievalLane('vector', vectorObjects, () => `independent vector candidate from ${vector?.providerId ?? 'unavailable'}`),
|
|
569
|
+
...(vector?.providerId ? { provider: vector.providerId } : {}),
|
|
570
|
+
...(vector?.unavailableReason ? { unavailableReason: vector.unavailableReason } : {}),
|
|
462
571
|
},
|
|
463
572
|
retrievalLane('graph', graphObjects, () => 'one-hop neighbor of an eligible exact/lexical/vector seed'),
|
|
464
573
|
];
|
|
465
|
-
const
|
|
466
|
-
.
|
|
574
|
+
const allLaneCards = {
|
|
575
|
+
exact: exactObjects.map((object) => ({
|
|
576
|
+
id: object.objectKey,
|
|
577
|
+
lane: 'exact',
|
|
578
|
+
relevance: 1,
|
|
579
|
+
summary: object.name,
|
|
580
|
+
})),
|
|
581
|
+
lexical: lexicalObjects.map((object) => ({
|
|
582
|
+
id: object.objectKey,
|
|
583
|
+
lane: 'lexical',
|
|
584
|
+
relevance: object.score ?? 0,
|
|
585
|
+
summary: object.name,
|
|
586
|
+
})),
|
|
587
|
+
vector: vectorObjects.map((object) => ({
|
|
588
|
+
id: object.objectKey,
|
|
589
|
+
lane: 'vector',
|
|
590
|
+
relevance: object.score ?? 0,
|
|
591
|
+
summary: object.name,
|
|
592
|
+
})),
|
|
593
|
+
graph: graphObjects.map((object) => ({
|
|
594
|
+
id: object.objectKey,
|
|
595
|
+
lane: 'graph',
|
|
596
|
+
relevance: object.score ?? 0,
|
|
597
|
+
summary: object.name,
|
|
598
|
+
})),
|
|
599
|
+
};
|
|
600
|
+
const fused = fuseContextCandidates(allLaneCards, Math.min(limit * 2, 100));
|
|
601
|
+
const objectsByKey = new Map(mergeObjects([...exactObjects, ...lexicalObjects, ...vectorObjects, ...graphObjects])
|
|
602
|
+
.map((object) => [object.objectKey, object]));
|
|
603
|
+
const selected = fused.candidates
|
|
604
|
+
.map((candidate) => objectsByKey.get(candidate.id))
|
|
605
|
+
.filter((object) => Boolean(object));
|
|
467
606
|
return {
|
|
468
607
|
snapshotId: catalog.state('fingerprint') ?? 'metadata-unavailable',
|
|
469
608
|
selected,
|
|
470
609
|
lanes,
|
|
610
|
+
fusion: {
|
|
611
|
+
selectedKeys: fused.diagnostics.selectedIds,
|
|
612
|
+
truncated: fused.diagnostics.truncated,
|
|
613
|
+
lanes: {
|
|
614
|
+
...laneCards.diagnostics.lanes,
|
|
615
|
+
graph: {
|
|
616
|
+
returned: graphObjects.length,
|
|
617
|
+
durationMs: Date.now() - graphStartedAt,
|
|
618
|
+
status: graphError ? 'error' : graphObjects.length > 0 ? 'ok' : 'empty',
|
|
619
|
+
...(graphError ? { error: graphError } : {}),
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
},
|
|
471
623
|
};
|
|
472
624
|
}
|
|
473
625
|
function retrievalLane(lane, objects, reason) {
|
|
@@ -614,7 +766,17 @@ export async function buildLocalContextPack(projectRoot, request) {
|
|
|
614
766
|
const runtimeValueObjects = runtimeValueMatchObjects(runtimeCatalog.searchRuntimeValues(metadataValueSearchTerms(request.question, questionPlan, followUp), 32));
|
|
615
767
|
const selectedObjects = selectedContextObjects(request.selectedContext);
|
|
616
768
|
const followUpObjects = followUpContextObjects(followUp);
|
|
617
|
-
|
|
769
|
+
let graphError;
|
|
770
|
+
let followUpSourceObjects = [];
|
|
771
|
+
try {
|
|
772
|
+
followUpSourceObjects = catalog.getObjectsByKeys(followUpSourceObjectKeys(followUp));
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
// A stale follow-up source reference is advisory context. Keep the
|
|
776
|
+
// successful exact/lexical/vector lanes and surface a typed graph lane
|
|
777
|
+
// diagnostic instead of failing the entire context pack (CTX-007).
|
|
778
|
+
graphError = error instanceof Error ? error.message : String(error);
|
|
779
|
+
}
|
|
618
780
|
const areaObjects = filterMetadataObjectsByDomainContext(catalog.listAllObjects({ objectTypes: ['model_area'] }), request.domainContext);
|
|
619
781
|
const focusedArea = resolveFocusedModelArea(request.domainContext, request.question, areaObjects);
|
|
620
782
|
const effectiveDomainContext = focusedArea && request.domainContext
|
|
@@ -655,9 +817,20 @@ export async function buildLocalContextPack(projectRoot, request) {
|
|
|
655
817
|
? ranked.selected
|
|
656
818
|
: mergeObjects([...followUpSourceObjects, ...followUpObjects, ...ranked.selected]);
|
|
657
819
|
const focusObjectKey = request.focusObjectKey ?? selected[0]?.objectKey ?? null;
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
820
|
+
let edgeWalk = [];
|
|
821
|
+
let graphObjects = [];
|
|
822
|
+
try {
|
|
823
|
+
edgeWalk = catalog.edgesForKeys(selected.map((row) => row.objectKey), 3);
|
|
824
|
+
const edgeObjectKeys = Array.from(new Set(edgeWalk.flatMap((edge) => [edge.fromKey, edge.toKey])));
|
|
825
|
+
graphObjects = retrievalObjects(catalog.getObjectsByKeys(edgeObjectKeys));
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
// The graph is an enrichment lane. A stale/corrupt graph must not discard
|
|
829
|
+
// successful snapshot, runtime, or semantic candidates (CTX-007).
|
|
830
|
+
graphError = error instanceof Error ? error.message : String(error);
|
|
831
|
+
edgeWalk = [];
|
|
832
|
+
graphObjects = [];
|
|
833
|
+
}
|
|
661
834
|
const rankedObjects = rankMetadataObjects({
|
|
662
835
|
rows: retrievalObjects(mergeObjects([...followUpSourceObjects, ...followUpObjects, ...selected, ...graphObjects, ...schemaShapeObjects, ...runtimeObjects, ...runtimeValueObjects, ...selectedObjects])),
|
|
663
836
|
question: searchQueries.join(' '),
|
|
@@ -665,7 +838,27 @@ export async function buildLocalContextPack(projectRoot, request) {
|
|
|
665
838
|
modelAreaId: focusedArea?.id,
|
|
666
839
|
limit: request.limit ?? 120,
|
|
667
840
|
}).selected;
|
|
668
|
-
|
|
841
|
+
// Reorder by what the QUESTION needs. RRF and the additive score rank by
|
|
842
|
+
// overlap, distance, and type weight — none of which read the question — so
|
|
843
|
+
// the object that answers it can sit behind ten near-synonyms and be cut by
|
|
844
|
+
// a top-N that never considered the ask. Advisory: the reranker may only
|
|
845
|
+
// reorder ids retrieval already returned, and any failure leaves this list
|
|
846
|
+
// exactly as it was.
|
|
847
|
+
const rerankedObjects = request.rerankCandidates
|
|
848
|
+
? applyRerank(rankedObjects, (object) => object.objectKey, await request.rerankCandidates(searchQueries.join(' '), rankedObjects.slice(0, 40).map((object) => ({
|
|
849
|
+
id: object.objectKey,
|
|
850
|
+
summary: [
|
|
851
|
+
object.name,
|
|
852
|
+
object.objectType,
|
|
853
|
+
object.description?.slice(0, 160),
|
|
854
|
+
].filter(Boolean).join(' — '),
|
|
855
|
+
}))).catch(() => undefined))
|
|
856
|
+
: rankedObjects;
|
|
857
|
+
if (process.env.DQL_ORCHESTRATOR_TRACE && request.rerankCandidates) {
|
|
858
|
+
const moved = rerankedObjects.findIndex((object, position) => object.objectKey !== rankedObjects[position]?.objectKey);
|
|
859
|
+
console.warn(`[dql] rerank: ${moved < 0 ? 'no change' : `reordered from position ${moved}`} over ${Math.min(40, rankedObjects.length)} candidates`);
|
|
860
|
+
}
|
|
861
|
+
const sqlParentObjects = sqlParentObjectsForSelectedColumns(rerankedObjects, mergeObjects([...graphObjects, ...runtimeObjects]), questionPlan);
|
|
669
862
|
// Deep-research over a SMALL catalog: skip top-k pruning and hand the model the
|
|
670
863
|
// entire relation set ("send everything, let the agent decide"). Only when the
|
|
671
864
|
// whole catalog fits comfortably in context — otherwise we keep ranked selection.
|
|
@@ -689,17 +882,27 @@ export async function buildLocalContextPack(projectRoot, request) {
|
|
|
689
882
|
limit: 8,
|
|
690
883
|
}).selected;
|
|
691
884
|
const objects = fullCatalogObjects
|
|
692
|
-
? mergeObjects([...
|
|
693
|
-
: mergeObjects([...
|
|
885
|
+
? mergeObjects([...rerankedObjects, ...sqlParentObjects, ...fullCatalogObjects, ...selectedSkillObjects, ...routeObjects])
|
|
886
|
+
: mergeObjects([...rerankedObjects, ...sqlParentObjects, ...selectedSkillObjects, ...routeObjects]);
|
|
694
887
|
const objectKeys = objects.map((row) => row.objectKey);
|
|
695
888
|
const allowedObjectKeys = new Set(objectKeys);
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
889
|
+
let contextEdges = mergeMetadataEdges(edgeWalk)
|
|
890
|
+
.filter((edge) => allowedObjectKeys.has(edge.fromKey) && allowedObjectKeys.has(edge.toKey));
|
|
891
|
+
try {
|
|
892
|
+
contextEdges = mergeMetadataEdges([
|
|
893
|
+
...contextEdges,
|
|
894
|
+
...catalog.edgesForKeys(objectKeys, 2),
|
|
895
|
+
]).filter((edge) => allowedObjectKeys.has(edge.fromKey) && allowedObjectKeys.has(edge.toKey));
|
|
896
|
+
}
|
|
897
|
+
catch (error) {
|
|
898
|
+
graphError ??= error instanceof Error ? error.message : String(error);
|
|
899
|
+
}
|
|
700
900
|
const queryRuns = runtimeCatalog.queryRunsForObjectKeys(objectKeys, 20);
|
|
701
901
|
const diagnostics = catalog.diagnostics();
|
|
702
|
-
const warnings =
|
|
902
|
+
const warnings = [
|
|
903
|
+
...buildWarnings(diagnostics, objects),
|
|
904
|
+
...(graphError ? [`Graph/context enrichment lane unavailable: ${graphError}`] : []),
|
|
905
|
+
];
|
|
703
906
|
const trustLabel = deriveTrust(objects);
|
|
704
907
|
const citations = buildCitations(objects, contextEdges);
|
|
705
908
|
const evidenceSummaries = buildEvidenceSummaries(objects, contextEdges, queryRuns, diagnostics);
|
|
@@ -796,6 +999,29 @@ export async function buildLocalContextPack(projectRoot, request) {
|
|
|
796
999
|
retrievalDiagnostics: {
|
|
797
1000
|
strategy: usedFullCatalog ? 'full_catalog' : 'sqlite_fts',
|
|
798
1001
|
lanes: snapshotRetrieval.lanes,
|
|
1002
|
+
...(snapshotRetrieval.fusion ? {
|
|
1003
|
+
fusion: {
|
|
1004
|
+
...snapshotRetrieval.fusion,
|
|
1005
|
+
...(graphError ? {
|
|
1006
|
+
lanes: {
|
|
1007
|
+
...(snapshotRetrieval.fusion.lanes ?? {}),
|
|
1008
|
+
graph: {
|
|
1009
|
+
...(snapshotRetrieval.fusion.lanes?.graph ?? { returned: 0 }),
|
|
1010
|
+
status: 'error',
|
|
1011
|
+
error: graphError,
|
|
1012
|
+
},
|
|
1013
|
+
},
|
|
1014
|
+
} : {}),
|
|
1015
|
+
},
|
|
1016
|
+
} : graphError ? {
|
|
1017
|
+
fusion: {
|
|
1018
|
+
selectedKeys: selected.map((object) => object.objectKey),
|
|
1019
|
+
truncated: false,
|
|
1020
|
+
lanes: {
|
|
1021
|
+
graph: { returned: 0, status: 'error', error: graphError },
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
} : {}),
|
|
799
1025
|
focusedModelAreaId: focusedArea?.id,
|
|
800
1026
|
modelAreaSource: focusedArea?.source,
|
|
801
1027
|
selectedObjects: objects.length,
|
|
@@ -1325,6 +1551,19 @@ function addManifestKnowledgeGraph(manifest, objects, edges) {
|
|
|
1325
1551
|
const displayName = knowledgeDisplayName(item.kind, item.localId, itemPayload);
|
|
1326
1552
|
const objectKey = knowledgeMetadataKey(item.kind, displayName, item.id, item.source.system);
|
|
1327
1553
|
keyByGraphId.set(item.id, objectKey);
|
|
1554
|
+
// The registry emits an UNRESOLVED ALIAS beside the entity-qualified node
|
|
1555
|
+
// for the same semantic member: `semantic::metric.jaffle_shop.revenue`
|
|
1556
|
+
// (note the empty entity segment) beside `semantic:order_item:revenue`.
|
|
1557
|
+
// The alias carries no aggregation, grain, or prose — its description is
|
|
1558
|
+
// the raw dbt unique_id — so it can never be proven compatible, yet it
|
|
1559
|
+
// competes for bounded retrieval and clarification slots with the node that
|
|
1560
|
+
// can. 18 of 37 metrics in the commerce fixture were these.
|
|
1561
|
+
//
|
|
1562
|
+
// Skipping it is a no-op for meaning: it holds nothing the qualified node
|
|
1563
|
+
// does not. The graph-id mapping above is still recorded so edges keep
|
|
1564
|
+
// resolving. Anything carrying real payload is indexed as before.
|
|
1565
|
+
if (isUnresolvedSemanticAlias(item.kind, item.id, itemPayload))
|
|
1566
|
+
continue;
|
|
1328
1567
|
const description = stringValue(itemPayload?.description)
|
|
1329
1568
|
?? stringValue(itemPayload?.businessContext)
|
|
1330
1569
|
?? itemAliases?.find((alias) => alias !== item.localId);
|
|
@@ -1410,6 +1649,32 @@ function addMetadataEdge(edges, edgeType, fromKey, toKey, payload) {
|
|
|
1410
1649
|
if (!edges.has(key))
|
|
1411
1650
|
edges.set(key, { edgeType, fromKey, toKey, confidence: 1, payload: compactObject(payload) });
|
|
1412
1651
|
}
|
|
1652
|
+
/**
|
|
1653
|
+
* True for the registry's payload-less alias of a semantic member.
|
|
1654
|
+
*
|
|
1655
|
+
* A qualified semantic identity is `semantic:<entity>:<member>`. When the
|
|
1656
|
+
* owning entity could not be resolved the registry still emits the node with an
|
|
1657
|
+
* EMPTY entity segment — `semantic::metric.jaffle_shop.revenue` — carrying only
|
|
1658
|
+
* the source's native id. Requiring an entity segment is the same
|
|
1659
|
+
* entity-qualified identity rule the semantic catalog is built on.
|
|
1660
|
+
*
|
|
1661
|
+
* Anything holding real meaning (an aggregation, a label, an expression, or
|
|
1662
|
+
* LLM context) is NOT an alias and is kept, even unqualified.
|
|
1663
|
+
*/
|
|
1664
|
+
function isUnresolvedSemanticAlias(kind, qualifiedId, payload) {
|
|
1665
|
+
if (!['metric', 'measure', 'dimension'].includes(kind))
|
|
1666
|
+
return false;
|
|
1667
|
+
if (!/^semantic::/.test(qualifiedId))
|
|
1668
|
+
return false;
|
|
1669
|
+
const record = payload ?? {};
|
|
1670
|
+
return !record.aggregation
|
|
1671
|
+
&& !record.llmContext
|
|
1672
|
+
&& !record.label
|
|
1673
|
+
&& !record.expression
|
|
1674
|
+
&& !record.formula
|
|
1675
|
+
&& !record.dimensions
|
|
1676
|
+
&& !record.entities;
|
|
1677
|
+
}
|
|
1413
1678
|
function knowledgeMetadataKey(kind, localId, qualifiedId, sourceSystem) {
|
|
1414
1679
|
switch (kind) {
|
|
1415
1680
|
case 'block': return `dql:block:${localId}`;
|
|
@@ -6637,11 +6902,56 @@ const VECTOR_EXCLUDED_OBJECT_TYPES = new Set([
|
|
|
6637
6902
|
'skill',
|
|
6638
6903
|
'runtime_value',
|
|
6639
6904
|
]);
|
|
6905
|
+
/**
|
|
6906
|
+
* A DOCUMENTED column earns a vector, even though columns are excluded as a class.
|
|
6907
|
+
*
|
|
6908
|
+
* Excluding every column left a question phrased in column vocabulary ("which
|
|
6909
|
+
* field holds the customer's tier?") with no semantic recall path at all — the
|
|
6910
|
+
* lexical lane is the only thing that could ever find it, and that is exactly
|
|
6911
|
+
* the case where the user's words and the schema's words differ.
|
|
6912
|
+
*
|
|
6913
|
+
* The exclusion existed for volume, and that concern is real: columns outnumber
|
|
6914
|
+
* models by 10-50x and `searchVectorObjects` is an O(n) scan in JS. A DESCRIPTION
|
|
6915
|
+
* is the natural bound — it means someone documented the column on purpose, which
|
|
6916
|
+
* is both a quality signal and a small fraction of any real warehouse. An
|
|
6917
|
+
* undocumented column has nothing to embed but its name, which the lexical lane
|
|
6918
|
+
* already matches better than a vector would.
|
|
6919
|
+
*/
|
|
6920
|
+
function isDocumentedColumnObject(object) {
|
|
6921
|
+
return COLUMN_OBJECT_TYPES.has(object.objectType)
|
|
6922
|
+
&& object.objectType !== 'runtime_value'
|
|
6923
|
+
&& (object.description?.trim().length ?? 0) > 0;
|
|
6924
|
+
}
|
|
6925
|
+
/**
|
|
6926
|
+
* A skill is written vocabulary — the words a team uses for a task — which is
|
|
6927
|
+
* exactly what a vector is for. Excluding the type wholesale meant a question
|
|
6928
|
+
* phrased in the team's language could only ever be found by keyword. Bounded
|
|
6929
|
+
* the same way as columns: it must actually be documented, so an empty skill
|
|
6930
|
+
* stub does not earn an embedding.
|
|
6931
|
+
*/
|
|
6932
|
+
function isDocumentedSkillObject(object) {
|
|
6933
|
+
return object.objectType === 'skill' && (object.description?.trim().length ?? 0) > 0;
|
|
6934
|
+
}
|
|
6640
6935
|
function isVectorIndexObject(object) {
|
|
6936
|
+
if (isDocumentedColumnObject(object))
|
|
6937
|
+
return true;
|
|
6938
|
+
if (isDocumentedSkillObject(object))
|
|
6939
|
+
return true;
|
|
6641
6940
|
return !VECTOR_EXCLUDED_OBJECT_TYPES.has(object.objectType);
|
|
6642
6941
|
}
|
|
6643
6942
|
function metadataVectorText(object) {
|
|
6943
|
+
// A bare column name is a naked token: `status` on its own is equally close to
|
|
6944
|
+
// every status-ish thing in the warehouse. Lead with the parent-qualified name
|
|
6945
|
+
// and the type so the vector carries WHICH table's status this is.
|
|
6946
|
+
const columnPrefix = isDocumentedColumnObject(object)
|
|
6947
|
+
? [
|
|
6948
|
+
`${object.fullName ?? object.name} column`,
|
|
6949
|
+
stringValue((object.payload ?? {}).type) ?? '',
|
|
6950
|
+
stringValue((object.payload ?? {}).relation) ?? '',
|
|
6951
|
+
].filter(Boolean).join(' ')
|
|
6952
|
+
: '';
|
|
6644
6953
|
return [
|
|
6954
|
+
columnPrefix,
|
|
6645
6955
|
object.name,
|
|
6646
6956
|
object.fullName ?? '',
|
|
6647
6957
|
object.description ?? '',
|