@duckcodeailabs/dql-agent 1.9.5 → 1.10.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 +13 -0
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +65 -2
- package/dist/agent-run-engine.js.map +1 -1
- package/dist/analytical-compatibility.d.ts +61 -0
- package/dist/analytical-compatibility.d.ts.map +1 -0
- package/dist/analytical-compatibility.js +515 -0
- package/dist/analytical-compatibility.js.map +1 -0
- package/dist/analytical-execution-graph.d.ts +174 -0
- package/dist/analytical-execution-graph.d.ts.map +1 -0
- package/dist/analytical-execution-graph.js +599 -0
- package/dist/analytical-execution-graph.js.map +1 -0
- package/dist/analytical-failure-repair.d.ts +91 -0
- package/dist/analytical-failure-repair.d.ts.map +1 -0
- package/dist/analytical-failure-repair.js +325 -0
- package/dist/analytical-failure-repair.js.map +1 -0
- package/dist/analytical-frame.d.ts +19 -0
- package/dist/analytical-frame.d.ts.map +1 -0
- package/dist/analytical-frame.js +299 -0
- package/dist/analytical-frame.js.map +1 -0
- package/dist/analytical-period-resolution.d.ts +53 -0
- package/dist/analytical-period-resolution.d.ts.map +1 -0
- package/dist/analytical-period-resolution.js +218 -0
- package/dist/analytical-period-resolution.js.map +1 -0
- package/dist/analytical-readiness.d.ts +17 -0
- package/dist/analytical-readiness.d.ts.map +1 -0
- package/dist/analytical-readiness.js +95 -0
- package/dist/analytical-readiness.js.map +1 -0
- package/dist/analytical-result-facts.d.ts +79 -0
- package/dist/analytical-result-facts.d.ts.map +1 -0
- package/dist/analytical-result-facts.js +301 -0
- package/dist/analytical-result-facts.js.map +1 -0
- package/dist/answer-loop.d.ts +82 -2
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +1129 -38
- package/dist/answer-loop.js.map +1 -1
- package/dist/embeddings/provider.d.ts +1 -1
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/governed-relational-compiler.d.ts +189 -0
- package/dist/governed-relational-compiler.d.ts.map +1 -0
- package/dist/governed-relational-compiler.js +458 -0
- package/dist/governed-relational-compiler.js.map +1 -0
- package/dist/index.d.ts +25 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +5 -0
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/build.d.ts +2 -2
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +329 -9
- package/dist/kg/build.js.map +1 -1
- package/dist/meaning-resolution.d.ts +17 -0
- package/dist/meaning-resolution.d.ts.map +1 -1
- package/dist/meaning-resolution.js +83 -4
- package/dist/meaning-resolution.js.map +1 -1
- package/dist/metadata/analysis-planner.d.ts.map +1 -1
- package/dist/metadata/analysis-planner.js +37 -7
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/catalog.d.ts +86 -3
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +607 -36
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/meaning-evidence.d.ts +18 -2
- package/dist/metadata/meaning-evidence.d.ts.map +1 -1
- package/dist/metadata/meaning-evidence.js +182 -7
- package/dist/metadata/meaning-evidence.js.map +1 -1
- package/dist/plan-execution-adapter.d.ts +131 -0
- package/dist/plan-execution-adapter.d.ts.map +1 -0
- package/dist/plan-execution-adapter.js +447 -0
- package/dist/plan-execution-adapter.js.map +1 -0
- package/dist/research-governance.d.ts +56 -0
- package/dist/research-governance.d.ts.map +1 -0
- package/dist/research-governance.js +68 -0
- package/dist/research-governance.js.map +1 -0
- package/dist/research-loop.d.ts +14 -0
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +77 -23
- package/dist/research-loop.js.map +1 -1
- package/dist/resolved-analytical-plan.d.ts +123 -0
- package/dist/resolved-analytical-plan.d.ts.map +1 -0
- package/dist/resolved-analytical-plan.js +400 -0
- package/dist/resolved-analytical-plan.js.map +1 -0
- package/dist/router.d.ts +3 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +195 -25
- package/dist/router.js.map +1 -1
- package/dist/skills/loader.d.ts +17 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +62 -4
- package/dist/skills/loader.js.map +1 -1
- package/package.json +4 -4
package/dist/metadata/catalog.js
CHANGED
|
@@ -11,20 +11,21 @@ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync
|
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
12
|
import { dirname, join } from 'node:path';
|
|
13
13
|
import { brotliCompressSync, brotliDecompressSync, constants as zlibConstants } from 'node:zlib';
|
|
14
|
-
import { buildManifest, buildManifestKnowledgeGraph, loadDomainPackageRegistry, loadManifestKnowledgeSkills, loadProjectConfig, parseContractRef, resolveDataLexManifestPath, resolveDbtManifestPath, resolveSemanticLayerWithDiagnostics, extractColumnLineage, normalizeDqlArtifactReference, composeEffectiveTrust, } from
|
|
15
|
-
import { buildKGFromManifest, buildKGFromSemanticLayer } from
|
|
16
|
-
import {
|
|
14
|
+
import { buildManifest, buildManifestKnowledgeGraph, loadDomainPackageRegistry, loadManifestKnowledgeSkills, loadProjectConfig, parseContractRef, resolveDataLexManifestPath, resolveDbtManifestPath, resolveSemanticLayerWithDiagnostics, extractColumnLineage, normalizeDqlArtifactReference, composeEffectiveTrust, normalizeMetricCapabilityContract, } from "@duckcodeailabs/dql-core";
|
|
15
|
+
import { buildKGFromManifest, buildKGFromSemanticLayer } from "../kg/build.js";
|
|
16
|
+
import { domainContextSearchDomains, } from "../domain-context.js";
|
|
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';
|
|
19
20
|
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-
|
|
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
|
-
|
|
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
|
|
110
|
-
|
|
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
|
|
315
|
-
const
|
|
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
|
|
404
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -689,6 +835,7 @@ export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer, skil
|
|
|
689
835
|
addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges, diagnostics);
|
|
690
836
|
addBlockDependencyEdges(manifest, edges);
|
|
691
837
|
addBlockOutputLineageObjects(manifest, objects, edges);
|
|
838
|
+
addCertifiedBlockAnalyticalCapabilities(objects);
|
|
692
839
|
// Manifest v3 is the unified DQL runtime. DataLex is migration input only;
|
|
693
840
|
// retaining it here would reintroduce a second analytical source of truth.
|
|
694
841
|
if (manifest.manifestVersion !== 3) {
|
|
@@ -732,6 +879,171 @@ export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer, skil
|
|
|
732
879
|
snapshot.fingerprint = fingerprintSnapshot(snapshot);
|
|
733
880
|
return snapshot;
|
|
734
881
|
}
|
|
882
|
+
/**
|
|
883
|
+
* Upgrade only fully declared certified blocks into RFC 0005 capabilities.
|
|
884
|
+
* Local/display-name matching is intentionally insufficient: every reusable
|
|
885
|
+
* metric, grain, dimension/filter, and output role must bind exactly to the
|
|
886
|
+
* already-normalized semantic capability in this immutable snapshot.
|
|
887
|
+
*
|
|
888
|
+
* Acceptance: CONTRACT-002, AGT-018.
|
|
889
|
+
*/
|
|
890
|
+
function addCertifiedBlockAnalyticalCapabilities(objects) {
|
|
891
|
+
const semanticCapabilities = [...objects.values()].flatMap((object) => {
|
|
892
|
+
if (object.objectType !== "semantic_metric")
|
|
893
|
+
return [];
|
|
894
|
+
const capability = normalizeMetricCapabilityContract(object.payload?.analyticalCapability);
|
|
895
|
+
if (!capability)
|
|
896
|
+
return [];
|
|
897
|
+
const payload = object.payload ?? {};
|
|
898
|
+
return [
|
|
899
|
+
{
|
|
900
|
+
object,
|
|
901
|
+
capability,
|
|
902
|
+
exactRefs: new Set(uniqueNonBlank([
|
|
903
|
+
capability.metricId,
|
|
904
|
+
object.objectKey,
|
|
905
|
+
object.fullName,
|
|
906
|
+
stringValue(payload.qualifiedId),
|
|
907
|
+
stringValue(payload.sourceNativeId),
|
|
908
|
+
...metadataStringArray(payload.aliases),
|
|
909
|
+
])),
|
|
910
|
+
},
|
|
911
|
+
];
|
|
912
|
+
});
|
|
913
|
+
for (const block of objects.values()) {
|
|
914
|
+
if (block.objectType !== "dql_block" || block.status !== "certified")
|
|
915
|
+
continue;
|
|
916
|
+
const payload = block.payload ?? {};
|
|
917
|
+
const metricRefs = uniqueNonBlank([
|
|
918
|
+
...metadataStringArray(payload.metricRefs),
|
|
919
|
+
]);
|
|
920
|
+
if (metricRefs.length !== 1)
|
|
921
|
+
continue;
|
|
922
|
+
const metrics = semanticCapabilities.filter((candidate) => candidate.exactRefs.has(metricRefs[0]));
|
|
923
|
+
if (metrics.length !== 1)
|
|
924
|
+
continue;
|
|
925
|
+
const semantic = metrics[0];
|
|
926
|
+
const grain = stringValue(payload.grain);
|
|
927
|
+
if (!grain || !semantic.capability.resultGrainIds.includes(grain))
|
|
928
|
+
continue;
|
|
929
|
+
const groupedRefs = uniqueNonBlank([
|
|
930
|
+
...metadataStringArray(payload.dimensions),
|
|
931
|
+
...metadataStringArray(payload.dimensionsRef),
|
|
932
|
+
]);
|
|
933
|
+
const filterRefs = uniqueNonBlank(metadataStringArray(payload.allowedFilters));
|
|
934
|
+
const declaredRefs = new Set([...groupedRefs, ...filterRefs]);
|
|
935
|
+
const capabilityDimensionIds = new Set(semantic.capability.dimensions.map((item) => item.dimensionId));
|
|
936
|
+
const capabilityTimeIds = new Set(semantic.capability.timeDimensions.map((item) => item.dimensionId));
|
|
937
|
+
if ([...declaredRefs].some((id) => !capabilityDimensionIds.has(id) && !capabilityTimeIds.has(id)))
|
|
938
|
+
continue;
|
|
939
|
+
const dimensions = semantic.capability.dimensions.flatMap((dimension) => {
|
|
940
|
+
if (!declaredRefs.has(dimension.dimensionId))
|
|
941
|
+
return [];
|
|
942
|
+
const roles = dimension.supportedRoles.filter((role) => (role === "filter" && filterRefs.includes(dimension.dimensionId)) ||
|
|
943
|
+
(role === "group_by" &&
|
|
944
|
+
groupedRefs.includes(dimension.dimensionId)) ||
|
|
945
|
+
(role === "display" &&
|
|
946
|
+
groupedRefs.includes(dimension.dimensionId)) ||
|
|
947
|
+
(role === "rank_entity" &&
|
|
948
|
+
payload.pattern === "ranking" &&
|
|
949
|
+
groupedRefs.includes(dimension.dimensionId)));
|
|
950
|
+
return roles.length > 0
|
|
951
|
+
? [{ ...dimension, supportedRoles: roles }]
|
|
952
|
+
: [];
|
|
953
|
+
});
|
|
954
|
+
if ([...declaredRefs].some((id) => capabilityDimensionIds.has(id) &&
|
|
955
|
+
!dimensions.some((dimension) => dimension.dimensionId === id)))
|
|
956
|
+
continue;
|
|
957
|
+
const timeDimensions = semantic.capability.timeDimensions.filter((dimension) => declaredRefs.has(dimension.dimensionId));
|
|
958
|
+
const outputs = analyticalBlockOutputs(payload.outputContract);
|
|
959
|
+
if (!outputs ||
|
|
960
|
+
outputs.ids.length === 0 ||
|
|
961
|
+
!outputs.kinds.includes("metric_value"))
|
|
962
|
+
continue;
|
|
963
|
+
const operations = [];
|
|
964
|
+
if (filterRefs.length > 0)
|
|
965
|
+
operations.push("filter");
|
|
966
|
+
if (groupedRefs.some((id) => capabilityDimensionIds.has(id)))
|
|
967
|
+
operations.push("group");
|
|
968
|
+
if (payload.pattern === "trend")
|
|
969
|
+
operations.push("trend");
|
|
970
|
+
if (payload.pattern === "ranking" && outputs.kinds.includes("rank"))
|
|
971
|
+
operations.push("rank");
|
|
972
|
+
if (outputs.kinds.includes("delta") ||
|
|
973
|
+
outputs.kinds.includes("percent_delta"))
|
|
974
|
+
operations.push("compare");
|
|
975
|
+
const blockId = stringValue(payload.qualifiedId) ?? block.objectKey;
|
|
976
|
+
const fingerprint = stringValue(recordPayload(payload.businessFingerprint)?.hash) ??
|
|
977
|
+
sha256(stableStringify({
|
|
978
|
+
blockId,
|
|
979
|
+
metricRefs,
|
|
980
|
+
grain,
|
|
981
|
+
groupedRefs,
|
|
982
|
+
filterRefs,
|
|
983
|
+
outputs,
|
|
984
|
+
}));
|
|
985
|
+
const capability = {
|
|
986
|
+
...semantic.capability,
|
|
987
|
+
defaultResultGrainId: grain,
|
|
988
|
+
resultGrainIds: uniqueNonBlank([
|
|
989
|
+
grain,
|
|
990
|
+
...dimensions.map((dimension) => dimension.entityId),
|
|
991
|
+
]),
|
|
992
|
+
dimensions,
|
|
993
|
+
timeDimensions,
|
|
994
|
+
operations: [...new Set(operations)],
|
|
995
|
+
supportedOutputKinds: outputs.kinds,
|
|
996
|
+
declaredOutputIds: outputs.ids,
|
|
997
|
+
executionCapabilities: [
|
|
998
|
+
{ route: "certified", adapterId: block.objectKey },
|
|
999
|
+
],
|
|
1000
|
+
sourceFingerprint: fingerprint.startsWith("sha256:")
|
|
1001
|
+
? fingerprint
|
|
1002
|
+
: `sha256:${fingerprint}`,
|
|
1003
|
+
};
|
|
1004
|
+
const normalized = normalizeMetricCapabilityContract(capability);
|
|
1005
|
+
if (!normalized)
|
|
1006
|
+
continue;
|
|
1007
|
+
objects.set(block.objectKey, {
|
|
1008
|
+
...block,
|
|
1009
|
+
payload: { ...payload, analyticalCapability: normalized },
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
function analyticalBlockOutputs(value) {
|
|
1014
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
1015
|
+
return undefined;
|
|
1016
|
+
const ids = [];
|
|
1017
|
+
const kinds = [];
|
|
1018
|
+
for (const item of value) {
|
|
1019
|
+
const output = recordPayload(item);
|
|
1020
|
+
const id = stringValue(output?.name);
|
|
1021
|
+
const role = stringValue(output?.role)?.toLowerCase();
|
|
1022
|
+
if (!id || !role)
|
|
1023
|
+
return undefined;
|
|
1024
|
+
const kind = role === "metric" || role === "measure" || role === "value"
|
|
1025
|
+
? "metric_value"
|
|
1026
|
+
: role === "dimension" || role === "grain"
|
|
1027
|
+
? "dimension"
|
|
1028
|
+
: role === "rank"
|
|
1029
|
+
? "rank"
|
|
1030
|
+
: role === "delta"
|
|
1031
|
+
? "delta"
|
|
1032
|
+
: role === "percent_delta"
|
|
1033
|
+
? "percent_delta"
|
|
1034
|
+
: undefined;
|
|
1035
|
+
if (!kind)
|
|
1036
|
+
return undefined;
|
|
1037
|
+
ids.push(id);
|
|
1038
|
+
kinds.push(kind);
|
|
1039
|
+
}
|
|
1040
|
+
return { ids: uniqueNonBlank(ids), kinds: [...new Set(kinds)] };
|
|
1041
|
+
}
|
|
1042
|
+
function recordPayload(value) {
|
|
1043
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
1044
|
+
? value
|
|
1045
|
+
: undefined;
|
|
1046
|
+
}
|
|
735
1047
|
/**
|
|
736
1048
|
* Manifest graph v2 intentionally omits verbose objects and edges. Snapshot
|
|
737
1049
|
* construction rematerializes that compiler graph once, then persists the
|
|
@@ -836,8 +1148,16 @@ function addManifestKnowledgeGraph(manifest, objects, edges) {
|
|
|
836
1148
|
sourceSystem: 'DQL compiled cross-domain policy',
|
|
837
1149
|
payload: compactObject(route),
|
|
838
1150
|
});
|
|
839
|
-
addMetadataEdge(edges,
|
|
840
|
-
|
|
1151
|
+
addMetadataEdge(edges, "cross_domain_route", keyByGraphId.get(`domain::${route.providerDomainId}`) ??
|
|
1152
|
+
`domain:${route.providerDomainId}`, objectKey, {
|
|
1153
|
+
state: route.state,
|
|
1154
|
+
reasonCodes: route.reasonCodes,
|
|
1155
|
+
});
|
|
1156
|
+
addMetadataEdge(edges, "cross_domain_route", objectKey, keyByGraphId.get(`domain::${route.consumerDomainId}`) ??
|
|
1157
|
+
`domain:${route.consumerDomainId}`, {
|
|
1158
|
+
state: route.state,
|
|
1159
|
+
reasonCodes: route.reasonCodes,
|
|
1160
|
+
});
|
|
841
1161
|
}
|
|
842
1162
|
for (const edge of graph.edges ?? []) {
|
|
843
1163
|
const fromKey = keyByGraphId.get(edge.from) ?? edge.from;
|
|
@@ -1036,6 +1356,15 @@ export class MetadataCatalog {
|
|
|
1036
1356
|
tokenize = 'porter unicode61'
|
|
1037
1357
|
);
|
|
1038
1358
|
|
|
1359
|
+
CREATE TABLE IF NOT EXISTS metadata_vector_index (
|
|
1360
|
+
object_key TEXT PRIMARY KEY,
|
|
1361
|
+
provider_id TEXT NOT NULL,
|
|
1362
|
+
dimensions INTEGER NOT NULL,
|
|
1363
|
+
vector BLOB NOT NULL,
|
|
1364
|
+
text_hash TEXT NOT NULL
|
|
1365
|
+
);
|
|
1366
|
+
CREATE INDEX IF NOT EXISTS idx_metadata_vector_provider ON metadata_vector_index(provider_id);
|
|
1367
|
+
|
|
1039
1368
|
CREATE TABLE IF NOT EXISTS metadata_edges (
|
|
1040
1369
|
edge_type TEXT NOT NULL,
|
|
1041
1370
|
from_key TEXT NOT NULL,
|
|
@@ -1192,6 +1521,10 @@ export class MetadataCatalog {
|
|
|
1192
1521
|
const insertFts = this.db.prepare(`
|
|
1193
1522
|
INSERT INTO metadata_fts (object_key, name, full_name, description, domain, owner, payload)
|
|
1194
1523
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1524
|
+
`);
|
|
1525
|
+
const insertVector = this.db.prepare(`
|
|
1526
|
+
INSERT INTO metadata_vector_index (object_key, provider_id, dimensions, vector, text_hash)
|
|
1527
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1195
1528
|
`);
|
|
1196
1529
|
const insertEdge = this.db.prepare(`
|
|
1197
1530
|
INSERT OR IGNORE INTO metadata_edges (
|
|
@@ -1226,6 +1559,7 @@ export class MetadataCatalog {
|
|
|
1226
1559
|
const txn = this.db.transaction(() => {
|
|
1227
1560
|
this.db.prepare('DELETE FROM metadata_edges').run();
|
|
1228
1561
|
this.db.prepare('DELETE FROM metadata_fts').run();
|
|
1562
|
+
this.db.prepare('DELETE FROM metadata_vector_index').run();
|
|
1229
1563
|
this.db.prepare('DELETE FROM metadata_objects').run();
|
|
1230
1564
|
this.db.prepare('DELETE FROM metadata_diagnostics').run();
|
|
1231
1565
|
this.db.prepare('DELETE FROM metadata_source_fingerprints').run();
|
|
@@ -1235,6 +1569,11 @@ export class MetadataCatalog {
|
|
|
1235
1569
|
const payload = object.payload ?? {};
|
|
1236
1570
|
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
1571
|
insertFts.run(object.objectKey, object.name, object.fullName ?? '', object.description ?? '', object.domain ?? '', object.owner ?? '', searchableMetadataPayload(payload));
|
|
1572
|
+
if (isVectorIndexObject(object)) {
|
|
1573
|
+
const vectorText = metadataVectorText(object);
|
|
1574
|
+
const vector = DEFAULT_VECTOR_PROVIDER.embedOne(vectorText);
|
|
1575
|
+
insertVector.run(object.objectKey, DEFAULT_VECTOR_PROVIDER.id, vector.length, encodeFloat32Vector(vector), sha256(vectorText));
|
|
1576
|
+
}
|
|
1238
1577
|
}
|
|
1239
1578
|
for (const edge of snapshot.edges) {
|
|
1240
1579
|
insertEdge.run(edge.edgeType, edge.fromKey, edge.toKey, edge.confidence ?? 1, JSON.stringify(edge.payload ?? {}), now);
|
|
@@ -1262,6 +1601,8 @@ export class MetadataCatalog {
|
|
|
1262
1601
|
setState.run('compile_conflicts_json', JSON.stringify(snapshot.compileConflicts ?? []));
|
|
1263
1602
|
setState.run('manifest_generated_at', snapshot.manifest.generatedAt);
|
|
1264
1603
|
setState.run('index_version', METADATA_INDEX_VERSION);
|
|
1604
|
+
setState.run('vector_provider', DEFAULT_VECTOR_PROVIDER.id);
|
|
1605
|
+
setState.run('vector_dimensions', String(DEFAULT_VECTOR_PROVIDER.dimensions));
|
|
1265
1606
|
});
|
|
1266
1607
|
txn();
|
|
1267
1608
|
}
|
|
@@ -1303,6 +1644,9 @@ export class MetadataCatalog {
|
|
|
1303
1644
|
const insertFts = this.db.prepare(`
|
|
1304
1645
|
INSERT INTO metadata_fts (object_key, name, full_name, description, domain, owner, payload)
|
|
1305
1646
|
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
1647
|
+
const insertVector = this.db.prepare(`
|
|
1648
|
+
INSERT INTO metadata_vector_index (object_key, provider_id, dimensions, vector, text_hash)
|
|
1649
|
+
VALUES (?, ?, ?, ?, ?)`);
|
|
1306
1650
|
const insertEdge = this.db.prepare(`
|
|
1307
1651
|
INSERT OR IGNORE INTO metadata_edges (edge_type, from_key, to_key, confidence, payload_json, created_at)
|
|
1308
1652
|
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
@@ -1324,15 +1668,18 @@ export class MetadataCatalog {
|
|
|
1324
1668
|
INSERT OR REPLACE INTO skill_bodies (body_hash, encoding, compressed_body, original_length)
|
|
1325
1669
|
VALUES (?, 'br', ?, ?)`);
|
|
1326
1670
|
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) = ?)');
|
|
1671
|
+
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
1672
|
const deleteObjectsForSource = this.db.prepare('DELETE FROM metadata_objects WHERE COALESCE(source_path, source_system) = ?');
|
|
1328
1673
|
const txn = this.db.transaction(() => {
|
|
1329
1674
|
// Drop changed + removed sources' rows, and ALL sourceless rows (never
|
|
1330
1675
|
// fingerprinted, so always refreshed).
|
|
1331
1676
|
for (const source of [...changedSources, ...removedSources]) {
|
|
1332
1677
|
deleteFtsForSource.run(source);
|
|
1678
|
+
deleteVectorsForSource.run(source);
|
|
1333
1679
|
deleteObjectsForSource.run(source);
|
|
1334
1680
|
}
|
|
1335
1681
|
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();
|
|
1682
|
+
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
1683
|
this.db.prepare('DELETE FROM metadata_objects WHERE source_path IS NULL AND source_system IS NULL').run();
|
|
1337
1684
|
// Re-insert only objects from changed sources or sourceless; unchanged
|
|
1338
1685
|
// sources' rows are left untouched.
|
|
@@ -1343,6 +1690,11 @@ export class MetadataCatalog {
|
|
|
1343
1690
|
const payload = object.payload ?? {};
|
|
1344
1691
|
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
1692
|
insertFts.run(object.objectKey, object.name, object.fullName ?? '', object.description ?? '', object.domain ?? '', object.owner ?? '', searchableMetadataPayload(payload));
|
|
1693
|
+
if (isVectorIndexObject(object)) {
|
|
1694
|
+
const vectorText = metadataVectorText(object);
|
|
1695
|
+
const vector = DEFAULT_VECTOR_PROVIDER.embedOne(vectorText);
|
|
1696
|
+
insertVector.run(object.objectKey, DEFAULT_VECTOR_PROVIDER.id, vector.length, encodeFloat32Vector(vector), sha256(vectorText));
|
|
1697
|
+
}
|
|
1346
1698
|
}
|
|
1347
1699
|
// Cross-source tables: rebuild fully.
|
|
1348
1700
|
this.db.prepare('DELETE FROM metadata_edges').run();
|
|
@@ -1378,12 +1730,14 @@ export class MetadataCatalog {
|
|
|
1378
1730
|
setState.run('compile_conflicts_json', JSON.stringify(snapshot.compileConflicts ?? []));
|
|
1379
1731
|
setState.run('manifest_generated_at', snapshot.manifest.generatedAt);
|
|
1380
1732
|
setState.run('index_version', METADATA_INDEX_VERSION);
|
|
1733
|
+
setState.run('vector_provider', DEFAULT_VECTOR_PROVIDER.id);
|
|
1734
|
+
setState.run('vector_dimensions', String(DEFAULT_VECTOR_PROVIDER.dimensions));
|
|
1381
1735
|
});
|
|
1382
1736
|
txn();
|
|
1383
1737
|
return { mode: 'incremental', changedSources: changedSources.size };
|
|
1384
1738
|
}
|
|
1385
1739
|
searchObjects(options) {
|
|
1386
|
-
const { query, objectTypes, domain, limit = 40 } = options;
|
|
1740
|
+
const { query, objectTypes, domain, domains, limit = 40 } = options;
|
|
1387
1741
|
const match = buildFtsMatch(query, { prefix: true });
|
|
1388
1742
|
if (!match.or)
|
|
1389
1743
|
return this.listObjects({ objectTypes, domain, limit });
|
|
@@ -1397,6 +1751,10 @@ export class MetadataCatalog {
|
|
|
1397
1751
|
filters.push('o.domain = ?');
|
|
1398
1752
|
extraParams.push(domain);
|
|
1399
1753
|
}
|
|
1754
|
+
else if (domains?.length) {
|
|
1755
|
+
filters.push(`(o.domain IS NULL OR o.domain IN (${domains.map(() => '?').join(', ')}))`);
|
|
1756
|
+
extraParams.push(...domains);
|
|
1757
|
+
}
|
|
1400
1758
|
const whereExtra = filters.length > 0 ? ` AND ${filters.join(' AND ')}` : '';
|
|
1401
1759
|
const runMatch = (matchExpr) => this.db.prepare(`
|
|
1402
1760
|
SELECT o.*,
|
|
@@ -1429,6 +1787,106 @@ export class MetadataCatalog {
|
|
|
1429
1787
|
snippet: row.snip ?? undefined,
|
|
1430
1788
|
}));
|
|
1431
1789
|
}
|
|
1790
|
+
/** Replace the snapshot vector lane with an explicitly configured provider. */
|
|
1791
|
+
async rebuildVectorIndex(provider, batchSize = 96) {
|
|
1792
|
+
const objects = [];
|
|
1793
|
+
this.scanObjects({ batchSize: 500 }, (rows) => {
|
|
1794
|
+
objects.push(...rows.filter(isVectorIndexObject));
|
|
1795
|
+
});
|
|
1796
|
+
const encoded = [];
|
|
1797
|
+
let dimensions = 0;
|
|
1798
|
+
for (let offset = 0; offset < objects.length; offset += Math.max(1, batchSize)) {
|
|
1799
|
+
const batch = objects.slice(offset, offset + Math.max(1, batchSize));
|
|
1800
|
+
const texts = batch.map(metadataVectorText);
|
|
1801
|
+
const vectors = await provider.embed(texts);
|
|
1802
|
+
if (vectors.length !== batch.length) {
|
|
1803
|
+
throw new Error(`Embedding provider ${provider.id} returned ${vectors.length} vectors for ${batch.length} objects.`);
|
|
1804
|
+
}
|
|
1805
|
+
for (let index = 0; index < batch.length; index += 1) {
|
|
1806
|
+
const vector = vectors[index] ?? [];
|
|
1807
|
+
if (vector.length === 0)
|
|
1808
|
+
throw new Error(`Embedding provider ${provider.id} returned an empty vector.`);
|
|
1809
|
+
if (dimensions === 0)
|
|
1810
|
+
dimensions = vector.length;
|
|
1811
|
+
if (vector.length !== dimensions) {
|
|
1812
|
+
throw new Error(`Embedding provider ${provider.id} changed dimensions from ${dimensions} to ${vector.length}.`);
|
|
1813
|
+
}
|
|
1814
|
+
encoded.push({
|
|
1815
|
+
objectKey: batch[index].objectKey,
|
|
1816
|
+
dimensions,
|
|
1817
|
+
vector: encodeFloat32Vector(vector),
|
|
1818
|
+
textHash: sha256(texts[index]),
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
const insert = this.db.prepare(`
|
|
1823
|
+
INSERT INTO metadata_vector_index (object_key, provider_id, dimensions, vector, text_hash)
|
|
1824
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1825
|
+
`);
|
|
1826
|
+
const setState = this.db.prepare('INSERT OR REPLACE INTO metadata_state (key, value) VALUES (?, ?)');
|
|
1827
|
+
this.db.transaction(() => {
|
|
1828
|
+
this.db.prepare('DELETE FROM metadata_vector_index').run();
|
|
1829
|
+
for (const item of encoded) {
|
|
1830
|
+
insert.run(item.objectKey, provider.id, item.dimensions, item.vector, item.textHash);
|
|
1831
|
+
}
|
|
1832
|
+
setState.run('vector_provider', provider.id);
|
|
1833
|
+
setState.run('vector_dimensions', String(dimensions));
|
|
1834
|
+
})();
|
|
1835
|
+
}
|
|
1836
|
+
/** Independent vector candidate generation over all eligible snapshot cards. */
|
|
1837
|
+
async searchVectorObjects(options) {
|
|
1838
|
+
const indexedProviderId = this.state('vector_provider') ?? DEFAULT_VECTOR_PROVIDER.id;
|
|
1839
|
+
const indexedDimensions = Number(this.state('vector_dimensions') ?? DEFAULT_VECTOR_PROVIDER.dimensions);
|
|
1840
|
+
const provider = options.provider ?? DEFAULT_VECTOR_PROVIDER;
|
|
1841
|
+
if (provider.id !== indexedProviderId) {
|
|
1842
|
+
return {
|
|
1843
|
+
providerId: indexedProviderId,
|
|
1844
|
+
dimensions: indexedDimensions,
|
|
1845
|
+
candidates: [],
|
|
1846
|
+
unavailableReason: `requested provider ${provider.id} does not match snapshot vector provider ${indexedProviderId}`,
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
const [queryVector] = provider === DEFAULT_VECTOR_PROVIDER
|
|
1850
|
+
? [DEFAULT_VECTOR_PROVIDER.embedOne(options.query)]
|
|
1851
|
+
: await provider.embed([options.query]);
|
|
1852
|
+
if (!queryVector || queryVector.length !== indexedDimensions) {
|
|
1853
|
+
return {
|
|
1854
|
+
providerId: indexedProviderId,
|
|
1855
|
+
dimensions: indexedDimensions,
|
|
1856
|
+
candidates: [],
|
|
1857
|
+
unavailableReason: `query vector dimensions do not match snapshot index (${queryVector?.length ?? 0} vs ${indexedDimensions})`,
|
|
1858
|
+
};
|
|
1859
|
+
}
|
|
1860
|
+
const filters = ['v.provider_id = ?'];
|
|
1861
|
+
const params = [indexedProviderId];
|
|
1862
|
+
if (options.objectTypes?.length) {
|
|
1863
|
+
filters.push(`o.object_type IN (${options.objectTypes.map(() => '?').join(', ')})`);
|
|
1864
|
+
params.push(...options.objectTypes);
|
|
1865
|
+
}
|
|
1866
|
+
if (options.domains?.length) {
|
|
1867
|
+
filters.push(`(o.domain IS NULL OR o.domain IN (${options.domains.map(() => '?').join(', ')}))`);
|
|
1868
|
+
params.push(...options.domains);
|
|
1869
|
+
}
|
|
1870
|
+
const rows = this.db.prepare(`
|
|
1871
|
+
SELECT o.*, v.vector AS vector_blob
|
|
1872
|
+
FROM metadata_vector_index AS v
|
|
1873
|
+
JOIN metadata_objects AS o ON o.object_key = v.object_key
|
|
1874
|
+
WHERE ${filters.join(' AND ')}
|
|
1875
|
+
ORDER BY o.object_key
|
|
1876
|
+
`).all(...params);
|
|
1877
|
+
const candidates = rows
|
|
1878
|
+
.map((row) => {
|
|
1879
|
+
const cosine = cosineSimilarity(queryVector, decodeFloat32Vector(row.vector_blob));
|
|
1880
|
+
return { ...rowToObject(row), score: Number(((cosine + 1) / 2).toFixed(6)) };
|
|
1881
|
+
})
|
|
1882
|
+
// A cosine of zero maps to 0.5 and is not evidence. Requiring positive
|
|
1883
|
+
// separation prevents hash collisions/unrelated cards from flooding the
|
|
1884
|
+
// downstream bounded context while retaining independent vector recall.
|
|
1885
|
+
.filter((row) => (row.score ?? 0) > 0.55)
|
|
1886
|
+
.sort((left, right) => (right.score ?? 0) - (left.score ?? 0) || left.objectKey.localeCompare(right.objectKey))
|
|
1887
|
+
.slice(0, Math.max(1, options.limit ?? 40));
|
|
1888
|
+
return { providerId: indexedProviderId, dimensions: indexedDimensions, candidates };
|
|
1889
|
+
}
|
|
1432
1890
|
listObjects(options = {}) {
|
|
1433
1891
|
const filters = [];
|
|
1434
1892
|
const params = [];
|
|
@@ -1556,13 +2014,49 @@ export class MetadataCatalog {
|
|
|
1556
2014
|
return rows;
|
|
1557
2015
|
}
|
|
1558
2016
|
findObjectByIdentity(identity) {
|
|
2017
|
+
return this.resolveIdentity(identity).object ?? null;
|
|
2018
|
+
}
|
|
2019
|
+
/** Resolve exact/qualified/native/alias identities without first-match wins. */
|
|
2020
|
+
resolveIdentity(identity) {
|
|
2021
|
+
const normalized = identity.trim();
|
|
2022
|
+
if (!normalized)
|
|
2023
|
+
return { identity, status: 'missing', candidates: [] };
|
|
2024
|
+
const exact = this.db.prepare(`
|
|
2025
|
+
SELECT * FROM metadata_objects WHERE object_key = ?
|
|
2026
|
+
`).get(normalized);
|
|
2027
|
+
if (exact) {
|
|
2028
|
+
const object = rowToObject(exact);
|
|
2029
|
+
return { identity, status: 'resolved', matchedBy: 'object_key', object, candidates: [object] };
|
|
2030
|
+
}
|
|
1559
2031
|
const rows = this.db.prepare(`
|
|
1560
|
-
SELECT
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
2032
|
+
SELECT DISTINCT o.*
|
|
2033
|
+
FROM metadata_objects AS o
|
|
2034
|
+
WHERE o.full_name = ?
|
|
2035
|
+
OR json_extract(o.payload_json, '$.sourceNativeId') = ?
|
|
2036
|
+
OR EXISTS (
|
|
2037
|
+
SELECT 1
|
|
2038
|
+
FROM json_each(COALESCE(json_extract(o.payload_json, '$.aliases'), '[]')) AS alias
|
|
2039
|
+
WHERE alias.value = ?
|
|
2040
|
+
)
|
|
2041
|
+
ORDER BY o.object_key
|
|
2042
|
+
LIMIT 21
|
|
2043
|
+
`).all(normalized, normalized, normalized);
|
|
2044
|
+
const candidates = rows.map(rowToObject);
|
|
2045
|
+
if (candidates.length === 0)
|
|
2046
|
+
return { identity, status: 'missing', candidates: [] };
|
|
2047
|
+
if (candidates.length > 1)
|
|
2048
|
+
return { identity, status: 'ambiguous', candidates };
|
|
2049
|
+
const object = candidates[0];
|
|
2050
|
+
const payload = object.payload ?? {};
|
|
2051
|
+
const aliases = metadataStringArray(payload.aliases);
|
|
2052
|
+
const matchedBy = object.fullName === normalized
|
|
2053
|
+
? 'qualified_id'
|
|
2054
|
+
: stringValue(payload.sourceNativeId) === normalized
|
|
2055
|
+
? 'source_native_id'
|
|
2056
|
+
: aliases.includes(normalized)
|
|
2057
|
+
? 'alias'
|
|
2058
|
+
: 'qualified_id';
|
|
2059
|
+
return { identity, status: 'resolved', matchedBy, object, candidates };
|
|
1566
2060
|
}
|
|
1567
2061
|
edgesForKeys(keys, hops = 1) {
|
|
1568
2062
|
let frontier = new Set(keys.filter(Boolean));
|
|
@@ -1944,11 +2438,11 @@ function objectFromKGNode(node) {
|
|
|
1944
2438
|
reviewCadence: node.reviewCadence,
|
|
1945
2439
|
pattern: node.pattern,
|
|
1946
2440
|
grain: node.grain,
|
|
1947
|
-
entities: node.entities ?? [],
|
|
2441
|
+
entities: node.entities ?? node.payload?.entities ?? [],
|
|
1948
2442
|
declaredOutputs: node.declaredOutputs ?? [],
|
|
1949
2443
|
outputs: node.outputs ?? [],
|
|
1950
2444
|
outputContract: node.outputContract ?? [],
|
|
1951
|
-
dimensions: node.dimensions ?? [],
|
|
2445
|
+
dimensions: node.dimensions ?? node.payload?.dimensions ?? [],
|
|
1952
2446
|
allowedFilters: node.allowedFilters ?? [],
|
|
1953
2447
|
parameterPolicy: node.parameterPolicy ?? [],
|
|
1954
2448
|
parameters: node.parameters ?? [],
|
|
@@ -2087,7 +2581,8 @@ function addSkillObjects(skills, objects, edges) {
|
|
|
2087
2581
|
domain: skill.domain,
|
|
2088
2582
|
domains: skill.domains ?? [],
|
|
2089
2583
|
modelAreaRefs: skill.modelAreaRefs ?? [],
|
|
2090
|
-
status: skill.status ??
|
|
2584
|
+
status: skill.status ?? "active",
|
|
2585
|
+
analyticalPolicy: skill.analyticalPolicy,
|
|
2091
2586
|
bodyHash,
|
|
2092
2587
|
}));
|
|
2093
2588
|
const objectKey = `skill:${identity}`;
|
|
@@ -2122,6 +2617,7 @@ function addSkillObjects(skills, objects, edges) {
|
|
|
2122
2617
|
examples: skill.examples ?? [],
|
|
2123
2618
|
sourceRefs: skill.sourceRefs ?? [],
|
|
2124
2619
|
vocabulary: skill.vocabulary,
|
|
2620
|
+
analyticalPolicy: skill.analyticalPolicy,
|
|
2125
2621
|
bodyHash,
|
|
2126
2622
|
sourceFingerprint,
|
|
2127
2623
|
isStarter: skill.isStarter,
|
|
@@ -3190,7 +3686,7 @@ async function planContextPackRoute(input) {
|
|
|
3190
3686
|
plan: input.questionPlan,
|
|
3191
3687
|
block: object,
|
|
3192
3688
|
exactExampleMatch: hasExactExampleQuestion(input.request.question, object),
|
|
3193
|
-
definitionLookup: intent === 'definition_lookup',
|
|
3689
|
+
definitionLookup: intent === 'definition_lookup' && objectNameInQuestion(input.request.question, object),
|
|
3194
3690
|
}),
|
|
3195
3691
|
applicabilityScore: applicabilityByKey.get(object.objectKey)?.score ?? 0,
|
|
3196
3692
|
}))
|
|
@@ -3238,7 +3734,7 @@ async function planContextPackRoute(input) {
|
|
|
3238
3734
|
: undefined;
|
|
3239
3735
|
const exactExampleMatch = exact ? hasExactExampleQuestion(input.request.question, exact) : false;
|
|
3240
3736
|
const directCertifiedBypass = Boolean(exact && (exactExampleMatch
|
|
3241
|
-
|| intent === 'definition_lookup'
|
|
3737
|
+
|| (intent === 'definition_lookup' && objectNameInQuestion(input.request.question, exact))
|
|
3242
3738
|
|| objectNameInQuestion(input.request.question, exact)));
|
|
3243
3739
|
const blockFitObject = exact
|
|
3244
3740
|
?? (contextApplicability ? input.objects.find((object) => object.objectKey === contextApplicability.objectKey) : undefined);
|
|
@@ -3248,7 +3744,8 @@ async function planContextPackRoute(input) {
|
|
|
3248
3744
|
plan: input.questionPlan,
|
|
3249
3745
|
block: blockFitObject,
|
|
3250
3746
|
exactExampleMatch: exact ? exactExampleMatch : false,
|
|
3251
|
-
definitionLookup: Boolean(exact && intent === 'definition_lookup'
|
|
3747
|
+
definitionLookup: Boolean(exact && intent === 'definition_lookup'
|
|
3748
|
+
&& objectNameInQuestion(input.request.question, blockFitObject)),
|
|
3252
3749
|
})
|
|
3253
3750
|
: undefined;
|
|
3254
3751
|
const blockFit = rawBlockFit && blockFitObject
|
|
@@ -3635,8 +4132,14 @@ function isGeneratedMetadataIntent(intent) {
|
|
|
3635
4132
|
function findExactCertifiedObject(question, intent, objects) {
|
|
3636
4133
|
const candidates = objects.filter((object) => isCertifiedMetadataObject(object));
|
|
3637
4134
|
if (intent === 'definition_lookup') {
|
|
3638
|
-
|
|
3639
|
-
|
|
4135
|
+
// A definition-shaped sentence is not itself evidence of a match. Small
|
|
4136
|
+
// catalogs often return every certified term; selecting the first one made
|
|
4137
|
+
// unrelated questions (for example weather) look governed. Prefer an
|
|
4138
|
+
// explicitly named/example block, then require the term/view name itself.
|
|
4139
|
+
return candidates.find((object) => object.objectType === 'dql_block'
|
|
4140
|
+
&& (hasExactExampleQuestion(question, object) || objectNameInQuestion(question, object)))
|
|
4141
|
+
?? candidates.find((object) => (object.objectType === 'dql_term' || object.objectType === 'business_view')
|
|
4142
|
+
&& objectNameInQuestion(question, object));
|
|
3640
4143
|
}
|
|
3641
4144
|
const namedExact = candidates.find((object) => object.objectType === 'dql_block' &&
|
|
3642
4145
|
objectNameInQuestion(question, object) &&
|
|
@@ -5555,6 +6058,32 @@ function searchableMetadataPayload(payload) {
|
|
|
5555
6058
|
visit(payload, 0);
|
|
5556
6059
|
return tokens.join(' ');
|
|
5557
6060
|
}
|
|
6061
|
+
const VECTOR_EXCLUDED_OBJECT_TYPES = new Set([
|
|
6062
|
+
...COLUMN_OBJECT_TYPES,
|
|
6063
|
+
'skill',
|
|
6064
|
+
'runtime_value',
|
|
6065
|
+
]);
|
|
6066
|
+
function isVectorIndexObject(object) {
|
|
6067
|
+
return !VECTOR_EXCLUDED_OBJECT_TYPES.has(object.objectType);
|
|
6068
|
+
}
|
|
6069
|
+
function metadataVectorText(object) {
|
|
6070
|
+
return [
|
|
6071
|
+
object.name,
|
|
6072
|
+
object.fullName ?? '',
|
|
6073
|
+
object.description ?? '',
|
|
6074
|
+
object.domain ?? '',
|
|
6075
|
+
object.owner ?? '',
|
|
6076
|
+
searchableMetadataPayload(object.payload ?? {}),
|
|
6077
|
+
].filter(Boolean).join('\n').slice(0, 16_000);
|
|
6078
|
+
}
|
|
6079
|
+
function encodeFloat32Vector(vector) {
|
|
6080
|
+
const values = Float32Array.from(vector);
|
|
6081
|
+
return Buffer.from(values.buffer, values.byteOffset, values.byteLength);
|
|
6082
|
+
}
|
|
6083
|
+
function decodeFloat32Vector(blob) {
|
|
6084
|
+
const bytes = blob.buffer.slice(blob.byteOffset, blob.byteOffset + blob.byteLength);
|
|
6085
|
+
return new Float32Array(bytes);
|
|
6086
|
+
}
|
|
5558
6087
|
function rowToObject(row) {
|
|
5559
6088
|
return {
|
|
5560
6089
|
objectKey: row.object_key,
|
|
@@ -5907,13 +6436,22 @@ function skillFromMetadataObject(object) {
|
|
|
5907
6436
|
examples: stringArrayPayload(payload.examples),
|
|
5908
6437
|
sourceRefs: stringArrayPayload(payload.sourceRefs),
|
|
5909
6438
|
vocabulary: stringRecordPayload(payload.vocabulary),
|
|
5910
|
-
|
|
5911
|
-
|
|
6439
|
+
analyticalPolicy: skillAnalyticalPolicyPayload(payload.analyticalPolicy),
|
|
6440
|
+
body: "",
|
|
6441
|
+
sourcePath: object.sourcePath ?? "",
|
|
5912
6442
|
isStarter: payload.isStarter === true,
|
|
5913
6443
|
};
|
|
5914
6444
|
}
|
|
5915
6445
|
function localContextSkillFromParsed(skill, object) {
|
|
5916
6446
|
const guidance = skill.body.slice(0, 4_000);
|
|
6447
|
+
const sourceHash = stringPayload(object.payload?.sourceFingerprint);
|
|
6448
|
+
const policy = skill.analyticalPolicy && sourceHash
|
|
6449
|
+
? {
|
|
6450
|
+
policyId: `${skill.qualifiedId ?? skill.id}#analytical`,
|
|
6451
|
+
sourceHash,
|
|
6452
|
+
...skill.analyticalPolicy,
|
|
6453
|
+
}
|
|
6454
|
+
: undefined;
|
|
5917
6455
|
return {
|
|
5918
6456
|
objectKey: object.objectKey,
|
|
5919
6457
|
id: skill.id,
|
|
@@ -5933,6 +6471,7 @@ function localContextSkillFromParsed(skill, object) {
|
|
|
5933
6471
|
requiredFilters: [...(skill.requiredFilters ?? [])],
|
|
5934
6472
|
clarifyWhen: [...(skill.clarifyWhen ?? [])],
|
|
5935
6473
|
vocabulary: { ...skill.vocabulary },
|
|
6474
|
+
...(policy ? { analyticalPolicy: policy } : {}),
|
|
5936
6475
|
guidance,
|
|
5937
6476
|
guidanceTruncated: guidance.length < skill.body.length,
|
|
5938
6477
|
sourceRefs: [...(skill.sourceRefs ?? [])],
|
|
@@ -5940,6 +6479,38 @@ function localContextSkillFromParsed(skill, object) {
|
|
|
5940
6479
|
sourcePath: object.sourcePath,
|
|
5941
6480
|
};
|
|
5942
6481
|
}
|
|
6482
|
+
function skillAnalyticalPolicyPayload(value) {
|
|
6483
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
6484
|
+
return undefined;
|
|
6485
|
+
const record = value;
|
|
6486
|
+
const completenessPolicy = stringPayload(record.completenessPolicy);
|
|
6487
|
+
const comparisonAlignment = stringPayload(record.comparisonAlignment);
|
|
6488
|
+
const defaultRankingPeriod = stringPayload(record.defaultRankingPeriod);
|
|
6489
|
+
const policy = {
|
|
6490
|
+
metricIds: stringArrayPayload(record.metricIds),
|
|
6491
|
+
timeRole: stringPayload(record.timeRole),
|
|
6492
|
+
calendarId: stringPayload(record.calendarId),
|
|
6493
|
+
timezone: stringPayload(record.timezone),
|
|
6494
|
+
completenessPolicy: completenessPolicy === "partial_current" ||
|
|
6495
|
+
completenessPolicy === "latest_complete" ||
|
|
6496
|
+
completenessPolicy === "closed_period"
|
|
6497
|
+
? completenessPolicy
|
|
6498
|
+
: undefined,
|
|
6499
|
+
comparisonAlignment: comparisonAlignment === "elapsed_period" ||
|
|
6500
|
+
comparisonAlignment === "calendar_period" ||
|
|
6501
|
+
comparisonAlignment === "fiscal_period"
|
|
6502
|
+
? comparisonAlignment
|
|
6503
|
+
: undefined,
|
|
6504
|
+
defaultRankingPeriod: defaultRankingPeriod === "current" ||
|
|
6505
|
+
defaultRankingPeriod === "comparison"
|
|
6506
|
+
? defaultRankingPeriod
|
|
6507
|
+
: undefined,
|
|
6508
|
+
narrativeGuidance: stringArrayPayload(record.narrativeGuidance),
|
|
6509
|
+
};
|
|
6510
|
+
return Object.values(policy).some((item) => Array.isArray(item) ? item.length > 0 : Boolean(item))
|
|
6511
|
+
? policy
|
|
6512
|
+
: undefined;
|
|
6513
|
+
}
|
|
5943
6514
|
function stringPayload(value) {
|
|
5944
6515
|
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
5945
6516
|
}
|