@duckcodeailabs/dql-agent 1.6.20 → 1.6.22
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 +287 -0
- package/dist/agent-run-engine.d.ts.map +1 -0
- package/dist/agent-run-engine.js +916 -0
- package/dist/agent-run-engine.js.map +1 -0
- package/dist/agent-run-gates.d.ts +26 -0
- package/dist/agent-run-gates.d.ts.map +1 -0
- package/dist/agent-run-gates.js +170 -0
- package/dist/agent-run-gates.js.map +1 -0
- package/dist/agent-run-planner.d.ts +32 -0
- package/dist/agent-run-planner.d.ts.map +1 -0
- package/dist/agent-run-planner.js +280 -0
- package/dist/agent-run-planner.js.map +1 -0
- package/dist/answer-loop.d.ts +49 -0
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +292 -17
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts +44 -2
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +558 -123
- package/dist/app-builder.js.map +1 -1
- package/dist/app-planner.d.ts +78 -0
- package/dist/app-planner.d.ts.map +1 -0
- package/dist/app-planner.js +145 -0
- package/dist/app-planner.js.map +1 -0
- package/dist/embeddings/provider.d.ts +66 -0
- package/dist/embeddings/provider.d.ts.map +1 -0
- package/dist/embeddings/provider.js +105 -0
- package/dist/embeddings/provider.js.map +1 -0
- package/dist/hints/git-store.d.ts +69 -0
- package/dist/hints/git-store.d.ts.map +1 -0
- package/dist/hints/git-store.js +244 -0
- package/dist/hints/git-store.js.map +1 -0
- package/dist/hints/retrieval.d.ts +49 -0
- package/dist/hints/retrieval.d.ts.map +1 -0
- package/dist/hints/retrieval.js +60 -0
- package/dist/hints/retrieval.js.map +1 -0
- package/dist/hints/store.d.ts +39 -0
- package/dist/hints/store.d.ts.map +1 -0
- package/dist/hints/store.js +279 -0
- package/dist/hints/store.js.map +1 -0
- package/dist/hints/types.d.ts +147 -0
- package/dist/hints/types.d.ts.map +1 -0
- package/dist/hints/types.js +92 -0
- package/dist/hints/types.js.map +1 -0
- package/dist/index.d.ts +43 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -3
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +63 -0
- package/dist/intent-controller.d.ts.map +1 -0
- package/dist/intent-controller.js +150 -0
- package/dist/intent-controller.js.map +1 -0
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +2 -0
- package/dist/kg/build.js.map +1 -1
- package/dist/kg/sqlite-fts.js +4 -0
- package/dist/kg/sqlite-fts.js.map +1 -1
- package/dist/kg/types.d.ts +10 -0
- package/dist/kg/types.d.ts.map +1 -1
- package/dist/metadata/catalog.d.ts +81 -4
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +208 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/drafts.d.ts +22 -0
- package/dist/metadata/drafts.d.ts.map +1 -1
- package/dist/metadata/drafts.js +126 -2
- package/dist/metadata/drafts.js.map +1 -1
- package/dist/metadata/grain-gate.d.ts +61 -0
- package/dist/metadata/grain-gate.d.ts.map +1 -0
- package/dist/metadata/grain-gate.js +265 -0
- package/dist/metadata/grain-gate.js.map +1 -0
- package/dist/metadata/identity.d.ts +47 -0
- package/dist/metadata/identity.d.ts.map +1 -0
- package/dist/metadata/identity.js +147 -0
- package/dist/metadata/identity.js.map +1 -0
- package/dist/metadata/metric-match.d.ts +93 -0
- package/dist/metadata/metric-match.d.ts.map +1 -0
- package/dist/metadata/metric-match.js +233 -0
- package/dist/metadata/metric-match.js.map +1 -0
- package/dist/metadata/sql-grounding.d.ts +149 -0
- package/dist/metadata/sql-grounding.d.ts.map +1 -0
- package/dist/metadata/sql-grounding.js +397 -0
- package/dist/metadata/sql-grounding.js.map +1 -0
- package/dist/metadata/sql-retrieval.d.ts +29 -0
- package/dist/metadata/sql-retrieval.d.ts.map +1 -0
- package/dist/metadata/sql-retrieval.js +79 -0
- package/dist/metadata/sql-retrieval.js.map +1 -0
- package/dist/narrate.d.ts +52 -0
- package/dist/narrate.d.ts.map +1 -0
- package/dist/narrate.js +263 -0
- package/dist/narrate.js.map +1 -0
- package/dist/propose/build-from-prompt.d.ts +160 -0
- package/dist/propose/build-from-prompt.d.ts.map +1 -0
- package/dist/propose/build-from-prompt.js +815 -0
- package/dist/propose/build-from-prompt.js.map +1 -0
- package/dist/propose/classify.d.ts +55 -0
- package/dist/propose/classify.d.ts.map +1 -0
- package/dist/propose/classify.js +167 -0
- package/dist/propose/classify.js.map +1 -0
- package/dist/propose/config.d.ts +39 -0
- package/dist/propose/config.d.ts.map +1 -0
- package/dist/propose/config.js +52 -0
- package/dist/propose/config.js.map +1 -0
- package/dist/propose/dbt-artifacts.d.ts +134 -0
- package/dist/propose/dbt-artifacts.d.ts.map +1 -0
- package/dist/propose/dbt-artifacts.js +320 -0
- package/dist/propose/dbt-artifacts.js.map +1 -0
- package/dist/propose/edit-block.d.ts +74 -0
- package/dist/propose/edit-block.d.ts.map +1 -0
- package/dist/propose/edit-block.js +133 -0
- package/dist/propose/edit-block.js.map +1 -0
- package/dist/propose/enrich.d.ts +48 -0
- package/dist/propose/enrich.d.ts.map +1 -0
- package/dist/propose/enrich.js +125 -0
- package/dist/propose/enrich.js.map +1 -0
- package/dist/propose/generate-sql.d.ts +61 -0
- package/dist/propose/generate-sql.d.ts.map +1 -0
- package/dist/propose/generate-sql.js +180 -0
- package/dist/propose/generate-sql.js.map +1 -0
- package/dist/propose/index.d.ts +24 -0
- package/dist/propose/index.d.ts.map +1 -0
- package/dist/propose/index.js +15 -0
- package/dist/propose/index.js.map +1 -0
- package/dist/propose/propose.d.ts +226 -0
- package/dist/propose/propose.d.ts.map +1 -0
- package/dist/propose/propose.js +668 -0
- package/dist/propose/propose.js.map +1 -0
- package/dist/propose/reflect-block.d.ts +99 -0
- package/dist/propose/reflect-block.d.ts.map +1 -0
- package/dist/propose/reflect-block.js +220 -0
- package/dist/propose/reflect-block.js.map +1 -0
- package/dist/propose/write-draft.d.ts +90 -0
- package/dist/propose/write-draft.d.ts.map +1 -0
- package/dist/propose/write-draft.js +185 -0
- package/dist/propose/write-draft.js.map +1 -0
- package/dist/providers/claude.d.ts +13 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +20 -2
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/gemini.d.ts +13 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +24 -3
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -3
- package/dist/providers/index.js.map +1 -1
- package/dist/research-loop.d.ts +62 -0
- package/dist/research-loop.d.ts.map +1 -0
- package/dist/research-loop.js +210 -0
- package/dist/research-loop.js.map +1 -0
- package/dist/skills/defaults.d.ts +37 -0
- package/dist/skills/defaults.d.ts.map +1 -0
- package/dist/skills/defaults.js +152 -0
- package/dist/skills/defaults.js.map +1 -0
- package/dist/skills/loader.d.ts +60 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +164 -2
- package/dist/skills/loader.js.map +1 -1
- package/package.json +7 -5
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local OSS owner identity (spec 14, part C).
|
|
3
|
+
*
|
|
4
|
+
* A new DRAFT block must never be born with a "Missing owner" Certifier strike
|
|
5
|
+
* just because the OSS user never passed `--owner`. We resolve a sensible
|
|
6
|
+
* default owner ONCE and persist it, then stamp it across every draft-creating
|
|
7
|
+
* path (propose, generate, enrichment, /api/ai/build) when no explicit owner is
|
|
8
|
+
* supplied.
|
|
9
|
+
*
|
|
10
|
+
* Resolution order (first non-empty wins):
|
|
11
|
+
* 1. Persisted owner — `dql.config.json` `identity.owner`, else `.dql/local`.
|
|
12
|
+
* 2. git `user.email` (project-local, then global).
|
|
13
|
+
* 3. `$USER` / `$USERNAME` / OS user — `<user>@local`.
|
|
14
|
+
* 4. `guest@local` (guaranteed non-empty fallback).
|
|
15
|
+
*
|
|
16
|
+
* This module is deterministic and offline — it never calls a provider. The git
|
|
17
|
+
* lookup is best-effort and degrades silently when git is absent or the repo has
|
|
18
|
+
* no configured email.
|
|
19
|
+
*/
|
|
20
|
+
import { execFileSync } from 'node:child_process';
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
|
+
import { userInfo } from 'node:os';
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
24
|
+
/** Where the resolved owner is cached when `dql.config.json` is unavailable. */
|
|
25
|
+
const LOCAL_OWNER_PATH = ['.dql', 'local', 'owner'];
|
|
26
|
+
function nonEmpty(value) {
|
|
27
|
+
if (typeof value !== 'string')
|
|
28
|
+
return undefined;
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
31
|
+
}
|
|
32
|
+
/** Read a persisted owner from `dql.config.json` `identity.owner`, else `.dql/local/owner`. */
|
|
33
|
+
export function readPersistedOwner(projectRoot) {
|
|
34
|
+
const configPath = join(projectRoot, 'dql.config.json');
|
|
35
|
+
if (existsSync(configPath)) {
|
|
36
|
+
try {
|
|
37
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
38
|
+
const owner = nonEmpty(typeof raw.identity?.owner === 'string' ? raw.identity.owner : undefined);
|
|
39
|
+
if (owner)
|
|
40
|
+
return owner;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// Malformed config is not fatal — fall through to the local cache.
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const localPath = join(projectRoot, ...LOCAL_OWNER_PATH);
|
|
47
|
+
if (existsSync(localPath)) {
|
|
48
|
+
try {
|
|
49
|
+
return nonEmpty(readFileSync(localPath, 'utf-8'));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
// Unreadable cache — fall through.
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/** Best-effort git `user.email` (project-local first, then global). Never throws. */
|
|
58
|
+
function gitUserEmail(projectRoot) {
|
|
59
|
+
const run = (args) => {
|
|
60
|
+
try {
|
|
61
|
+
const out = execFileSync('git', args, {
|
|
62
|
+
cwd: projectRoot,
|
|
63
|
+
encoding: 'utf-8',
|
|
64
|
+
timeout: 3000,
|
|
65
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
66
|
+
});
|
|
67
|
+
return nonEmpty(out);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return run(['config', 'user.email']) ?? run(['config', '--global', 'user.email']);
|
|
74
|
+
}
|
|
75
|
+
/** `$USER` / OS user → `<user>@local`, else undefined. */
|
|
76
|
+
function osUserOwner() {
|
|
77
|
+
const fromEnv = nonEmpty(process.env.USER) ?? nonEmpty(process.env.USERNAME);
|
|
78
|
+
if (fromEnv)
|
|
79
|
+
return `${fromEnv}@local`;
|
|
80
|
+
try {
|
|
81
|
+
const name = nonEmpty(userInfo().username);
|
|
82
|
+
if (name)
|
|
83
|
+
return `${name}@local`;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// userInfo can throw on exotic platforms — ignore.
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Persist the owner into `dql.config.json` `identity.owner` when the config
|
|
92
|
+
* exists; otherwise cache it under `.dql/local/owner`. Best-effort — a failure to
|
|
93
|
+
* persist never blocks the caller (the owner is still returned).
|
|
94
|
+
*/
|
|
95
|
+
export function persistOwner(projectRoot, owner) {
|
|
96
|
+
const value = nonEmpty(owner);
|
|
97
|
+
if (!value)
|
|
98
|
+
return;
|
|
99
|
+
const configPath = join(projectRoot, 'dql.config.json');
|
|
100
|
+
if (existsSync(configPath)) {
|
|
101
|
+
try {
|
|
102
|
+
const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
103
|
+
const identity = (raw.identity && typeof raw.identity === 'object'
|
|
104
|
+
? raw.identity
|
|
105
|
+
: {});
|
|
106
|
+
if (identity.owner === value)
|
|
107
|
+
return; // already current — no rewrite.
|
|
108
|
+
identity.owner = value;
|
|
109
|
+
raw.identity = identity;
|
|
110
|
+
writeFileSync(configPath, JSON.stringify(raw, null, 2) + '\n', 'utf-8');
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Fall through to the local cache when the config cannot be rewritten.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const localPath = join(projectRoot, ...LOCAL_OWNER_PATH);
|
|
119
|
+
mkdirSync(dirname(localPath), { recursive: true });
|
|
120
|
+
writeFileSync(localPath, value + '\n', 'utf-8');
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Caching is advisory; a read-only filesystem must not break resolution.
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the default local OSS owner. Always returns a non-empty string. When
|
|
128
|
+
* an explicit owner is given it wins (and is persisted); otherwise the first
|
|
129
|
+
* persisted/derived value is used and persisted for reuse.
|
|
130
|
+
*/
|
|
131
|
+
export function resolveLocalOwner(projectRoot, options = {}) {
|
|
132
|
+
const persist = options.persist !== false;
|
|
133
|
+
const explicit = nonEmpty(options.explicit);
|
|
134
|
+
if (explicit) {
|
|
135
|
+
if (persist)
|
|
136
|
+
persistOwner(projectRoot, explicit);
|
|
137
|
+
return explicit;
|
|
138
|
+
}
|
|
139
|
+
const persisted = readPersistedOwner(projectRoot);
|
|
140
|
+
if (persisted)
|
|
141
|
+
return persisted;
|
|
142
|
+
const resolved = gitUserEmail(projectRoot) ?? osUserOwner() ?? 'guest@local';
|
|
143
|
+
if (persist)
|
|
144
|
+
persistOwner(projectRoot, resolved);
|
|
145
|
+
return resolved;
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"identity.js","sourceRoot":"","sources":["../../src/metadata/identity.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic-metric matching (spec 17, part C).
|
|
3
|
+
*
|
|
4
|
+
* The answer-loop already has a `semantic_layer` tier in its route order
|
|
5
|
+
* (certified → business_context → semantic_layer → dbt_manifest → generated →
|
|
6
|
+
* refuse). The miss it fixes here: matching a clear metric question to a governed
|
|
7
|
+
* metric was purely string-based (FTS token overlap), so "what is our total
|
|
8
|
+
* revenue" did not connect to a revenue-family metric like `cumulative_revenue`,
|
|
9
|
+
* `food_revenue`, or `lifetime_spend` — no metric is literally named "revenue" —
|
|
10
|
+
* and the loop refused instead of routing to the semantic tier.
|
|
11
|
+
*
|
|
12
|
+
* `matchSemanticMetric` ranks candidate metric KG nodes against the question
|
|
13
|
+
* using:
|
|
14
|
+
* - a synonym/family expansion of the question's measure terms (revenue ⇄
|
|
15
|
+
* spend/sales/income/arr; orders ⇄ purchases; ...), and of each metric's
|
|
16
|
+
* searchable text (name + synonyms + label + description + tags),
|
|
17
|
+
* - lexical token overlap blended through the spec-11 `hybridRank`
|
|
18
|
+
* (alpha defaults to 0 → pure lexical, offline-stable for tests),
|
|
19
|
+
* - a strong boost when a measure FAMILY (e.g. "revenue") is shared between
|
|
20
|
+
* the question and the metric, which is what makes "total revenue" reach the
|
|
21
|
+
* revenue family even though the exact word never appears in a metric name.
|
|
22
|
+
*
|
|
23
|
+
* It returns the single best metric only when the score clears a confidence
|
|
24
|
+
* threshold, so genuinely ad-hoc questions ("median order value by region")
|
|
25
|
+
* still fall through to generated SQL, and questions with no measure at all
|
|
26
|
+
* still refuse honestly.
|
|
27
|
+
*/
|
|
28
|
+
import { type EmbeddingProvider } from '../embeddings/provider.js';
|
|
29
|
+
import type { KGNode } from '../kg/types.js';
|
|
30
|
+
/** The measure families a metric KG node resolves to (revenue, orders, ...). */
|
|
31
|
+
export declare function metricFamilies(metric: KGNode): Set<string>;
|
|
32
|
+
export interface MetricMatch {
|
|
33
|
+
metric: KGNode;
|
|
34
|
+
/** Blended relevance in [0, 1]. */
|
|
35
|
+
score: number;
|
|
36
|
+
/** The shared measure family that justified the match, if any. */
|
|
37
|
+
family?: string;
|
|
38
|
+
}
|
|
39
|
+
export interface MatchSemanticMetricOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Confidence threshold. A match below this is treated as "no confident metric"
|
|
42
|
+
* so ad-hoc questions still fall through to generated SQL. Default 0.34.
|
|
43
|
+
*/
|
|
44
|
+
threshold?: number;
|
|
45
|
+
/** Vector-similarity weight; 0 (default) = offline-stable pure lexical. */
|
|
46
|
+
alpha?: number;
|
|
47
|
+
provider?: EmbeddingProvider;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Pick the single best governed metric for a question, or `null` when no metric
|
|
51
|
+
* clears the confidence bar. Deterministic + offline by default (alpha=0).
|
|
52
|
+
*
|
|
53
|
+
* Scoring per candidate metric:
|
|
54
|
+
* base = lexical token overlap fraction (question content tokens present in
|
|
55
|
+
* the metric's searchable text), in [0, 1]
|
|
56
|
+
* family = +0.6 when the question and metric share a measure family
|
|
57
|
+
* name = +0.25 when a question content token appears in the metric NAME
|
|
58
|
+
* (so "food revenue" prefers `food_revenue` over `drink_revenue`)
|
|
59
|
+
* The blended FTS score is fed to `hybridRank` so spec-11 embeddings can refine
|
|
60
|
+
* it when a provider/alpha is supplied.
|
|
61
|
+
*/
|
|
62
|
+
export declare function matchSemanticMetric(question: string, metrics: KGNode[], options?: MatchSemanticMetricOptions): Promise<MetricMatch | null>;
|
|
63
|
+
/**
|
|
64
|
+
* Parse a metric KG node's governed definition: the aggregate `sql:` expression
|
|
65
|
+
* and its `table:` relation, stored on `llmContext`. Returns undefined when the
|
|
66
|
+
* `sql:` is not an aggregate expression (a bare metric-name reference) or there is
|
|
67
|
+
* no table — those are not directly executable.
|
|
68
|
+
*/
|
|
69
|
+
export declare function parseMetricDefinition(metric: KGNode): {
|
|
70
|
+
expr: string;
|
|
71
|
+
table: string;
|
|
72
|
+
} | undefined;
|
|
73
|
+
/**
|
|
74
|
+
* Deterministic, offline-safe governed SQL for a matched metric (spec 17, part C):
|
|
75
|
+
* a single read-only `SELECT <expr> AS <alias> FROM <table>` synthesised from the
|
|
76
|
+
* metric's governed definition. Returns undefined when the definition is too thin
|
|
77
|
+
* to execute, preserving honest refusal.
|
|
78
|
+
*/
|
|
79
|
+
export declare function metricToGovernedSql(metric: KGNode): string | undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Resolve a matched metric to executable governed SQL. dbt's semantic layer splits
|
|
82
|
+
* a metric from its measure: a derived MetricFlow metric (e.g. `revenue`, type=simple
|
|
83
|
+
* over measure `revenue`) carries no `table:`/`sql:` of its own — the aggregate lives
|
|
84
|
+
* on the backing measure node (`order_item.revenue` → `SUM(product_price)` over
|
|
85
|
+
* `order_items`). When the matched metric is itself synthesizable we use it directly;
|
|
86
|
+
* otherwise we fall back to a sibling whose LEAF name matches (`revenue` ⇒
|
|
87
|
+
* `<model>.revenue`). No fuzzy family guessing — precise resolution or nothing.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveGovernedMetricSql(metric: KGNode, pool: KGNode[]): {
|
|
90
|
+
sql: string;
|
|
91
|
+
metric: KGNode;
|
|
92
|
+
} | undefined;
|
|
93
|
+
//# sourceMappingURL=metric-match.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-match.d.ts","sourceRoot":"","sources":["../../src/metadata/metric-match.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAwC,KAAK,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACzG,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AA0F7C,gFAAgF;AAChF,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,CAE1D;AAED,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2EAA2E;IAC3E,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAsB,mBAAmB,CACvC,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,EAAE,EACjB,OAAO,GAAE,0BAA+B,GACvC,OAAO,CAAC,WAAW,GAAG,IAAI,CAAC,CAyD7B;AAOD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAMjG;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAKtE;AAED;;;;;;;;GAQG;AACH,wBAAgB,wBAAwB,CACtC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,MAAM,EAAE,GACb;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAW7C"}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic-metric matching (spec 17, part C).
|
|
3
|
+
*
|
|
4
|
+
* The answer-loop already has a `semantic_layer` tier in its route order
|
|
5
|
+
* (certified → business_context → semantic_layer → dbt_manifest → generated →
|
|
6
|
+
* refuse). The miss it fixes here: matching a clear metric question to a governed
|
|
7
|
+
* metric was purely string-based (FTS token overlap), so "what is our total
|
|
8
|
+
* revenue" did not connect to a revenue-family metric like `cumulative_revenue`,
|
|
9
|
+
* `food_revenue`, or `lifetime_spend` — no metric is literally named "revenue" —
|
|
10
|
+
* and the loop refused instead of routing to the semantic tier.
|
|
11
|
+
*
|
|
12
|
+
* `matchSemanticMetric` ranks candidate metric KG nodes against the question
|
|
13
|
+
* using:
|
|
14
|
+
* - a synonym/family expansion of the question's measure terms (revenue ⇄
|
|
15
|
+
* spend/sales/income/arr; orders ⇄ purchases; ...), and of each metric's
|
|
16
|
+
* searchable text (name + synonyms + label + description + tags),
|
|
17
|
+
* - lexical token overlap blended through the spec-11 `hybridRank`
|
|
18
|
+
* (alpha defaults to 0 → pure lexical, offline-stable for tests),
|
|
19
|
+
* - a strong boost when a measure FAMILY (e.g. "revenue") is shared between
|
|
20
|
+
* the question and the metric, which is what makes "total revenue" reach the
|
|
21
|
+
* revenue family even though the exact word never appears in a metric name.
|
|
22
|
+
*
|
|
23
|
+
* It returns the single best metric only when the score clears a confidence
|
|
24
|
+
* threshold, so genuinely ad-hoc questions ("median order value by region")
|
|
25
|
+
* still fall through to generated SQL, and questions with no measure at all
|
|
26
|
+
* still refuse honestly.
|
|
27
|
+
*/
|
|
28
|
+
import { defaultEmbeddingProvider, hybridRank } from '../embeddings/provider.js';
|
|
29
|
+
const TOKEN_RE = /[\p{L}\p{N}_]+/gu;
|
|
30
|
+
/** Stopwords that should never count toward a measure/metric match. */
|
|
31
|
+
const STOPWORDS = new Set([
|
|
32
|
+
'what', 'whats', 'is', 'are', 'was', 'were', 'the', 'a', 'an', 'our', 'my', 'we',
|
|
33
|
+
'of', 'for', 'in', 'on', 'to', 'by', 'and', 'or', 'show', 'me', 'give', 'tell',
|
|
34
|
+
'how', 'much', 'many', 'this', 'that', 'these', 'those', 'do', 'does', 'did',
|
|
35
|
+
'total', 'overall', 'current', 'all', 'per', 'each', 'value', 'amount', 'number',
|
|
36
|
+
'count', 'sum', 'average', 'avg', 'get', 'find', 'whole', 'entire',
|
|
37
|
+
]);
|
|
38
|
+
/**
|
|
39
|
+
* Measure synonym families. Each family maps a canonical measure to the set of
|
|
40
|
+
* words that signal it. A question and a metric "share a family" when any word
|
|
41
|
+
* from the question and any word from the metric resolve to the same family.
|
|
42
|
+
*/
|
|
43
|
+
const MEASURE_FAMILIES = {
|
|
44
|
+
revenue: ['revenue', 'revenues', 'sales', 'income', 'turnover', 'earnings', 'gmv', 'arr', 'mrr', 'bookings', 'topline'],
|
|
45
|
+
spend: ['spend', 'spending', 'spent', 'cost', 'costs', 'expense', 'expenses', 'cogs'],
|
|
46
|
+
profit: ['profit', 'profits', 'margin', 'margins', 'gross', 'net', 'ebitda'],
|
|
47
|
+
orders: ['order', 'orders', 'purchase', 'purchases', 'transaction', 'transactions', 'checkout', 'checkouts'],
|
|
48
|
+
customers: ['customer', 'customers', 'user', 'users', 'account', 'accounts', 'buyer', 'buyers', 'shopper', 'shoppers'],
|
|
49
|
+
churn: ['churn', 'churned', 'attrition', 'cancel', 'cancellation', 'cancellations', 'logo'],
|
|
50
|
+
retention: ['retention', 'retained', 'renewal', 'renewals'],
|
|
51
|
+
quantity: ['quantity', 'units', 'volume', 'qty'],
|
|
52
|
+
aov: ['aov', 'basket'],
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Row/record-listing intent. A question asking to ENUMERATE rows or identifiers
|
|
56
|
+
* ("list the 5 most recent orders with their ids") is not a metric question even
|
|
57
|
+
* though it names a measure entity ("orders") — it wants raw rows, so it should
|
|
58
|
+
* fall through to grounded generated SQL, not a governed aggregate. Conservative:
|
|
59
|
+
* only the clearest listing signals, so true aggregate questions ("how many
|
|
60
|
+
* orders", "total revenue", "average order value") are never suppressed.
|
|
61
|
+
*/
|
|
62
|
+
const ROW_REQUEST_RE = /\b(list|listing|rows?|records?|individual|line[\s-]?items?|ids?)\b/i;
|
|
63
|
+
const RECENCY_RE = /\b(most\s+recent|latest|newest|oldest|last\s+\d+)\b/i;
|
|
64
|
+
function looksLikeRowRequest(question) {
|
|
65
|
+
return ROW_REQUEST_RE.test(question) || RECENCY_RE.test(question);
|
|
66
|
+
}
|
|
67
|
+
function tokenize(text) {
|
|
68
|
+
return (text.toLowerCase().match(TOKEN_RE) ?? []).filter((t) => t.length > 1);
|
|
69
|
+
}
|
|
70
|
+
/** Content tokens (stopwords stripped) used for measure/family signal. */
|
|
71
|
+
function contentTokens(text) {
|
|
72
|
+
return tokenize(text).filter((t) => !STOPWORDS.has(t));
|
|
73
|
+
}
|
|
74
|
+
/** The measure families a set of words resolves to. */
|
|
75
|
+
function familiesFor(words) {
|
|
76
|
+
const set = new Set(words);
|
|
77
|
+
const families = new Set();
|
|
78
|
+
for (const [family, members] of Object.entries(MEASURE_FAMILIES)) {
|
|
79
|
+
if (members.some((member) => set.has(member)))
|
|
80
|
+
families.add(family);
|
|
81
|
+
}
|
|
82
|
+
return families;
|
|
83
|
+
}
|
|
84
|
+
/** Searchable text for a metric KG node: name, synonyms, label, description, tags. */
|
|
85
|
+
function metricSearchText(metric) {
|
|
86
|
+
return [
|
|
87
|
+
metric.name,
|
|
88
|
+
metric.name.replace(/[_.]+/g, ' '),
|
|
89
|
+
metric.description ?? '',
|
|
90
|
+
metric.llmContext ?? '',
|
|
91
|
+
...(metric.tags ?? []),
|
|
92
|
+
]
|
|
93
|
+
.filter(Boolean)
|
|
94
|
+
.join(' ');
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Text used ONLY for measure-FAMILY detection: the metric's name + description +
|
|
98
|
+
* tags, but NOT its `llmContext`. The llmContext carries `table:`/`sql:` lines, so
|
|
99
|
+
* including it wrongly assigns a metric to the family of its backing TABLE (a
|
|
100
|
+
* `tax_paid` measure on the `orders` table would falsely read as the "orders"
|
|
101
|
+
* family). Family must reflect what the metric MEASURES, i.e. its name/label.
|
|
102
|
+
*/
|
|
103
|
+
function metricFamilyText(metric) {
|
|
104
|
+
return [metric.name, metric.name.replace(/[_.]+/g, ' '), metric.description ?? '', ...(metric.tags ?? [])]
|
|
105
|
+
.filter(Boolean)
|
|
106
|
+
.join(' ');
|
|
107
|
+
}
|
|
108
|
+
/** The measure families a metric KG node resolves to (revenue, orders, ...). */
|
|
109
|
+
export function metricFamilies(metric) {
|
|
110
|
+
return familiesFor(contentTokens(metricFamilyText(metric)));
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Pick the single best governed metric for a question, or `null` when no metric
|
|
114
|
+
* clears the confidence bar. Deterministic + offline by default (alpha=0).
|
|
115
|
+
*
|
|
116
|
+
* Scoring per candidate metric:
|
|
117
|
+
* base = lexical token overlap fraction (question content tokens present in
|
|
118
|
+
* the metric's searchable text), in [0, 1]
|
|
119
|
+
* family = +0.6 when the question and metric share a measure family
|
|
120
|
+
* name = +0.25 when a question content token appears in the metric NAME
|
|
121
|
+
* (so "food revenue" prefers `food_revenue` over `drink_revenue`)
|
|
122
|
+
* The blended FTS score is fed to `hybridRank` so spec-11 embeddings can refine
|
|
123
|
+
* it when a provider/alpha is supplied.
|
|
124
|
+
*/
|
|
125
|
+
export async function matchSemanticMetric(question, metrics, options = {}) {
|
|
126
|
+
const candidates = metrics.filter((m) => m.kind === 'metric');
|
|
127
|
+
if (candidates.length === 0)
|
|
128
|
+
return null;
|
|
129
|
+
// A row/record-listing question wants raw rows, not a governed aggregate —
|
|
130
|
+
// let it fall through to grounded generated SQL even if it names a measure.
|
|
131
|
+
if (looksLikeRowRequest(question))
|
|
132
|
+
return null;
|
|
133
|
+
const threshold = options.threshold ?? 0.34;
|
|
134
|
+
const qContent = new Set(contentTokens(question));
|
|
135
|
+
if (qContent.size === 0)
|
|
136
|
+
return null;
|
|
137
|
+
const qFamilies = familiesFor(qContent);
|
|
138
|
+
// A measure question must carry at least one content word OR a measure family;
|
|
139
|
+
// a bare "what is this" carries neither and should not match a metric.
|
|
140
|
+
const items = candidates.map((metric) => {
|
|
141
|
+
const text = metricSearchText(metric).toLowerCase();
|
|
142
|
+
const nameTokens = new Set(tokenize(metric.name));
|
|
143
|
+
// Family is derived from the metric's name/label only (not its `table:`), so a
|
|
144
|
+
// measure is never mis-assigned to the family of the relation it sits on.
|
|
145
|
+
const metricFams = familiesFor(contentTokens(metricFamilyText(metric)));
|
|
146
|
+
let overlap = 0;
|
|
147
|
+
let nameHit = 0;
|
|
148
|
+
for (const token of qContent) {
|
|
149
|
+
if (text.includes(token))
|
|
150
|
+
overlap += 1;
|
|
151
|
+
if (nameTokens.has(token))
|
|
152
|
+
nameHit += 1;
|
|
153
|
+
}
|
|
154
|
+
const lexical = overlap / qContent.size;
|
|
155
|
+
// Fraction of question content tokens present in the metric NAME — this is
|
|
156
|
+
// what separates `food_revenue` from `cumulative_revenue` for "food revenue".
|
|
157
|
+
const nameFraction = nameHit / qContent.size;
|
|
158
|
+
let sharedFamily;
|
|
159
|
+
for (const family of qFamilies) {
|
|
160
|
+
if (metricFams.has(family)) {
|
|
161
|
+
sharedFamily = family;
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
// Blend the signals on a fixed budget so no single component saturates the
|
|
166
|
+
// clamp and hides a more specific name match.
|
|
167
|
+
const familyBoost = sharedFamily ? 0.45 : 0;
|
|
168
|
+
const fts = clamp01(0.35 * lexical + familyBoost + 0.35 * nameFraction);
|
|
169
|
+
return { metric, text: metricSearchText(metric), ftsScore: fts, family: sharedFamily };
|
|
170
|
+
});
|
|
171
|
+
const ranked = await hybridRank(question, items.map((entry) => ({ item: entry, text: entry.text, ftsScore: entry.ftsScore })), { alpha: options.alpha ?? 0, provider: options.provider ?? (options.alpha ? defaultEmbeddingProvider() : undefined) });
|
|
172
|
+
const best = ranked[0];
|
|
173
|
+
if (!best || best.score < threshold)
|
|
174
|
+
return null;
|
|
175
|
+
return { metric: best.item.metric, score: best.score, family: best.item.family };
|
|
176
|
+
}
|
|
177
|
+
function clamp01(value) {
|
|
178
|
+
if (!Number.isFinite(value))
|
|
179
|
+
return 0;
|
|
180
|
+
return Math.max(0, Math.min(1, value));
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Parse a metric KG node's governed definition: the aggregate `sql:` expression
|
|
184
|
+
* and its `table:` relation, stored on `llmContext`. Returns undefined when the
|
|
185
|
+
* `sql:` is not an aggregate expression (a bare metric-name reference) or there is
|
|
186
|
+
* no table — those are not directly executable.
|
|
187
|
+
*/
|
|
188
|
+
export function parseMetricDefinition(metric) {
|
|
189
|
+
const context = metric.llmContext ?? '';
|
|
190
|
+
const expr = context.match(/(?:^|\n)\s*sql:\s*(.+?)\s*(?:\n|$)/i)?.[1]?.trim();
|
|
191
|
+
const table = context.match(/(?:^|\n)\s*table:\s*(.+?)\s*(?:\n|$)/i)?.[1]?.trim();
|
|
192
|
+
if (!expr || !table || !/[()]/.test(expr))
|
|
193
|
+
return undefined;
|
|
194
|
+
return { expr, table };
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Deterministic, offline-safe governed SQL for a matched metric (spec 17, part C):
|
|
198
|
+
* a single read-only `SELECT <expr> AS <alias> FROM <table>` synthesised from the
|
|
199
|
+
* metric's governed definition. Returns undefined when the definition is too thin
|
|
200
|
+
* to execute, preserving honest refusal.
|
|
201
|
+
*/
|
|
202
|
+
export function metricToGovernedSql(metric) {
|
|
203
|
+
const def = parseMetricDefinition(metric);
|
|
204
|
+
if (!def)
|
|
205
|
+
return undefined;
|
|
206
|
+
const alias = metric.name.replace(/[^a-zA-Z0-9_]+/g, '_').replace(/^_+|_+$/g, '') || 'metric_value';
|
|
207
|
+
return `SELECT ${def.expr} AS ${alias}\nFROM ${def.table}`;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Resolve a matched metric to executable governed SQL. dbt's semantic layer splits
|
|
211
|
+
* a metric from its measure: a derived MetricFlow metric (e.g. `revenue`, type=simple
|
|
212
|
+
* over measure `revenue`) carries no `table:`/`sql:` of its own — the aggregate lives
|
|
213
|
+
* on the backing measure node (`order_item.revenue` → `SUM(product_price)` over
|
|
214
|
+
* `order_items`). When the matched metric is itself synthesizable we use it directly;
|
|
215
|
+
* otherwise we fall back to a sibling whose LEAF name matches (`revenue` ⇒
|
|
216
|
+
* `<model>.revenue`). No fuzzy family guessing — precise resolution or nothing.
|
|
217
|
+
*/
|
|
218
|
+
export function resolveGovernedMetricSql(metric, pool) {
|
|
219
|
+
const direct = metricToGovernedSql(metric);
|
|
220
|
+
if (direct)
|
|
221
|
+
return { sql: direct, metric };
|
|
222
|
+
const leaf = metric.name.split('.').pop()?.toLowerCase();
|
|
223
|
+
if (!leaf)
|
|
224
|
+
return undefined;
|
|
225
|
+
const leafSibling = pool
|
|
226
|
+
.filter((node) => node.nodeId !== metric.nodeId && node.name.split('.').pop()?.toLowerCase() === leaf)
|
|
227
|
+
.sort((a, b) => a.name.length - b.name.length)
|
|
228
|
+
.find((node) => metricToGovernedSql(node));
|
|
229
|
+
if (leafSibling)
|
|
230
|
+
return { sql: metricToGovernedSql(leafSibling), metric: leafSibling };
|
|
231
|
+
return undefined;
|
|
232
|
+
}
|
|
233
|
+
//# sourceMappingURL=metric-match.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metric-match.js","sourceRoot":"","sources":["../../src/metadata/metric-match.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared SQL grounding (spec 15) — ONE grounding for BOTH the governed
|
|
3
|
+
* answer-loop and the AI build engine. There is no "weak path".
|
|
4
|
+
*
|
|
5
|
+
* The grounding takes dbt artifacts (+ an optional set of relevant model names)
|
|
6
|
+
* and produces, for each table:
|
|
7
|
+
* - the REAL qualified warehouse relation (`database.schema.alias`, e.g.
|
|
8
|
+
* `dev.order_items`) — never a bare model name,
|
|
9
|
+
* - the `{{ ref('<model>') }}` form for governed BLOCK SQL,
|
|
10
|
+
* - real columns + types (catalog.json preferred, manifest YAML fallback),
|
|
11
|
+
* - JOIN KEYS inferred from dbt `depends_on` and shared `*_id` / `*_key`
|
|
12
|
+
* columns,
|
|
13
|
+
* - a `certifiedBlock` flag (reserved for callers that pass certified models).
|
|
14
|
+
*
|
|
15
|
+
* On top of the grounding sit two deterministic, offline-safe primitives the
|
|
16
|
+
* build path was missing:
|
|
17
|
+
* - `resolveRelationsInSql` — rewrites any bare / unqualified table name to
|
|
18
|
+
* its real qualified relation (model name OR alias → `db.schema.alias`),
|
|
19
|
+
* - `validateSqlAgainstGrounding` — flags any table or column the generated
|
|
20
|
+
* SQL references that is not in the grounding.
|
|
21
|
+
*
|
|
22
|
+
* Both are pure string/AST helpers so they work with no provider and never
|
|
23
|
+
* crash the offline path.
|
|
24
|
+
*/
|
|
25
|
+
import type { DbtArtifacts } from '../propose/dbt-artifacts.js';
|
|
26
|
+
/** A single grounded table the model is allowed to reference. */
|
|
27
|
+
export interface GroundedColumn {
|
|
28
|
+
name: string;
|
|
29
|
+
type?: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface GroundedTable {
|
|
33
|
+
/** Logical model / source name (what `{{ ref() }}` uses). */
|
|
34
|
+
name: string;
|
|
35
|
+
/** Real qualified warehouse relation, e.g. `dev.order_items`. */
|
|
36
|
+
qualifiedRelation: string;
|
|
37
|
+
/** `{{ ref('<model>') }}` (models) or `{{ source(...) }}` (sources) form. */
|
|
38
|
+
refForm?: string;
|
|
39
|
+
/** Resolved columns with types. */
|
|
40
|
+
columns: GroundedColumn[];
|
|
41
|
+
/** 'model' | 'source'. */
|
|
42
|
+
kind: 'model' | 'source';
|
|
43
|
+
/** dbt uniqueId (for join-key inference and citations). */
|
|
44
|
+
uniqueId: string;
|
|
45
|
+
/** True when this table is backed by a certified DQL block. */
|
|
46
|
+
certifiedBlock?: boolean;
|
|
47
|
+
}
|
|
48
|
+
/** A join key the model can use to connect two grounded relations. */
|
|
49
|
+
export interface GroundedJoinKey {
|
|
50
|
+
leftRelation: string;
|
|
51
|
+
leftColumn: string;
|
|
52
|
+
rightRelation: string;
|
|
53
|
+
rightColumn: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
}
|
|
56
|
+
export interface SchemaGrounding {
|
|
57
|
+
tables: GroundedTable[];
|
|
58
|
+
joinKeys: GroundedJoinKey[];
|
|
59
|
+
/** Lower-cased lookup: bare name / alias / qualified relation → GroundedTable. */
|
|
60
|
+
byKey: Map<string, GroundedTable>;
|
|
61
|
+
}
|
|
62
|
+
export interface BuildSchemaGroundingOptions {
|
|
63
|
+
/** Cap on tables included in the grounding (top-K from retrieval). */
|
|
64
|
+
limit?: number;
|
|
65
|
+
/** Model/relation names that are backed by certified DQL blocks. */
|
|
66
|
+
certifiedModelNames?: Iterable<string>;
|
|
67
|
+
}
|
|
68
|
+
/** Lower-cased keys a relation can be referenced by (qualified, last 2, bare). */
|
|
69
|
+
export declare function relationKeys(relation: string): string[];
|
|
70
|
+
/**
|
|
71
|
+
* Build the shared schema grounding for a set of dbt models (+ sources).
|
|
72
|
+
* `relevantModels`, when provided, restricts/orders the grounding to the
|
|
73
|
+
* retrieval-selected relations; otherwise all models are grounded (capped).
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildSchemaGrounding(artifacts: DbtArtifacts | undefined, relevantModels?: Iterable<string> | undefined, options?: BuildSchemaGroundingOptions): SchemaGrounding;
|
|
76
|
+
/** A pre-qualified runtime relation (the answer-loop's schema context shape). */
|
|
77
|
+
export interface RuntimeRelationInput {
|
|
78
|
+
/** Already-qualified relation, e.g. `dev.order_items`. */
|
|
79
|
+
relation: string;
|
|
80
|
+
/** Bare table name. */
|
|
81
|
+
name?: string;
|
|
82
|
+
columns: Array<{
|
|
83
|
+
name: string;
|
|
84
|
+
type?: string;
|
|
85
|
+
description?: string;
|
|
86
|
+
}>;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Build a `SchemaGrounding` from already-qualified runtime relations (the
|
|
90
|
+
* answer-loop receives these from the host as `schemaContext` /
|
|
91
|
+
* `allowedSqlContext`, so the relations are already real warehouse relations).
|
|
92
|
+
* This lets the governed answer-loop reuse the SAME resolver + validator as the
|
|
93
|
+
* build path without re-reading dbt artifacts. No join-key inference here —
|
|
94
|
+
* the answer-loop already renders join paths from its context pack.
|
|
95
|
+
*/
|
|
96
|
+
export declare function buildGroundingFromRuntimeRelations(relations: RuntimeRelationInput[]): SchemaGrounding;
|
|
97
|
+
export interface RelationResolution {
|
|
98
|
+
/** SQL with bare / unqualified table names rewritten to qualified relations. */
|
|
99
|
+
sql: string;
|
|
100
|
+
/** Each rewrite applied, e.g. order_items → dev.order_items. */
|
|
101
|
+
rewrites: Array<{
|
|
102
|
+
from: string;
|
|
103
|
+
to: string;
|
|
104
|
+
}>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Deterministically rewrite any bare / unqualified table name in `FROM` /
|
|
108
|
+
* `JOIN` clauses to its real qualified relation from the grounding. Maps both
|
|
109
|
+
* the model name and its alias to `database.schema.alias`. Already-qualified
|
|
110
|
+
* relations that match the grounding are left untouched.
|
|
111
|
+
*
|
|
112
|
+
* For BLOCK SQL the caller may request the `{{ ref() }}` form instead of the
|
|
113
|
+
* qualified relation via `prefer: 'ref'`.
|
|
114
|
+
*/
|
|
115
|
+
export declare function resolveRelationsInSql(sql: string, grounding: SchemaGrounding, options?: {
|
|
116
|
+
prefer?: 'qualified' | 'ref';
|
|
117
|
+
}): RelationResolution;
|
|
118
|
+
export type GroundingValidationCode = 'unknown_relation' | 'unknown_column' | 'unsafe_sql' | 'unparseable';
|
|
119
|
+
export type GroundingValidationResult = {
|
|
120
|
+
ok: true;
|
|
121
|
+
warnings: string[];
|
|
122
|
+
referencedRelations: string[];
|
|
123
|
+
} | {
|
|
124
|
+
ok: false;
|
|
125
|
+
code: GroundingValidationCode;
|
|
126
|
+
error: string;
|
|
127
|
+
warnings: string[];
|
|
128
|
+
referencedRelations: string[];
|
|
129
|
+
/** The specific table/column that failed, for a targeted repair re-prompt. */
|
|
130
|
+
offending?: {
|
|
131
|
+
relation?: string;
|
|
132
|
+
column?: string;
|
|
133
|
+
};
|
|
134
|
+
};
|
|
135
|
+
/**
|
|
136
|
+
* Validate that generated SQL references ONLY relations and columns present in
|
|
137
|
+
* the shared grounding. Read-only safety is enforced too (no DML/DDL). Returns
|
|
138
|
+
* a precise `offending` token so the build path can re-prompt the model with
|
|
139
|
+
* the exact miss.
|
|
140
|
+
*/
|
|
141
|
+
export declare function validateSqlAgainstGrounding(sql: string, grounding: SchemaGrounding): GroundingValidationResult;
|
|
142
|
+
/**
|
|
143
|
+
* Render the grounding as a prompt block. Each table is presented as BOTH its
|
|
144
|
+
* qualified relation AND its `{{ ref() }}` form with real columns+types, plus
|
|
145
|
+
* the inferred join keys. The convention line tells the model how to reference
|
|
146
|
+
* relations for the given target (CELL → qualified; BLOCK → `{{ ref() }}`).
|
|
147
|
+
*/
|
|
148
|
+
export declare function renderGroundingForPrompt(grounding: SchemaGrounding, target?: 'cell' | 'block' | 'answer'): string;
|
|
149
|
+
//# sourceMappingURL=sql-grounding.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sql-grounding.d.ts","sourceRoot":"","sources":["../../src/metadata/sql-grounding.ts"],"names":[],"mappings":"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"}
|