@duckcodeailabs/dql-agent 1.6.33 → 1.6.34
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 +26 -8
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-engine.js +46 -7
- 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 +15 -3
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-planner.d.ts.map +1 -1
- package/dist/agent-run-planner.js +14 -3
- package/dist/agent-run-planner.js.map +1 -1
- package/dist/agentic/answer-contract.d.ts +37 -0
- package/dist/agentic/answer-contract.d.ts.map +1 -0
- package/dist/agentic/answer-contract.js +47 -0
- package/dist/agentic/answer-contract.js.map +1 -0
- package/dist/agentic/tool-loop.d.ts +48 -0
- package/dist/agentic/tool-loop.d.ts.map +1 -0
- package/dist/agentic/tool-loop.js +181 -0
- package/dist/agentic/tool-loop.js.map +1 -0
- package/dist/agentic/toolset.d.ts +40 -0
- package/dist/agentic/toolset.d.ts.map +1 -0
- package/dist/agentic/toolset.js +265 -0
- package/dist/agentic/toolset.js.map +1 -0
- package/dist/answer-loop.d.ts +25 -2
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +368 -72
- package/dist/answer-loop.js.map +1 -1
- package/dist/cascade/budgets.d.ts +20 -2
- package/dist/cascade/budgets.d.ts.map +1 -1
- package/dist/cascade/budgets.js +76 -43
- package/dist/cascade/budgets.js.map +1 -1
- package/dist/cascade/route-policy.d.ts +1 -1
- package/dist/cascade/route-policy.d.ts.map +1 -1
- package/dist/cascade/route-policy.js +1 -0
- package/dist/cascade/route-policy.js.map +1 -1
- package/dist/embeddings/provider.d.ts +67 -4
- package/dist/embeddings/provider.d.ts.map +1 -1
- package/dist/embeddings/provider.js +167 -6
- package/dist/embeddings/provider.js.map +1 -1
- package/dist/hints/correction-eval.d.ts +30 -0
- package/dist/hints/correction-eval.d.ts.map +1 -0
- package/dist/hints/correction-eval.js +63 -0
- package/dist/hints/correction-eval.js.map +1 -0
- package/dist/hints/staleness.d.ts +30 -0
- package/dist/hints/staleness.d.ts.map +1 -0
- package/dist/hints/staleness.js +38 -0
- package/dist/hints/staleness.js.map +1 -0
- package/dist/hints/store.d.ts.map +1 -1
- package/dist/hints/store.js +3 -32
- package/dist/hints/store.js.map +1 -1
- package/dist/index.d.ts +39 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +143 -7
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts.map +1 -1
- package/dist/intent-controller.js +15 -3
- package/dist/intent-controller.js.map +1 -1
- package/dist/kg/sqlite-fts.d.ts +13 -0
- package/dist/kg/sqlite-fts.d.ts.map +1 -1
- package/dist/kg/sqlite-fts.js +75 -47
- package/dist/kg/sqlite-fts.js.map +1 -1
- package/dist/memory/fts-query.d.ts +37 -4
- package/dist/memory/fts-query.d.ts.map +1 -1
- package/dist/memory/fts-query.js +53 -12
- package/dist/memory/fts-query.js.map +1 -1
- package/dist/metadata/analysis-planner.js +45 -10
- package/dist/metadata/analysis-planner.js.map +1 -1
- package/dist/metadata/block-adapt.d.ts +37 -0
- package/dist/metadata/block-adapt.d.ts.map +1 -0
- package/dist/metadata/block-adapt.js +89 -0
- package/dist/metadata/block-adapt.js.map +1 -0
- package/dist/metadata/catalog.d.ts +39 -0
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +361 -41
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/example-match.d.ts +30 -0
- package/dist/metadata/example-match.d.ts.map +1 -0
- package/dist/metadata/example-match.js +69 -0
- package/dist/metadata/example-match.js.map +1 -0
- package/dist/metadata/grain-ledger.d.ts +51 -0
- package/dist/metadata/grain-ledger.d.ts.map +1 -0
- package/dist/metadata/grain-ledger.js +174 -0
- package/dist/metadata/grain-ledger.js.map +1 -0
- package/dist/metadata/metric-match.d.ts +21 -1
- package/dist/metadata/metric-match.d.ts.map +1 -1
- package/dist/metadata/metric-match.js +157 -21
- package/dist/metadata/metric-match.js.map +1 -1
- package/dist/metadata/sql-grounding.d.ts +4 -2
- package/dist/metadata/sql-grounding.d.ts.map +1 -1
- package/dist/metadata/sql-grounding.js +4 -2
- package/dist/metadata/sql-grounding.js.map +1 -1
- package/dist/metadata/sql-retrieval.d.ts +3 -2
- package/dist/metadata/sql-retrieval.d.ts.map +1 -1
- package/dist/metadata/sql-retrieval.js +23 -5
- package/dist/metadata/sql-retrieval.js.map +1 -1
- package/dist/propose/build-from-prompt.d.ts.map +1 -1
- package/dist/propose/build-from-prompt.js +10 -4
- package/dist/propose/build-from-prompt.js.map +1 -1
- package/dist/propose/failure-analysis.d.ts +48 -0
- package/dist/propose/failure-analysis.d.ts.map +1 -0
- package/dist/propose/failure-analysis.js +71 -0
- package/dist/propose/failure-analysis.js.map +1 -0
- package/dist/propose/join-mining.d.ts +18 -0
- package/dist/propose/join-mining.d.ts.map +1 -0
- package/dist/propose/join-mining.js +74 -0
- package/dist/propose/join-mining.js.map +1 -0
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +36 -1
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/index.d.ts +2 -2
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +34 -1
- package/dist/providers/openai.js.map +1 -1
- package/dist/providers/reasoning-effort.d.ts +27 -0
- package/dist/providers/reasoning-effort.d.ts.map +1 -1
- package/dist/providers/reasoning-effort.js +32 -0
- package/dist/providers/reasoning-effort.js.map +1 -1
- package/dist/providers/types.d.ts +2 -1
- package/dist/providers/types.d.ts.map +1 -1
- package/dist/research-loop.d.ts.map +1 -1
- package/dist/research-loop.js +9 -2
- package/dist/research-loop.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +147 -20
- package/dist/semantic-bridge/compose.js.map +1 -1
- package/dist/skills/bootstrap.d.ts +57 -0
- package/dist/skills/bootstrap.d.ts.map +1 -0
- package/dist/skills/bootstrap.js +260 -0
- package/dist/skills/bootstrap.js.map +1 -0
- package/dist/skills/defaults.d.ts +2 -2
- package/dist/skills/defaults.js +2 -2
- package/dist/skills/domain-skills.d.ts +24 -0
- package/dist/skills/domain-skills.d.ts.map +1 -0
- package/dist/skills/domain-skills.js +76 -0
- package/dist/skills/domain-skills.js.map +1 -0
- package/dist/skills/loader.d.ts +46 -8
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +186 -41
- package/dist/skills/loader.js.map +1 -1
- package/dist/tools/registry.js +12 -12
- package/dist/tools/registry.js.map +1 -1
- package/package.json +4 -4
package/dist/metadata/catalog.js
CHANGED
|
@@ -19,6 +19,9 @@ import { buildRuntimeValueIndex, normalizeValueIndexText, } from '../grounding/v
|
|
|
19
19
|
import { grainMatches, requestedGrainFromPlan, } from './grain-gate.js';
|
|
20
20
|
import { certifiedFitAllowsTier1, evaluateCertifiedBlockFit, } from './block-fit.js';
|
|
21
21
|
import { retrieveScopedHints } from '../hints/retrieval.js';
|
|
22
|
+
import { buildFtsMatch, sanitizeFtsQuery } from '../memory/fts-query.js';
|
|
23
|
+
import { envEmbeddingProvider } from '../embeddings/provider.js';
|
|
24
|
+
import { matchExampleParaphrase } from './example-match.js';
|
|
22
25
|
const require = createRequire(import.meta.url);
|
|
23
26
|
let databaseCtor = null;
|
|
24
27
|
function loadDatabase() {
|
|
@@ -27,6 +30,39 @@ function loadDatabase() {
|
|
|
27
30
|
}
|
|
28
31
|
const SCHEMA_SHAPE_CACHE_LIMIT = 64;
|
|
29
32
|
const schemaShapeCandidateCache = new Map();
|
|
33
|
+
/**
|
|
34
|
+
* Extract single-column `unique` dbt tests, keyed by model name (W5.3). A column
|
|
35
|
+
* a `unique` test guards is a grain key, which feeds the grain ledger's fan-out
|
|
36
|
+
* detection — the common way real dbt repos declare keys. Handles both the
|
|
37
|
+
* `attached_node` (newer dbt) and `depends_on` shapes, and top-level or kwargs
|
|
38
|
+
* column names.
|
|
39
|
+
*/
|
|
40
|
+
export function extractDbtUniqueColumns(nodes) {
|
|
41
|
+
const byModel = new Map();
|
|
42
|
+
for (const node of Object.values(nodes)) {
|
|
43
|
+
if (node.resource_type !== 'test')
|
|
44
|
+
continue;
|
|
45
|
+
const testMetadata = node.test_metadata;
|
|
46
|
+
if (stringValue(testMetadata?.name) !== 'unique')
|
|
47
|
+
continue;
|
|
48
|
+
const column = stringValue(node.column_name) ?? stringValue(testMetadata?.kwargs?.column_name);
|
|
49
|
+
if (!column)
|
|
50
|
+
continue;
|
|
51
|
+
const dependsOn = node.depends_on?.nodes;
|
|
52
|
+
const attached = stringValue(node.attached_node)
|
|
53
|
+
?? (Array.isArray(dependsOn) ? dependsOn.map(String).find((ref) => ref.startsWith('model.')) : undefined);
|
|
54
|
+
if (!attached)
|
|
55
|
+
continue;
|
|
56
|
+
const modelName = attached.split('.').at(-1);
|
|
57
|
+
if (!modelName)
|
|
58
|
+
continue;
|
|
59
|
+
const existing = byModel.get(modelName) ?? [];
|
|
60
|
+
if (!existing.includes(column))
|
|
61
|
+
existing.push(column);
|
|
62
|
+
byModel.set(modelName, existing);
|
|
63
|
+
}
|
|
64
|
+
return byModel;
|
|
65
|
+
}
|
|
30
66
|
const OBJECT_PRIORITY = {
|
|
31
67
|
dql_block: 1,
|
|
32
68
|
dql_block_output: 1.5,
|
|
@@ -74,7 +110,15 @@ export async function ensureMetadataCatalogFresh(projectRoot, options = {}) {
|
|
|
74
110
|
fingerprint: snapshot.fingerprint,
|
|
75
111
|
};
|
|
76
112
|
}
|
|
77
|
-
|
|
113
|
+
// W3.4 — incremental reindex when prior state exists (only re-tokenizes changed
|
|
114
|
+
// sources' FTS); `force` does a clean full rebuild. Proven equal to a full
|
|
115
|
+
// rebuild by incremental-reindex.test.ts.
|
|
116
|
+
if (options.force) {
|
|
117
|
+
catalog.rebuild(snapshot);
|
|
118
|
+
}
|
|
119
|
+
else {
|
|
120
|
+
catalog.rebuildIncremental(snapshot);
|
|
121
|
+
}
|
|
78
122
|
return {
|
|
79
123
|
path: defaultMetadataPath(projectRoot),
|
|
80
124
|
refreshed: true,
|
|
@@ -221,7 +265,7 @@ export async function buildLocalContextPack(projectRoot, request) {
|
|
|
221
265
|
const trustLabel = deriveTrust(objects);
|
|
222
266
|
const citations = buildCitations(objects, contextEdges);
|
|
223
267
|
const evidenceSummaries = buildEvidenceSummaries(objects, contextEdges, queryRuns, diagnostics);
|
|
224
|
-
const allowedSqlContext = sortAllowedSqlContextForAnalysisPlan(buildAllowedSqlContext(objects, contextEdges), questionPlan);
|
|
268
|
+
const allowedSqlContext = sortAllowedSqlContextForAnalysisPlan(buildAllowedSqlContext(objects, contextEdges, tokenizeQuestionForColumns(request.question)), questionPlan);
|
|
225
269
|
const selectedRelations = allowedSqlContext.relations.slice(0, 24).map((relation, index) => {
|
|
226
270
|
const scored = scoreAllowedSqlRelationWithAnalysisPlan(relation, questionPlan);
|
|
227
271
|
return {
|
|
@@ -362,6 +406,16 @@ export function recordRuntimeSchemaSnapshot(projectRoot, snapshot) {
|
|
|
362
406
|
catalog.close();
|
|
363
407
|
}
|
|
364
408
|
}
|
|
409
|
+
/** The most-recent stored live-warehouse schema snapshot for a project, or null. */
|
|
410
|
+
export function latestRuntimeSchemaSnapshotForProject(projectRoot) {
|
|
411
|
+
const catalog = openMetadataCatalog(projectRoot);
|
|
412
|
+
try {
|
|
413
|
+
return catalog.latestRuntimeSchemaSnapshot();
|
|
414
|
+
}
|
|
415
|
+
finally {
|
|
416
|
+
catalog.close();
|
|
417
|
+
}
|
|
418
|
+
}
|
|
365
419
|
export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer) {
|
|
366
420
|
const manifestGraph = buildKGFromManifest(manifest);
|
|
367
421
|
const semanticGraph = buildKGFromSemanticLayer(semanticLayer);
|
|
@@ -643,23 +697,130 @@ export class MetadataCatalog {
|
|
|
643
697
|
});
|
|
644
698
|
txn();
|
|
645
699
|
}
|
|
700
|
+
/**
|
|
701
|
+
* Incremental reindex (W3.4). Only re-inserts objects + FTS rows for sources
|
|
702
|
+
* whose per-source fingerprint changed (or vanished); unchanged sources' rows are
|
|
703
|
+
* left in place (skipping the expensive FTS re-tokenization at scale). Edges,
|
|
704
|
+
* diagnostics, domain shards, and sourceless objects are always rebuilt because
|
|
705
|
+
* they cross source boundaries. Provably equal to a full rebuild for the semantic
|
|
706
|
+
* content (object keys/types/names/domains/payloads + FTS + edges); only the
|
|
707
|
+
* `updated_at`/`built_at` timestamps of untouched rows differ. Falls back to a
|
|
708
|
+
* full rebuild when there is no prior fingerprint state.
|
|
709
|
+
*/
|
|
710
|
+
rebuildIncremental(snapshot) {
|
|
711
|
+
const stored = new Map();
|
|
712
|
+
for (const row of this.db.prepare('SELECT source_path, fingerprint FROM metadata_source_fingerprints').all()) {
|
|
713
|
+
stored.set(row.source_path, row.fingerprint);
|
|
714
|
+
}
|
|
715
|
+
if (stored.size === 0) {
|
|
716
|
+
this.rebuild(snapshot);
|
|
717
|
+
return { mode: 'full', changedSources: 0 };
|
|
718
|
+
}
|
|
719
|
+
const now = new Date().toISOString();
|
|
720
|
+
const incoming = buildSourceFingerprints(snapshot.objects, now);
|
|
721
|
+
const incomingSources = new Set(incoming.map((f) => f.sourcePath));
|
|
722
|
+
const changedSources = new Set();
|
|
723
|
+
for (const fingerprint of incoming) {
|
|
724
|
+
if (stored.get(fingerprint.sourcePath) !== fingerprint.fingerprint)
|
|
725
|
+
changedSources.add(fingerprint.sourcePath);
|
|
726
|
+
}
|
|
727
|
+
const removedSources = [...stored.keys()].filter((source) => !incomingSources.has(source));
|
|
728
|
+
const sourceOf = (object) => object.sourcePath ?? object.sourceSystem ?? null;
|
|
729
|
+
const domainShards = buildDomainShards(snapshot.objects, now);
|
|
730
|
+
const insertObject = this.db.prepare(`
|
|
731
|
+
INSERT INTO metadata_objects (
|
|
732
|
+
object_key, object_type, name, full_name, domain, owner, status,
|
|
733
|
+
description, source_path, source_system, payload_json, updated_at
|
|
734
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
735
|
+
const insertFts = this.db.prepare(`
|
|
736
|
+
INSERT INTO metadata_fts (object_key, name, full_name, description, domain, owner, payload)
|
|
737
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
738
|
+
const insertEdge = this.db.prepare(`
|
|
739
|
+
INSERT OR IGNORE INTO metadata_edges (edge_type, from_key, to_key, confidence, payload_json, created_at)
|
|
740
|
+
VALUES (?, ?, ?, ?, ?, ?)`);
|
|
741
|
+
const insertDiagnostic = this.db.prepare(`
|
|
742
|
+
INSERT INTO metadata_diagnostics (id, kind, severity, message, object_key, file_path, created_at)
|
|
743
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
744
|
+
const insertSourceFingerprint = this.db.prepare(`
|
|
745
|
+
INSERT OR REPLACE INTO metadata_source_fingerprints (source_path, fingerprint, object_count, updated_at)
|
|
746
|
+
VALUES (?, ?, ?, ?)`);
|
|
747
|
+
const insertDomainShard = this.db.prepare(`
|
|
748
|
+
INSERT OR REPLACE INTO metadata_domain_shards (
|
|
749
|
+
domain, object_count, block_count, certified_block_count,
|
|
750
|
+
semantic_metric_count, dbt_object_count, updated_at
|
|
751
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)`);
|
|
752
|
+
const setState = this.db.prepare('INSERT OR REPLACE INTO metadata_state (key, value) VALUES (?, ?)');
|
|
753
|
+
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) = ?)');
|
|
754
|
+
const deleteObjectsForSource = this.db.prepare('DELETE FROM metadata_objects WHERE COALESCE(source_path, source_system) = ?');
|
|
755
|
+
const txn = this.db.transaction(() => {
|
|
756
|
+
// Drop changed + removed sources' rows, and ALL sourceless rows (never
|
|
757
|
+
// fingerprinted, so always refreshed).
|
|
758
|
+
for (const source of [...changedSources, ...removedSources]) {
|
|
759
|
+
deleteFtsForSource.run(source);
|
|
760
|
+
deleteObjectsForSource.run(source);
|
|
761
|
+
}
|
|
762
|
+
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();
|
|
763
|
+
this.db.prepare('DELETE FROM metadata_objects WHERE source_path IS NULL AND source_system IS NULL').run();
|
|
764
|
+
// Re-insert only objects from changed sources or sourceless; unchanged
|
|
765
|
+
// sources' rows are left untouched.
|
|
766
|
+
for (const object of snapshot.objects) {
|
|
767
|
+
const source = sourceOf(object);
|
|
768
|
+
if (source !== null && !changedSources.has(source))
|
|
769
|
+
continue;
|
|
770
|
+
const payload = object.payload ?? {};
|
|
771
|
+
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);
|
|
772
|
+
insertFts.run(object.objectKey, object.name, object.fullName ?? '', object.description ?? '', object.domain ?? '', object.owner ?? '', JSON.stringify(payload));
|
|
773
|
+
}
|
|
774
|
+
// Cross-source tables: rebuild fully.
|
|
775
|
+
this.db.prepare('DELETE FROM metadata_edges').run();
|
|
776
|
+
for (const edge of snapshot.edges) {
|
|
777
|
+
insertEdge.run(edge.edgeType, edge.fromKey, edge.toKey, edge.confidence ?? 1, JSON.stringify(edge.payload ?? {}), now);
|
|
778
|
+
}
|
|
779
|
+
this.db.prepare('DELETE FROM metadata_diagnostics').run();
|
|
780
|
+
for (const diagnostic of snapshot.diagnostics) {
|
|
781
|
+
insertDiagnostic.run(diagnosticId(diagnostic), diagnostic.kind, diagnostic.severity, diagnostic.message, diagnostic.objectKey ?? null, diagnostic.filePath ?? null, now);
|
|
782
|
+
}
|
|
783
|
+
this.db.prepare('DELETE FROM metadata_domain_shards').run();
|
|
784
|
+
for (const shard of domainShards) {
|
|
785
|
+
insertDomainShard.run(shard.domain, shard.objectCount, shard.blockCount, shard.certifiedBlockCount, shard.semanticMetricCount, shard.dbtObjectCount, shard.updatedAt);
|
|
786
|
+
}
|
|
787
|
+
for (const source of removedSources) {
|
|
788
|
+
this.db.prepare('DELETE FROM metadata_source_fingerprints WHERE source_path = ?').run(source);
|
|
789
|
+
}
|
|
790
|
+
for (const fingerprint of incoming) {
|
|
791
|
+
insertSourceFingerprint.run(fingerprint.sourcePath, fingerprint.fingerprint, fingerprint.objectCount, fingerprint.updatedAt);
|
|
792
|
+
}
|
|
793
|
+
setState.run('built_at', now);
|
|
794
|
+
setState.run('fingerprint', snapshot.fingerprint);
|
|
795
|
+
setState.run('project_root', snapshot.projectRoot);
|
|
796
|
+
setState.run('object_count', String(snapshot.objects.length));
|
|
797
|
+
setState.run('edge_count', String(snapshot.edges.length));
|
|
798
|
+
setState.run('source_fingerprint_count', String(incoming.length));
|
|
799
|
+
setState.run('domain_shard_count', String(domainShards.length));
|
|
800
|
+
setState.run('diagnostics_json', JSON.stringify(snapshot.diagnostics));
|
|
801
|
+
setState.run('compile_conflicts_json', JSON.stringify(snapshot.compileConflicts ?? []));
|
|
802
|
+
setState.run('manifest_generated_at', snapshot.manifest.generatedAt);
|
|
803
|
+
});
|
|
804
|
+
txn();
|
|
805
|
+
return { mode: 'incremental', changedSources: changedSources.size };
|
|
806
|
+
}
|
|
646
807
|
searchObjects(options) {
|
|
647
808
|
const { query, objectTypes, domain, limit = 40 } = options;
|
|
648
|
-
const
|
|
649
|
-
if (!
|
|
809
|
+
const match = buildFtsMatch(query, { prefix: true });
|
|
810
|
+
if (!match.or)
|
|
650
811
|
return this.listObjects({ objectTypes, domain, limit });
|
|
651
812
|
const filters = [];
|
|
652
|
-
const
|
|
813
|
+
const extraParams = [];
|
|
653
814
|
if (objectTypes && objectTypes.length > 0) {
|
|
654
815
|
filters.push(`o.object_type IN (${objectTypes.map(() => '?').join(', ')})`);
|
|
655
|
-
|
|
816
|
+
extraParams.push(...objectTypes);
|
|
656
817
|
}
|
|
657
818
|
if (domain) {
|
|
658
819
|
filters.push('o.domain = ?');
|
|
659
|
-
|
|
820
|
+
extraParams.push(domain);
|
|
660
821
|
}
|
|
661
822
|
const whereExtra = filters.length > 0 ? ` AND ${filters.join(' AND ')}` : '';
|
|
662
|
-
const
|
|
823
|
+
const runMatch = (matchExpr) => this.db.prepare(`
|
|
663
824
|
SELECT o.*,
|
|
664
825
|
bm25(metadata_fts) AS rank,
|
|
665
826
|
snippet(metadata_fts, -1, '<mark>', '</mark>', '...', 12) AS snip
|
|
@@ -668,7 +829,15 @@ export class MetadataCatalog {
|
|
|
668
829
|
WHERE metadata_fts MATCH ?${whereExtra}
|
|
669
830
|
ORDER BY rank
|
|
670
831
|
LIMIT ?
|
|
671
|
-
`).all(...
|
|
832
|
+
`).all(matchExpr, ...extraParams, limit);
|
|
833
|
+
// Precision-first, recall-preserving UNION: all-terms-co-occur (AND) matches
|
|
834
|
+
// lead, then OR-of-terms matches fill the remaining budget. A doc mentioning
|
|
835
|
+
// only one term (a relevant context column) still surfaces, while a doc
|
|
836
|
+
// containing every term ranks ahead of it. Dedup by object_key.
|
|
837
|
+
const andRows = match.and ? runMatch(match.and) : [];
|
|
838
|
+
const seen = new Set(andRows.map((row) => row.object_key));
|
|
839
|
+
const orRows = runMatch(match.or).filter((row) => !seen.has(row.object_key));
|
|
840
|
+
const rows = [...andRows, ...orRows].slice(0, limit);
|
|
672
841
|
return rows.map((row) => ({
|
|
673
842
|
...rowToObject(row),
|
|
674
843
|
score: row.rank ? 1 / (1 + Math.max(0, row.rank)) : 1,
|
|
@@ -839,6 +1008,14 @@ export class MetadataCatalog {
|
|
|
839
1008
|
`);
|
|
840
1009
|
const txn = this.db.transaction(() => {
|
|
841
1010
|
insertSnapshot.run(id, cleanSnapshot.source ?? null, JSON.stringify(cleanSnapshot), capturedAt);
|
|
1011
|
+
// Only the most-recent snapshot is ever read; rows otherwise accumulate one per
|
|
1012
|
+
// question forever. Keep a small margin and prune the rest (P7).
|
|
1013
|
+
this.db.prepare(`
|
|
1014
|
+
DELETE FROM runtime_schema_snapshots
|
|
1015
|
+
WHERE id NOT IN (
|
|
1016
|
+
SELECT id FROM runtime_schema_snapshots ORDER BY captured_at DESC LIMIT 5
|
|
1017
|
+
)
|
|
1018
|
+
`).run();
|
|
842
1019
|
this.db.prepare('DELETE FROM runtime_value_fts').run();
|
|
843
1020
|
this.db.prepare('DELETE FROM runtime_value_index').run();
|
|
844
1021
|
for (const entry of valueEntries) {
|
|
@@ -853,7 +1030,9 @@ export class MetadataCatalog {
|
|
|
853
1030
|
}
|
|
854
1031
|
searchRuntimeValues(terms, limit = 40) {
|
|
855
1032
|
const cleanTerms = uniqueStrings(terms.map(normalizeValueIndexText).filter(Boolean)).slice(0, 8);
|
|
856
|
-
|
|
1033
|
+
// Filter VALUES may legitimately be stop words (a category named "current");
|
|
1034
|
+
// fall back to raw tokens so value binding still fires rather than dropping to empty.
|
|
1035
|
+
const query = sanitizeFtsQuery(cleanTerms.join(' '), { prefix: true, fallbackToRawTokens: true });
|
|
857
1036
|
if (!query)
|
|
858
1037
|
return [];
|
|
859
1038
|
const rows = this.db.prepare(`
|
|
@@ -1197,6 +1376,7 @@ function addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges,
|
|
|
1197
1376
|
return;
|
|
1198
1377
|
}
|
|
1199
1378
|
const catalogColumns = loadRawDbtCatalogColumns(join(dirname(manifestPath), 'catalog.json'));
|
|
1379
|
+
const uniqueColumnsByModel = extractDbtUniqueColumns(raw.nodes ?? {});
|
|
1200
1380
|
const entries = [];
|
|
1201
1381
|
for (const [uniqueId, node] of Object.entries(raw.nodes ?? {})) {
|
|
1202
1382
|
if (node.resource_type !== 'model')
|
|
@@ -1216,6 +1396,7 @@ function addRawDbtManifestCatalogObjects(projectRoot, manifest, objects, edges,
|
|
|
1216
1396
|
database,
|
|
1217
1397
|
schema,
|
|
1218
1398
|
catalogColumns: catalogColumns.get(uniqueId),
|
|
1399
|
+
uniqueColumns: uniqueColumnsByModel.get(name),
|
|
1219
1400
|
};
|
|
1220
1401
|
entries.push(entry);
|
|
1221
1402
|
addRawDbtCatalogObject({
|
|
@@ -1285,6 +1466,8 @@ function addRawDbtCatalogObject(input) {
|
|
|
1285
1466
|
catalogOnly: existing ? undefined : true,
|
|
1286
1467
|
columnCompleteness,
|
|
1287
1468
|
columns,
|
|
1469
|
+
// W5.3 — single-column `unique` dbt tests become grain-ledger keys.
|
|
1470
|
+
uniqueColumns: input.uniqueColumns && input.uniqueColumns.length > 0 ? input.uniqueColumns : undefined,
|
|
1288
1471
|
}),
|
|
1289
1472
|
}));
|
|
1290
1473
|
for (const column of columns) {
|
|
@@ -1659,6 +1842,10 @@ function addDataLexManifestObjects(projectRoot, manifest, objects, edges, diagno
|
|
|
1659
1842
|
payload: compactObject({
|
|
1660
1843
|
tags: entity.tags ?? [],
|
|
1661
1844
|
binding: entity.binding,
|
|
1845
|
+
// Entity-level grain + keys (W5.2) feed the grain ledger for fan-out safety.
|
|
1846
|
+
grain: entity.grain,
|
|
1847
|
+
candidateKeys: entity.candidate_keys,
|
|
1848
|
+
businessKeys: entity.business_keys,
|
|
1662
1849
|
fields: (entity.fields ?? []).slice(0, 100).map((field) => compactObject({
|
|
1663
1850
|
name: field.name,
|
|
1664
1851
|
type: field.type,
|
|
@@ -1744,6 +1931,58 @@ function addDataLexManifestObjects(projectRoot, manifest, objects, edges, diagno
|
|
|
1744
1931
|
}),
|
|
1745
1932
|
});
|
|
1746
1933
|
}
|
|
1934
|
+
// Concept-to-physical conformance (W5.1) → searchable `datalex_concept` nodes
|
|
1935
|
+
// carrying the canonical key + the physical relations that realize the concept,
|
|
1936
|
+
// plus `datalex_conforms` edges so retrieval can collapse several physical tables
|
|
1937
|
+
// into one business concept ("Customer" → dim_customer, stg_customers) and reason
|
|
1938
|
+
// at the concept grain. Modeling metadata only — no certification coupling.
|
|
1939
|
+
for (const conformance of raw.conformance ?? []) {
|
|
1940
|
+
if (!conformance?.concept)
|
|
1941
|
+
continue;
|
|
1942
|
+
const conceptDomain = conformance.domain ?? '';
|
|
1943
|
+
const conceptKey = `datalex:concept:${conceptDomain}.${conformance.concept}`;
|
|
1944
|
+
const physicalRefs = (conformance.physical ?? [])
|
|
1945
|
+
.map((physical) => (physical.binding ? objectKeyForDataLexBinding(physical.binding) : undefined))
|
|
1946
|
+
.filter((key) => Boolean(key));
|
|
1947
|
+
const canonicalKey = conformance.canonical_key ?? [];
|
|
1948
|
+
objects.set(conceptKey, mergeObject(objects.get(conceptKey), {
|
|
1949
|
+
objectKey: conceptKey,
|
|
1950
|
+
objectType: 'datalex_concept',
|
|
1951
|
+
name: conformance.concept,
|
|
1952
|
+
fullName: `${conceptDomain}.${conformance.concept}`,
|
|
1953
|
+
domain: conceptDomain || undefined,
|
|
1954
|
+
status: 'contract_evidence',
|
|
1955
|
+
description: `Business concept ${conformance.concept}`
|
|
1956
|
+
+ (canonicalKey.length ? ` (canonical key: ${canonicalKey.join(', ')})` : '')
|
|
1957
|
+
+ (physicalRefs.length ? `, realized by ${physicalRefs.length} physical relation(s).` : '.'),
|
|
1958
|
+
sourcePath: manifestPath,
|
|
1959
|
+
sourceSystem: 'DataLex manifest',
|
|
1960
|
+
payload: compactObject({
|
|
1961
|
+
canonicalKey,
|
|
1962
|
+
businessKey: conformance.business_key ?? [],
|
|
1963
|
+
implements: conformance.implements ?? [],
|
|
1964
|
+
physicalRefs,
|
|
1965
|
+
}),
|
|
1966
|
+
}));
|
|
1967
|
+
if (conceptDomain) {
|
|
1968
|
+
putEdge(edges, {
|
|
1969
|
+
edgeType: 'contains',
|
|
1970
|
+
fromKey: `datalex:domain:${conceptDomain}`,
|
|
1971
|
+
toKey: conceptKey,
|
|
1972
|
+
confidence: 1,
|
|
1973
|
+
payload: { source: 'datalex conformance' },
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
for (const ref of physicalRefs) {
|
|
1977
|
+
putEdge(edges, {
|
|
1978
|
+
edgeType: 'datalex_conforms',
|
|
1979
|
+
fromKey: conceptKey,
|
|
1980
|
+
toKey: ref,
|
|
1981
|
+
confidence: 0.95,
|
|
1982
|
+
payload: compactObject({ source: 'datalex conformance', concept: conformance.concept, canonicalKey }),
|
|
1983
|
+
});
|
|
1984
|
+
}
|
|
1985
|
+
}
|
|
1747
1986
|
for (const block of Object.values(manifest.blocks ?? {})) {
|
|
1748
1987
|
if (!block.datalexContract)
|
|
1749
1988
|
continue;
|
|
@@ -2056,15 +2295,30 @@ async function planContextPackRoute(input) {
|
|
|
2056
2295
|
const exactByApplicability = [...applicabilityByKey.values()]
|
|
2057
2296
|
.filter((item) => item.kind === 'exact_answer' || item.kind === 'safe_parameterized')
|
|
2058
2297
|
.sort((a, b) => b.score - a.score)[0];
|
|
2059
|
-
|
|
2298
|
+
let exact = exactByApplicability
|
|
2060
2299
|
? input.objects.find((object) => object.objectKey === exactByApplicability.objectKey)
|
|
2061
2300
|
: findExactCertifiedObject(input.request.question, intent, input.objects);
|
|
2062
|
-
const certifiedApplicability = exact
|
|
2063
|
-
? applicabilityByKey.get(exact.objectKey) ?? certifiedApplicabilityForObject(exact, input.questionPlan)
|
|
2064
|
-
: undefined;
|
|
2065
2301
|
const contextApplicability = [...applicabilityByKey.values()]
|
|
2066
2302
|
.filter((item) => item.kind === 'context_only')
|
|
2067
2303
|
.sort((a, b) => b.score - a.score)[0];
|
|
2304
|
+
// W2.1 — paraphrase promotion. When no block string/lexically matched, promote a
|
|
2305
|
+
// top context-only certified candidate whose example QUESTION the user paraphrased
|
|
2306
|
+
// (semantic cosine + direction) to the certified candidate. It still runs the full
|
|
2307
|
+
// shape/grain fit below (paraphraseExampleMatch is kept OUT of directCertifiedBypass,
|
|
2308
|
+
// the fit's exactExampleMatch, and the grain-gate skip) — so paraphrase never bypasses
|
|
2309
|
+
// grain the way a user naming the block directly does.
|
|
2310
|
+
let paraphraseExampleMatch = false;
|
|
2311
|
+
if (!exact && contextApplicability) {
|
|
2312
|
+
const candidate = input.objects.find((object) => object.objectKey === contextApplicability.objectKey);
|
|
2313
|
+
if (candidate && isCertifiedMetadataObject(candidate)
|
|
2314
|
+
&& await matchExampleParaphrase(input.request.question, candidate, envEmbeddingProvider())) {
|
|
2315
|
+
exact = candidate;
|
|
2316
|
+
paraphraseExampleMatch = true;
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
const certifiedApplicability = exact
|
|
2320
|
+
? applicabilityByKey.get(exact.objectKey) ?? certifiedApplicabilityForObject(exact, input.questionPlan)
|
|
2321
|
+
: undefined;
|
|
2068
2322
|
const exactExampleMatch = exact ? hasExactExampleQuestion(input.request.question, exact) : false;
|
|
2069
2323
|
const directCertifiedBypass = Boolean(exact && (exactExampleMatch
|
|
2070
2324
|
|| intent === 'definition_lookup'
|
|
@@ -2127,6 +2381,7 @@ async function planContextPackRoute(input) {
|
|
|
2127
2381
|
|| intent === 'exact_certified_lookup'
|
|
2128
2382
|
|| intent === 'definition_lookup'
|
|
2129
2383
|
|| exactExampleMatch
|
|
2384
|
+
|| paraphraseExampleMatch
|
|
2130
2385
|
|| (intent === 'ad_hoc_ranking' && objectNameInQuestion(input.request.question, exact)))) {
|
|
2131
2386
|
return {
|
|
2132
2387
|
route: 'certified',
|
|
@@ -3006,6 +3261,55 @@ function isSqlParentObject(object) {
|
|
|
3006
3261
|
object.objectType === 'warehouse_table' ||
|
|
3007
3262
|
object.objectType === 'runtime_table';
|
|
3008
3263
|
}
|
|
3264
|
+
/** Column budget for a single relation in the prompt's allowed-SQL context. */
|
|
3265
|
+
const MAX_ALLOWED_SQL_COLUMNS = 120;
|
|
3266
|
+
/**
|
|
3267
|
+
* Relevance score for keeping a column when a wide relation must be truncated (W3.3).
|
|
3268
|
+
* Structural columns (join keys, time/grain) always score so joins and grain survive;
|
|
3269
|
+
* a column whose name overlaps the question scores highest so the needed column on a
|
|
3270
|
+
* 300-column table is not dropped just because it sits past the budget cut.
|
|
3271
|
+
*/
|
|
3272
|
+
function columnBudgetScore(name, questionTokens) {
|
|
3273
|
+
const lower = name.toLowerCase();
|
|
3274
|
+
let score = 0;
|
|
3275
|
+
if (/(^|_)(id|key)$/.test(lower) || lower === 'id')
|
|
3276
|
+
score += 3;
|
|
3277
|
+
if (/(date|time|_at$|_ts$|day|month|year|week|quarter)/.test(lower))
|
|
3278
|
+
score += 2;
|
|
3279
|
+
if (questionTokens) {
|
|
3280
|
+
for (const token of questionTokens) {
|
|
3281
|
+
if (token.length > 2 && lower.includes(token)) {
|
|
3282
|
+
score += 5;
|
|
3283
|
+
break;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
return score;
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* Cap a relation's column list to the prompt budget and downgrade completeness to
|
|
3291
|
+
* 'partial' when columns are dropped. Keeping a truncated relation 'complete' would
|
|
3292
|
+
* let column validation false-positive a valid column past the cut as unknown_column
|
|
3293
|
+
* (the >120-column latent bug, W1.4). When truncating, keep the columns most relevant
|
|
3294
|
+
* to the question + structural keys (W3.3) rather than the arbitrary first-N, then
|
|
3295
|
+
* restore original order among the survivors so prompt ordering is unchanged.
|
|
3296
|
+
*/
|
|
3297
|
+
function capAllowedSqlColumns(columns, completeness, questionTokens) {
|
|
3298
|
+
if (columns.length <= MAX_ALLOWED_SQL_COLUMNS) {
|
|
3299
|
+
return { columns, columnCompleteness: completeness };
|
|
3300
|
+
}
|
|
3301
|
+
const kept = columns
|
|
3302
|
+
.map((column, index) => ({ column, index, score: columnBudgetScore(column.name, questionTokens) }))
|
|
3303
|
+
.sort((a, b) => (b.score - a.score) || (a.index - b.index))
|
|
3304
|
+
.slice(0, MAX_ALLOWED_SQL_COLUMNS)
|
|
3305
|
+
.sort((a, b) => a.index - b.index)
|
|
3306
|
+
.map((entry) => entry.column);
|
|
3307
|
+
return { columns: kept, columnCompleteness: 'partial' };
|
|
3308
|
+
}
|
|
3309
|
+
/** Lowercased word tokens from the question, for column-relevance ranking. */
|
|
3310
|
+
function tokenizeQuestionForColumns(question) {
|
|
3311
|
+
return (question.toLowerCase().match(/[a-z0-9_]+/g) ?? []).filter((token) => token.length > 2);
|
|
3312
|
+
}
|
|
3009
3313
|
const FULL_CATALOG_RELATION_TYPES = ['dbt_model', 'dbt_source', 'warehouse_table', 'runtime_table'];
|
|
3010
3314
|
const FULL_CATALOG_COLUMN_TYPES = ['dbt_column', 'warehouse_column', 'runtime_column'];
|
|
3011
3315
|
/** Deep mode over a catalog this small hands the model everything instead of top-k. */
|
|
@@ -3032,7 +3336,7 @@ function collectFullCatalogObjects(catalog) {
|
|
|
3032
3336
|
return undefined;
|
|
3033
3337
|
return mergeObjects([...relations, ...columns]);
|
|
3034
3338
|
}
|
|
3035
|
-
function buildAllowedSqlContext(objects, edges) {
|
|
3339
|
+
export function buildAllowedSqlContext(objects, edges, questionTokens) {
|
|
3036
3340
|
const byRelation = new Map();
|
|
3037
3341
|
const objectsByKey = new Map(objects.map((object) => [object.objectKey, object]));
|
|
3038
3342
|
const addRelation = (relation) => {
|
|
@@ -3041,16 +3345,17 @@ function buildAllowedSqlContext(objects, edges) {
|
|
|
3041
3345
|
return;
|
|
3042
3346
|
const existing = byRelation.get(key);
|
|
3043
3347
|
if (!existing) {
|
|
3044
|
-
|
|
3348
|
+
const capped = capAllowedSqlColumns(dedupeRuntimeColumns(relation.columns), relation.columnCompleteness, questionTokens);
|
|
3349
|
+
byRelation.set(key, { ...relation, columns: capped.columns, columnCompleteness: capped.columnCompleteness });
|
|
3045
3350
|
return;
|
|
3046
3351
|
}
|
|
3047
|
-
const
|
|
3352
|
+
const capped = capAllowedSqlColumns(mergeRelationColumns(existing, relation), mergeRelationCompletenessForCatalog(existing, relation), questionTokens);
|
|
3048
3353
|
byRelation.set(key, {
|
|
3049
3354
|
...existing,
|
|
3050
3355
|
objectKey: existing.objectKey ?? relation.objectKey,
|
|
3051
3356
|
source: mergeRelationSources(existing.source, relation.source),
|
|
3052
|
-
columnCompleteness,
|
|
3053
|
-
columns:
|
|
3357
|
+
columnCompleteness: capped.columnCompleteness,
|
|
3358
|
+
columns: capped.columns,
|
|
3054
3359
|
});
|
|
3055
3360
|
};
|
|
3056
3361
|
for (const object of objects) {
|
|
@@ -3500,12 +3805,13 @@ function coalesceRelationAliases(relations) {
|
|
|
3500
3805
|
continue;
|
|
3501
3806
|
const target = targets[0];
|
|
3502
3807
|
const targetKey = normalizeRelationKey(target.relation);
|
|
3808
|
+
const capped = capAllowedSqlColumns(mergeRelationColumns(target, relation), mergeRelationCompletenessForCatalog(target, relation));
|
|
3503
3809
|
mergedByRelation.set(targetKey, {
|
|
3504
3810
|
...target,
|
|
3505
3811
|
objectKey: target.objectKey ?? relation.objectKey,
|
|
3506
3812
|
source: mergeRelationSources(target.source, relation.source),
|
|
3507
|
-
columnCompleteness:
|
|
3508
|
-
columns:
|
|
3813
|
+
columnCompleteness: capped.columnCompleteness,
|
|
3814
|
+
columns: capped.columns,
|
|
3509
3815
|
});
|
|
3510
3816
|
skipped.add(relationKey);
|
|
3511
3817
|
}
|
|
@@ -4273,10 +4579,39 @@ function safeJson(raw, fallback) {
|
|
|
4273
4579
|
return fallback;
|
|
4274
4580
|
}
|
|
4275
4581
|
}
|
|
4582
|
+
/**
|
|
4583
|
+
* W3.2 — domain-affinity ranking boost for MULTI-DOMAIN catalogs. At scale
|
|
4584
|
+
* (thousands of models across domains), a question about one domain can be crowded
|
|
4585
|
+
* out of the top-k by lexically-similar objects from other domains. This infers the
|
|
4586
|
+
* dominant domain from the highest-scoring candidates and gives same-domain objects
|
|
4587
|
+
* a small additive bonus. It is RECALL-PRESERVING — it only reorders; every
|
|
4588
|
+
* candidate stays in the ranking, and the bonus (10% of the top score) is small
|
|
4589
|
+
* enough that a much stronger cross-domain signal still wins. No-op for single-domain
|
|
4590
|
+
* catalogs (fewer than 2 domains carry positive score).
|
|
4591
|
+
*/
|
|
4592
|
+
export function applyDomainAffinityBoost(scored) {
|
|
4593
|
+
const domainScore = new Map();
|
|
4594
|
+
for (const item of scored) {
|
|
4595
|
+
const domain = item.row.domain;
|
|
4596
|
+
if (domain && item.score > 0)
|
|
4597
|
+
domainScore.set(domain, (domainScore.get(domain) ?? 0) + item.score);
|
|
4598
|
+
}
|
|
4599
|
+
if (domainScore.size < 2)
|
|
4600
|
+
return undefined;
|
|
4601
|
+
const [dominant] = [...domainScore.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
4602
|
+
const maxScore = Math.max(0, ...scored.map((item) => item.score));
|
|
4603
|
+
const bonus = Number((0.1 * maxScore).toFixed(3));
|
|
4604
|
+
if (bonus <= 0)
|
|
4605
|
+
return undefined;
|
|
4606
|
+
for (const item of scored) {
|
|
4607
|
+
if (item.row.domain === dominant)
|
|
4608
|
+
item.score = Number((item.score + bonus).toFixed(3));
|
|
4609
|
+
}
|
|
4610
|
+
return dominant;
|
|
4611
|
+
}
|
|
4276
4612
|
function rankMetadataObjects(args) {
|
|
4277
4613
|
const terms = tokenize(args.question).slice(0, 12);
|
|
4278
|
-
const
|
|
4279
|
-
.map((row) => {
|
|
4614
|
+
const scored = mergeObjects(args.rows).map((row) => {
|
|
4280
4615
|
const baseScore = scoreMetadataObject(row, terms);
|
|
4281
4616
|
const planScore = args.questionPlan ? scoreMetadataObjectWithAnalysisPlan(row, args.questionPlan) : { score: 0, reasons: [] };
|
|
4282
4617
|
const score = Number((baseScore + planScore.score).toFixed(3));
|
|
@@ -4287,7 +4622,9 @@ function rankMetadataObjects(args) {
|
|
|
4287
4622
|
reason: selectionReason(row, score, planScore.reasons),
|
|
4288
4623
|
priorityTier: priorityTier(row),
|
|
4289
4624
|
};
|
|
4290
|
-
})
|
|
4625
|
+
});
|
|
4626
|
+
applyDomainAffinityBoost(scored);
|
|
4627
|
+
const ranked = scored
|
|
4291
4628
|
.sort((a, b) => b.score - a.score || objectPriority(a.row) - objectPriority(b.row) || a.row.name.localeCompare(b.row.name))
|
|
4292
4629
|
.map((item, index) => ({ ...item, rank: index + 1 }));
|
|
4293
4630
|
const selectedRanked = selectRankedMetadataObjects(ranked, args.limit);
|
|
@@ -4633,21 +4970,4 @@ function scoreText(value, terms) {
|
|
|
4633
4970
|
const lower = value.toLowerCase();
|
|
4634
4971
|
return terms.reduce((score, term) => score + (lower.includes(term) ? 1 : 0), 0);
|
|
4635
4972
|
}
|
|
4636
|
-
const STOP_WORDS = new Set([
|
|
4637
|
-
'a', 'about', 'after', 'all', 'an', 'and', 'any', 'are', 'as', 'at', 'be', 'by',
|
|
4638
|
-
'can', 'could', 'did', 'do', 'does', 'down', 'for', 'from', 'give', 'had', 'has',
|
|
4639
|
-
'have', 'how', 'i', 'if', 'in', 'into', 'is', 'it', 'me', 'most', 'my', 'of',
|
|
4640
|
-
'on', 'or', 'please', 'query', 'show', 'sql', 'than', 'that', 'the', 'their',
|
|
4641
|
-
'them', 'this', 'to', 'up', 'using', 'was', 'we', 'were', 'what', 'when',
|
|
4642
|
-
'where', 'which', 'who', 'why', 'with', 'would', 'you', 'your',
|
|
4643
|
-
]);
|
|
4644
|
-
function sanitizeFtsQuery(raw) {
|
|
4645
|
-
return raw
|
|
4646
|
-
.split(/\s+/)
|
|
4647
|
-
.map((term) => term.replace(/[^\p{L}\p{N}_]/gu, ''))
|
|
4648
|
-
.filter((term) => term.length > 1 && !STOP_WORDS.has(term.toLowerCase()))
|
|
4649
|
-
.slice(0, 48)
|
|
4650
|
-
.map((term) => `"${term}"*`)
|
|
4651
|
-
.join(' OR ');
|
|
4652
|
-
}
|
|
4653
4973
|
//# sourceMappingURL=catalog.js.map
|