@duckcodeailabs/dql-mcp 1.6.21 → 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/server.d.ts.map +1 -1
- package/dist/server.js +42 -2
- package/dist/server.js.map +1 -1
- package/dist/tools/hints.d.ts +150 -0
- package/dist/tools/hints.d.ts.map +1 -0
- package/dist/tools/hints.js +128 -0
- package/dist/tools/hints.js.map +1 -0
- package/dist/tools/lineage-impact.d.ts +4 -0
- package/dist/tools/lineage-impact.d.ts.map +1 -1
- package/dist/tools/lineage-impact.js +36 -1
- package/dist/tools/lineage-impact.js.map +1 -1
- package/dist/tools/metadata.d.ts +109 -0
- package/dist/tools/metadata.d.ts.map +1 -0
- package/dist/tools/metadata.js +122 -0
- package/dist/tools/metadata.js.map +1 -0
- package/dist/tools/query-via-block.d.ts +23 -8
- package/dist/tools/query-via-block.d.ts.map +1 -1
- package/dist/tools/query-via-block.js +76 -0
- package/dist/tools/query-via-block.js.map +1 -1
- package/dist/tools/query-via-metadata.d.ts +54 -0
- package/dist/tools/query-via-metadata.d.ts.map +1 -1
- package/dist/tools/query-via-metadata.js +112 -0
- package/dist/tools/query-via-metadata.js.map +1 -1
- package/dist/tools/workflows.d.ts +8 -1
- package/dist/tools/workflows.d.ts.map +1 -1
- package/dist/tools/workflows.js +19 -1
- package/dist/tools/workflows.js.map +1 -1
- package/package.json +5 -5
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Grounded-SQL metadata tools (spec 15.5) — thin wrappers over the dbt catalog
|
|
3
|
+
* + the shared sql-grounding so EXTERNAL agents look up the same context the
|
|
4
|
+
* notebook build path uses:
|
|
5
|
+
*
|
|
6
|
+
* - `search_metadata` — rank dbt tables relevant to a request, with their
|
|
7
|
+
* qualified relation + {{ ref() }} form.
|
|
8
|
+
* - `get_table_schema` — the qualified relation, {{ ref() }} form, real
|
|
9
|
+
* columns + types, and join keys for a table.
|
|
10
|
+
* - `validate_sql` — validate a SQL string references ONLY known
|
|
11
|
+
* relations/columns, returning the precise miss.
|
|
12
|
+
*
|
|
13
|
+
* All three load dbt artifacts read-only and never write. They degrade
|
|
14
|
+
* gracefully (a clear hint) when no dbt manifest is present.
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { resolveDbtManifestPath } from '@duckcodeailabs/dql-core';
|
|
18
|
+
import { buildSchemaGrounding, loadDbtArtifacts, relationKeys, selectRelevantModels, validateSqlAgainstGrounding, } from '@duckcodeailabs/dql-agent';
|
|
19
|
+
/** Load dbt artifacts for the project, or undefined when no manifest exists. */
|
|
20
|
+
function loadArtifacts(ctx) {
|
|
21
|
+
const manifestPath = resolveDbtManifestPath(ctx.projectRoot);
|
|
22
|
+
if (!manifestPath)
|
|
23
|
+
return undefined;
|
|
24
|
+
try {
|
|
25
|
+
return loadDbtArtifacts(manifestPath);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const NO_MANIFEST_HINT = 'No dbt manifest was found. Run `dbt compile`/`dbt build` (or `dql sync dbt`) so target/manifest.json exists.';
|
|
32
|
+
// ─── search_metadata ──────────────────────────────────────────────────────────
|
|
33
|
+
export const searchMetadataInput = {
|
|
34
|
+
query: z.string().describe('Natural-language request to find relevant tables for.'),
|
|
35
|
+
limit: z.number().int().min(1).max(40).optional().describe('Max tables to return (default 12).'),
|
|
36
|
+
};
|
|
37
|
+
export async function searchMetadata(ctx, args) {
|
|
38
|
+
const artifacts = loadArtifacts(ctx);
|
|
39
|
+
if (!artifacts)
|
|
40
|
+
return { tables: [], hint: NO_MANIFEST_HINT };
|
|
41
|
+
const topK = args.limit ?? 12;
|
|
42
|
+
const relevant = await selectRelevantModels(artifacts, args.query, { topK });
|
|
43
|
+
const grounding = buildSchemaGrounding(artifacts, relevant, { limit: topK });
|
|
44
|
+
return {
|
|
45
|
+
total: grounding.tables.length,
|
|
46
|
+
tables: grounding.tables.map((table) => ({
|
|
47
|
+
name: table.name,
|
|
48
|
+
qualifiedRelation: table.qualifiedRelation,
|
|
49
|
+
refForm: table.refForm ?? null,
|
|
50
|
+
kind: table.kind,
|
|
51
|
+
columnCount: table.columns.length,
|
|
52
|
+
certifiedBlock: table.certifiedBlock ?? false,
|
|
53
|
+
})),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// ─── get_table_schema ─────────────────────────────────────────────────────────
|
|
57
|
+
export const getTableSchemaInput = {
|
|
58
|
+
table: z.string().describe('Model name, alias, or qualified relation (e.g. order_items or dev.order_items).'),
|
|
59
|
+
};
|
|
60
|
+
export function getTableSchema(ctx, args) {
|
|
61
|
+
const artifacts = loadArtifacts(ctx);
|
|
62
|
+
if (!artifacts)
|
|
63
|
+
return { found: false, hint: NO_MANIFEST_HINT };
|
|
64
|
+
// Ground the whole project so we can look up any relation and surface joins.
|
|
65
|
+
const grounding = buildSchemaGrounding(artifacts, undefined, { limit: 500 });
|
|
66
|
+
const keys = relationKeys(args.table);
|
|
67
|
+
const table = keys.map((key) => grounding.byKey.get(key)).find(Boolean);
|
|
68
|
+
if (!table) {
|
|
69
|
+
return {
|
|
70
|
+
found: false,
|
|
71
|
+
hint: `Table "${args.table}" was not found. Use search_metadata to discover available relations.`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const joinKeys = grounding.joinKeys.filter((join) => join.leftRelation === table.qualifiedRelation || join.rightRelation === table.qualifiedRelation);
|
|
75
|
+
return {
|
|
76
|
+
found: true,
|
|
77
|
+
name: table.name,
|
|
78
|
+
qualifiedRelation: table.qualifiedRelation,
|
|
79
|
+
refForm: table.refForm ?? null,
|
|
80
|
+
kind: table.kind,
|
|
81
|
+
certifiedBlock: table.certifiedBlock ?? false,
|
|
82
|
+
columns: table.columns.map((column) => ({ name: column.name, type: column.type ?? null, description: column.description ?? null })),
|
|
83
|
+
joinKeys: joinKeys.map((join) => ({
|
|
84
|
+
leftRelation: join.leftRelation,
|
|
85
|
+
leftColumn: join.leftColumn,
|
|
86
|
+
rightRelation: join.rightRelation,
|
|
87
|
+
rightColumn: join.rightColumn,
|
|
88
|
+
reason: join.reason,
|
|
89
|
+
})),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
// ─── validate_sql ─────────────────────────────────────────────────────────────
|
|
93
|
+
export const validateSqlInput = {
|
|
94
|
+
sql: z.string().describe('A read-only SELECT/WITH query to validate against the dbt schema.'),
|
|
95
|
+
query: z
|
|
96
|
+
.string()
|
|
97
|
+
.optional()
|
|
98
|
+
.describe('Optional original request, used to scope the grounding to relevant tables.'),
|
|
99
|
+
};
|
|
100
|
+
export async function validateSql(ctx, args) {
|
|
101
|
+
const artifacts = loadArtifacts(ctx);
|
|
102
|
+
if (!artifacts) {
|
|
103
|
+
return { ok: false, code: 'no_schema', error: NO_MANIFEST_HINT, warnings: [] };
|
|
104
|
+
}
|
|
105
|
+
// Ground broadly (so a valid table is never wrongly flagged), optionally
|
|
106
|
+
// ordered by the original request.
|
|
107
|
+
const relevant = args.query ? await selectRelevantModels(artifacts, args.query, { topK: 500 }) : undefined;
|
|
108
|
+
const grounding = buildSchemaGrounding(artifacts, relevant, { limit: 500 });
|
|
109
|
+
const result = validateSqlAgainstGrounding(args.sql, grounding);
|
|
110
|
+
if (result.ok) {
|
|
111
|
+
return { ok: true, warnings: result.warnings, referencedRelations: result.referencedRelations };
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
code: result.code,
|
|
116
|
+
error: result.error,
|
|
117
|
+
warnings: result.warnings,
|
|
118
|
+
referencedRelations: result.referencedRelations,
|
|
119
|
+
offending: result.offending ?? null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../../src/tools/metadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import type { InvariantResult } from '@duckcodeailabs/dql-governance';
|
|
2
3
|
import type { DQLContext } from '../context.js';
|
|
3
4
|
export declare const queryViaBlockInput: {
|
|
4
5
|
name: z.ZodString;
|
|
5
6
|
limit: z.ZodOptional<z.ZodNumber>;
|
|
7
|
+
question: z.ZodOptional<z.ZodString>;
|
|
6
8
|
serverUrl: z.ZodOptional<z.ZodString>;
|
|
7
9
|
};
|
|
8
10
|
/**
|
|
@@ -13,18 +15,22 @@ export declare const queryViaBlockInput: {
|
|
|
13
15
|
export declare function queryViaBlock(ctx: DQLContext, args: {
|
|
14
16
|
name: string;
|
|
15
17
|
limit?: number;
|
|
18
|
+
question?: string;
|
|
16
19
|
serverUrl?: string;
|
|
17
20
|
}): Promise<{
|
|
18
21
|
error: string;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
routeReason?: undefined;
|
|
23
|
+
grainGate?: undefined;
|
|
24
|
+
} | {
|
|
25
|
+
error: string;
|
|
26
|
+
routeReason: string;
|
|
27
|
+
grainGate: {
|
|
28
|
+
allow: false;
|
|
29
|
+
kind: import("@duckcodeailabs/dql-agent").GrainGateKind;
|
|
30
|
+
requestedGrain: string;
|
|
31
|
+
blockGrain: string;
|
|
32
|
+
};
|
|
25
33
|
} | {
|
|
26
|
-
block: string;
|
|
27
|
-
blockPath: string;
|
|
28
34
|
rowCount: number;
|
|
29
35
|
durationMs: number | null;
|
|
30
36
|
columns: {
|
|
@@ -32,6 +38,15 @@ export declare function queryViaBlock(ctx: DQLContext, args: {
|
|
|
32
38
|
type?: string;
|
|
33
39
|
}[];
|
|
34
40
|
rows: unknown[];
|
|
41
|
+
invariantResults?: InvariantResult[] | undefined;
|
|
42
|
+
dataStateDetail?: string | undefined;
|
|
43
|
+
block: string;
|
|
44
|
+
blockPath: string;
|
|
45
|
+
trustLabel: string;
|
|
46
|
+
invariantViolation: boolean;
|
|
47
|
+
dataState: "fresh" | "stale" | "failed" | "unknown";
|
|
35
48
|
error?: undefined;
|
|
49
|
+
routeReason?: undefined;
|
|
50
|
+
grainGate?: undefined;
|
|
36
51
|
}>;
|
|
37
52
|
//# sourceMappingURL=query-via-block.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-via-block.d.ts","sourceRoot":"","sources":["../../src/tools/query-via-block.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,eAAO,MAAM,kBAAkB
|
|
1
|
+
{"version":3,"file":"query-via-block.d.ts","sourceRoot":"","sources":["../../src/tools/query-via-block.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AAYtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAEhD,eAAO,MAAM,kBAAkB;;;;;CAe9B,CAAC;AAEF;;;;GAIG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,UAAU,EACf,IAAI,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE;;;;;;;;;;;;;;;;;cAuFjD,MAAM;eAAS,MAAM;;;;;;;;;;;;;GAmDlD"}
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { buildAnalysisQuestionPlan, grainMatches, requestedGrainFromPlan, } from '@duckcodeailabs/dql-agent';
|
|
5
|
+
import { composeEffectiveTrust, TRUST_QUALIFIER_INVARIANT_VIOLATED, } from '@duckcodeailabs/dql-core';
|
|
4
6
|
export const queryViaBlockInput = {
|
|
5
7
|
name: z.string().describe('Certified block to execute.'),
|
|
6
8
|
limit: z.number().int().min(1).max(10000).optional().describe('Max rows to return.'),
|
|
9
|
+
question: z
|
|
10
|
+
.string()
|
|
11
|
+
.optional()
|
|
12
|
+
.describe('Original question this block is being served for. When provided, query_via_block re-checks the block grain against the requested grain (defense in depth) and refuses on a genuine grain mismatch.'),
|
|
7
13
|
serverUrl: z
|
|
8
14
|
.string()
|
|
9
15
|
.optional()
|
|
@@ -23,6 +29,29 @@ export async function queryViaBlock(ctx, args) {
|
|
|
23
29
|
error: `Block "${args.name}" is "${block.status ?? 'draft'}" — only certified blocks can be executed via MCP.`,
|
|
24
30
|
};
|
|
25
31
|
}
|
|
32
|
+
// Grain-gate defense in depth. When a question is provided, re-run the same
|
|
33
|
+
// grain check the router uses before serving this certified block. A genuine
|
|
34
|
+
// grain/entity mismatch is refused here even if the tool was called directly,
|
|
35
|
+
// so a near-miss certified block can never be served as a confidently-wrong
|
|
36
|
+
// governed answer. Behavior is unchanged when no question is supplied or when
|
|
37
|
+
// the block / question carries no clearly-extractable grain.
|
|
38
|
+
if (args.question) {
|
|
39
|
+
const plan = buildAnalysisQuestionPlan(args.question);
|
|
40
|
+
const requestedGrain = requestedGrainFromPlan(plan);
|
|
41
|
+
const gate = grainMatches(blockToGrainObject(block), requestedGrain);
|
|
42
|
+
if (!gate.allow) {
|
|
43
|
+
return {
|
|
44
|
+
error: `Block "${args.name}" failed the grain gate: ${gate.reason}. Refusing to serve a near-miss certified answer; generate SQL for the requested grain instead.`,
|
|
45
|
+
routeReason: gate.reason,
|
|
46
|
+
grainGate: {
|
|
47
|
+
allow: gate.allow,
|
|
48
|
+
kind: gate.kind,
|
|
49
|
+
requestedGrain: gate.requestedGrainLabel,
|
|
50
|
+
blockGrain: gate.blockGrainLabel,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
26
55
|
// v1.6 — DataLex contract enforcement (the wedge).
|
|
27
56
|
// When a certified block declares datalex_contract, refuse the call if
|
|
28
57
|
// the reference doesn't resolve in the project's loaded DataLex
|
|
@@ -76,13 +105,60 @@ export async function queryViaBlock(ctx, args) {
|
|
|
76
105
|
if (payload.error)
|
|
77
106
|
return { error: payload.error };
|
|
78
107
|
const rows = payload.result?.rows ?? [];
|
|
108
|
+
// Invariant enforcement: the runtime evaluates the block's declared
|
|
109
|
+
// invariants against this run's result. A real violation downgrades the
|
|
110
|
+
// trust label even though the block is certified — "certified" means the
|
|
111
|
+
// logic was reviewed, not that today's data honors every stated guarantee.
|
|
112
|
+
const invariantResults = payload.invariantResults ?? [];
|
|
113
|
+
const invariantViolation = Boolean(payload.invariantViolation);
|
|
114
|
+
const declaredInvariants = block.invariants ?? [];
|
|
115
|
+
// Freshness-aware trust: "certified" is the logic axis; the data axis is the
|
|
116
|
+
// health of the block's upstream dbt models (last-run status + freshness),
|
|
117
|
+
// computed at build time into `block.dataState`. A certified block whose
|
|
118
|
+
// upstream failed/is stale is downgraded to "Certified · upstream failed" /
|
|
119
|
+
// "Certified · stale data". An invariant violation is a stronger signal, so
|
|
120
|
+
// it keeps the qualifier slot when both apply (composeEffectiveTrust honors
|
|
121
|
+
// the existing qualifier first). Missing run_results → undefined dataState →
|
|
122
|
+
// plain "Certified" (backward compatible).
|
|
123
|
+
const dataState = block.dataState;
|
|
124
|
+
const effectiveTrust = composeEffectiveTrust({
|
|
125
|
+
id: 'certified',
|
|
126
|
+
dataState,
|
|
127
|
+
existingQualifier: invariantViolation ? TRUST_QUALIFIER_INVARIANT_VIOLATED : undefined,
|
|
128
|
+
});
|
|
79
129
|
return {
|
|
80
130
|
block: args.name,
|
|
81
131
|
blockPath: block.filePath,
|
|
132
|
+
trustLabel: effectiveTrust.display,
|
|
133
|
+
invariantViolation,
|
|
134
|
+
dataState: dataState ?? 'unknown',
|
|
135
|
+
...(block.dataStateDetail
|
|
136
|
+
? { dataStateDetail: block.dataStateDetail }
|
|
137
|
+
: {}),
|
|
138
|
+
...(declaredInvariants.length > 0 || invariantResults.length > 0
|
|
139
|
+
? { invariantResults }
|
|
140
|
+
: {}),
|
|
82
141
|
rowCount: rows.length,
|
|
83
142
|
durationMs: payload.result?.executionTime ?? null,
|
|
84
143
|
columns: payload.result?.columns ?? [],
|
|
85
144
|
rows: args.limit ? rows.slice(0, args.limit) : rows,
|
|
86
145
|
};
|
|
87
146
|
}
|
|
147
|
+
/**
|
|
148
|
+
* Adapt a manifest block into the minimal `MetadataObject` shape the grain gate
|
|
149
|
+
* reads (it only inspects `payload.grain`, `payload.declaredOutputs`, and
|
|
150
|
+
* `payload.entities`).
|
|
151
|
+
*/
|
|
152
|
+
function blockToGrainObject(block) {
|
|
153
|
+
return {
|
|
154
|
+
objectKey: `dql:block:${block.name}`,
|
|
155
|
+
objectType: 'dql_block',
|
|
156
|
+
name: block.name,
|
|
157
|
+
payload: {
|
|
158
|
+
grain: block.grain,
|
|
159
|
+
declaredOutputs: block.declaredOutputs ?? [],
|
|
160
|
+
entities: block.entities ?? [],
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
88
164
|
//# sourceMappingURL=query-via-block.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-via-block.js","sourceRoot":"","sources":["../../src/tools/query-via-block.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"query-via-block.js","sourceRoot":"","sources":["../../src/tools/query-via-block.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import type { DQLContext } from '../context.js';
|
|
3
3
|
import { type LocalContextPack } from '@duckcodeailabs/dql-agent';
|
|
4
|
+
import type { DataLexConformance, DataLexRelationship, JoinPathResolution } from '@duckcodeailabs/dql-core';
|
|
4
5
|
export declare const queryViaMetadataInput: {
|
|
5
6
|
question: z.ZodString;
|
|
6
7
|
proposedSql: z.ZodOptional<z.ZodString>;
|
|
@@ -70,6 +71,7 @@ export declare function queryViaMetadata(ctx: DQLContext, args: {
|
|
|
70
71
|
reviewStatus: string;
|
|
71
72
|
planningOnly: boolean;
|
|
72
73
|
contextPack: LocalContextPack | undefined;
|
|
74
|
+
datalexJoinGuidance: DataLexJoinGuidance | undefined;
|
|
73
75
|
routeDecision: import("@duckcodeailabs/dql-agent").MetadataRouteDecision | undefined;
|
|
74
76
|
allowedSqlContext: import("@duckcodeailabs/dql-agent").MetadataAllowedSqlContext | undefined;
|
|
75
77
|
selectedRelations: {
|
|
@@ -257,6 +259,7 @@ export declare function queryViaMetadata(ctx: DQLContext, args: {
|
|
|
257
259
|
proposedSql: string;
|
|
258
260
|
draftBlock: undefined;
|
|
259
261
|
planningOnly?: undefined;
|
|
262
|
+
datalexJoinGuidance?: undefined;
|
|
260
263
|
routeDecision?: undefined;
|
|
261
264
|
allowedSqlContext?: undefined;
|
|
262
265
|
selectedRelations?: undefined;
|
|
@@ -348,6 +351,7 @@ export declare function queryViaMetadata(ctx: DQLContext, args: {
|
|
|
348
351
|
assumptions: string[];
|
|
349
352
|
};
|
|
350
353
|
contextPack: LocalContextPack | undefined;
|
|
354
|
+
datalexJoinGuidance: DataLexJoinGuidance | undefined;
|
|
351
355
|
reason: string;
|
|
352
356
|
proposedSql: string;
|
|
353
357
|
draftBlock: {
|
|
@@ -457,6 +461,7 @@ export declare function queryViaMetadata(ctx: DQLContext, args: {
|
|
|
457
461
|
validationWarnings: string[];
|
|
458
462
|
reviewStatus?: undefined;
|
|
459
463
|
planningOnly?: undefined;
|
|
464
|
+
datalexJoinGuidance?: undefined;
|
|
460
465
|
routeDecision?: undefined;
|
|
461
466
|
allowedSqlContext?: undefined;
|
|
462
467
|
selectedRelations?: undefined;
|
|
@@ -550,6 +555,7 @@ export declare function queryViaMetadata(ctx: DQLContext, args: {
|
|
|
550
555
|
assumptions: string[];
|
|
551
556
|
};
|
|
552
557
|
contextPack: LocalContextPack | undefined;
|
|
558
|
+
datalexJoinGuidance: DataLexJoinGuidance | undefined;
|
|
553
559
|
reason: string;
|
|
554
560
|
question: string;
|
|
555
561
|
rowCount: number;
|
|
@@ -577,6 +583,54 @@ export declare function queryViaMetadata(ctx: DQLContext, args: {
|
|
|
577
583
|
errorCode?: undefined;
|
|
578
584
|
error?: undefined;
|
|
579
585
|
}>;
|
|
586
|
+
export interface DataLexJoinGuidance {
|
|
587
|
+
source: 'datalex_manifest';
|
|
588
|
+
note: string;
|
|
589
|
+
entities: Array<{
|
|
590
|
+
concept: string;
|
|
591
|
+
domain?: string;
|
|
592
|
+
canonicalKey?: string[];
|
|
593
|
+
physical: string[];
|
|
594
|
+
}>;
|
|
595
|
+
joins: Array<{
|
|
596
|
+
from: string;
|
|
597
|
+
to: string;
|
|
598
|
+
on?: string;
|
|
599
|
+
cardinality?: string;
|
|
600
|
+
fansOut: boolean;
|
|
601
|
+
guidance: string;
|
|
602
|
+
}>;
|
|
603
|
+
unresolved?: Array<{
|
|
604
|
+
from: string;
|
|
605
|
+
to: string;
|
|
606
|
+
reason: string;
|
|
607
|
+
message: string;
|
|
608
|
+
}>;
|
|
609
|
+
}
|
|
610
|
+
interface JoinGuidanceRegistry {
|
|
611
|
+
conformance(): DataLexConformance[];
|
|
612
|
+
relationships(): DataLexRelationship[];
|
|
613
|
+
joinPath(base: string, target: string, opts?: {
|
|
614
|
+
baseDomain?: string;
|
|
615
|
+
targetDomain?: string;
|
|
616
|
+
}): JoinPathResolution;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Build grain-safe join guidance from the DataLex manifest for the entities a
|
|
620
|
+
* Tier-2 question touches. This is what turns conformance + typed relationships
|
|
621
|
+
* into something the agent can act on BEFORE it writes SQL: the canonical key to
|
|
622
|
+
* join each business concept on, and — for each pair — whether joining one onto
|
|
623
|
+
* the other fans out (and therefore needs aggregation to stay grain-safe).
|
|
624
|
+
*
|
|
625
|
+
* `refs` are the tables/entities the agent thinks are involved; physical model
|
|
626
|
+
* names (`dim_customer`), physical entity names (`DimCustomer`), and concept
|
|
627
|
+
* names (`Customer`) all resolve. With none resolved we surface the whole
|
|
628
|
+
* (small) conformance map so the agent still sees the modeled joins.
|
|
629
|
+
*
|
|
630
|
+
* Returns undefined when no DataLex manifest is loaded — Tier-2 must keep working
|
|
631
|
+
* for projects that never adopted DataLex (graduated trust).
|
|
632
|
+
*/
|
|
633
|
+
export declare function buildDataLexJoinGuidance(registry: JoinGuidanceRegistry, refs: string[]): DataLexJoinGuidance | undefined;
|
|
580
634
|
type MetadataResearchIntent = 'diagnose_change' | 'driver_breakdown' | 'segment_compare' | 'entity_drilldown' | 'anomaly_investigation' | 'trust_gap_review';
|
|
581
635
|
type MetadataFollowUpContext = {
|
|
582
636
|
kind: 'generic' | 'drilldown';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query-via-metadata.d.ts","sourceRoot":"","sources":["../../src/tools/query-via-metadata.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAML,KAAK,gBAAgB,EACtB,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"query-via-metadata.d.ts","sourceRoot":"","sources":["../../src/tools/query-via-metadata.ts"],"names":[],"mappings":"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"}
|
|
@@ -89,6 +89,12 @@ export async function queryViaMetadata(ctx, args) {
|
|
|
89
89
|
const intent = normalizeMetadataResearchIntent(args.intent, args.question);
|
|
90
90
|
const contextPack = await buildTier2ContextPack(ctx.projectRoot, args.question, args.contextPackId, args.followUp);
|
|
91
91
|
const hasInspectedContext = hasInspectedMetadataContext(contextPack);
|
|
92
|
+
// DataLex modeled join semantics (canonical keys + grain-safe orientation) for
|
|
93
|
+
// the entities this question touches. Undefined when no manifest is loaded.
|
|
94
|
+
const datalexJoinGuidance = buildDataLexJoinGuidance(ctx.datalexRegistry, [
|
|
95
|
+
...(args.upstreamRefs ?? []),
|
|
96
|
+
...(args.proposedEntity ? [args.proposedEntity] : []),
|
|
97
|
+
]);
|
|
92
98
|
const contextNeedsClarification = hasInspectedContext && contextPack?.routeDecision.route === 'clarify';
|
|
93
99
|
if (contextNeedsClarification || !args.proposedSql?.trim()) {
|
|
94
100
|
recordTier2QueryRun(ctx.projectRoot, {
|
|
@@ -104,6 +110,7 @@ export async function queryViaMetadata(ctx, args) {
|
|
|
104
110
|
reviewStatus: 'none',
|
|
105
111
|
planningOnly: true,
|
|
106
112
|
contextPack,
|
|
113
|
+
datalexJoinGuidance,
|
|
107
114
|
routeDecision: contextPack?.routeDecision,
|
|
108
115
|
allowedSqlContext: contextPack?.allowedSqlContext,
|
|
109
116
|
selectedRelations: contextPack?.retrievalDiagnostics.selectedRelations?.slice(0, 12) ?? [],
|
|
@@ -183,6 +190,7 @@ export async function queryViaMetadata(ctx, args) {
|
|
|
183
190
|
trustStatus: metadataTrustStatus('draft_ready', intent, draftBlock, undefined, contextValidation.warnings),
|
|
184
191
|
evidence: metadataEvidence(intent, args, { status: 'dry_run', draftBlock, validationWarnings: contextValidation.warnings }, contextPack),
|
|
185
192
|
contextPack,
|
|
193
|
+
datalexJoinGuidance,
|
|
186
194
|
reason: 'dryRun=true; SQL not executed. Returned proposal only.',
|
|
187
195
|
proposedSql,
|
|
188
196
|
draftBlock,
|
|
@@ -273,6 +281,7 @@ export async function queryViaMetadata(ctx, args) {
|
|
|
273
281
|
validationWarnings: contextValidation.warnings,
|
|
274
282
|
}, contextPack),
|
|
275
283
|
contextPack,
|
|
284
|
+
datalexJoinGuidance,
|
|
276
285
|
reason: 'no certified block matched the question; result derived from manifest + dbt schema',
|
|
277
286
|
question: args.question,
|
|
278
287
|
rowCount: rows.length,
|
|
@@ -287,6 +296,109 @@ export async function queryViaMetadata(ctx, args) {
|
|
|
287
296
|
: undefined,
|
|
288
297
|
};
|
|
289
298
|
}
|
|
299
|
+
/**
|
|
300
|
+
* Build grain-safe join guidance from the DataLex manifest for the entities a
|
|
301
|
+
* Tier-2 question touches. This is what turns conformance + typed relationships
|
|
302
|
+
* into something the agent can act on BEFORE it writes SQL: the canonical key to
|
|
303
|
+
* join each business concept on, and — for each pair — whether joining one onto
|
|
304
|
+
* the other fans out (and therefore needs aggregation to stay grain-safe).
|
|
305
|
+
*
|
|
306
|
+
* `refs` are the tables/entities the agent thinks are involved; physical model
|
|
307
|
+
* names (`dim_customer`), physical entity names (`DimCustomer`), and concept
|
|
308
|
+
* names (`Customer`) all resolve. With none resolved we surface the whole
|
|
309
|
+
* (small) conformance map so the agent still sees the modeled joins.
|
|
310
|
+
*
|
|
311
|
+
* Returns undefined when no DataLex manifest is loaded — Tier-2 must keep working
|
|
312
|
+
* for projects that never adopted DataLex (graduated trust).
|
|
313
|
+
*/
|
|
314
|
+
export function buildDataLexJoinGuidance(registry, refs) {
|
|
315
|
+
// Gate on conformance, NOT registry.isLoaded() — that's contracts-based, and a
|
|
316
|
+
// modeling-primary manifest carries conformance + relationships with zero
|
|
317
|
+
// contracts. No conformance (or no manifest) => no guidance; Tier-2 still runs.
|
|
318
|
+
const conformance = registry.conformance();
|
|
319
|
+
if (conformance.length === 0)
|
|
320
|
+
return undefined;
|
|
321
|
+
const conceptByKey = new Map();
|
|
322
|
+
for (const c of conformance) {
|
|
323
|
+
conceptByKey.set(c.concept.toLowerCase(), c);
|
|
324
|
+
for (const p of c.physical ?? []) {
|
|
325
|
+
if (p.entity)
|
|
326
|
+
conceptByKey.set(p.entity.toLowerCase(), c);
|
|
327
|
+
if (p.binding?.ref)
|
|
328
|
+
conceptByKey.set(p.binding.ref.toLowerCase(), c);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
const involved = [];
|
|
332
|
+
const seen = new Set();
|
|
333
|
+
for (const ref of refs) {
|
|
334
|
+
const c = conceptByKey.get(String(ref ?? '').toLowerCase());
|
|
335
|
+
if (c && !seen.has(c.concept.toLowerCase())) {
|
|
336
|
+
seen.add(c.concept.toLowerCase());
|
|
337
|
+
involved.push(c);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Scope to the involved concepts; with none resolved, fall back to the whole
|
|
341
|
+
// (bounded) conformance map so the agent still gets the modeled join keys.
|
|
342
|
+
const concepts = involved.length > 0 ? involved : conformance.slice(0, 12);
|
|
343
|
+
const entities = concepts.map((c) => ({
|
|
344
|
+
concept: c.concept,
|
|
345
|
+
domain: c.domain,
|
|
346
|
+
canonicalKey: c.canonical_key,
|
|
347
|
+
physical: (c.physical ?? [])
|
|
348
|
+
.map((p) => p.binding?.ref)
|
|
349
|
+
.filter((r) => Boolean(r)),
|
|
350
|
+
}));
|
|
351
|
+
const joins = [];
|
|
352
|
+
const unresolved = [];
|
|
353
|
+
for (let i = 0; i < concepts.length; i += 1) {
|
|
354
|
+
for (let j = i + 1; j < concepts.length; j += 1) {
|
|
355
|
+
const a = concepts[i];
|
|
356
|
+
const b = concepts[j];
|
|
357
|
+
let jp = registry.joinPath(a.concept, b.concept, {
|
|
358
|
+
baseDomain: a.domain,
|
|
359
|
+
targetDomain: b.domain,
|
|
360
|
+
});
|
|
361
|
+
// Prefer the non-fanning orientation (join the "one" side onto the "many"
|
|
362
|
+
// side, i.e. the dimension onto the fact) so the primary guidance is the
|
|
363
|
+
// grain-safe join; the fan-out direction is still called out in the text.
|
|
364
|
+
if (jp.ok && jp.fansOut) {
|
|
365
|
+
const flipped = registry.joinPath(b.concept, a.concept, {
|
|
366
|
+
baseDomain: b.domain,
|
|
367
|
+
targetDomain: a.domain,
|
|
368
|
+
});
|
|
369
|
+
if (flipped.ok && !flipped.fansOut)
|
|
370
|
+
jp = flipped;
|
|
371
|
+
}
|
|
372
|
+
if (!jp.ok) {
|
|
373
|
+
if (jp.reason === 'ambiguous') {
|
|
374
|
+
unresolved.push({ from: a.concept, to: b.concept, reason: jp.reason, message: jp.message });
|
|
375
|
+
}
|
|
376
|
+
// 'no_relationship' is expected for unrelated pairs — skip quietly.
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
const on = jp.base.column && jp.target.column
|
|
380
|
+
? `${jp.base.entity}.${jp.base.column} = ${jp.target.entity}.${jp.target.column}`
|
|
381
|
+
: undefined;
|
|
382
|
+
joins.push({
|
|
383
|
+
from: jp.base.entity,
|
|
384
|
+
to: jp.target.entity,
|
|
385
|
+
on,
|
|
386
|
+
cardinality: jp.cardinality,
|
|
387
|
+
fansOut: jp.fansOut,
|
|
388
|
+
guidance: jp.fansOut
|
|
389
|
+
? `${jp.base.entity} ↔ ${jp.target.entity} is ${jp.cardinality}; either direction can multiply rows — aggregate to one grain (GROUP BY the canonical key) before joining.`
|
|
390
|
+
: `Each ${jp.base.entity} has one ${jp.target.entity}: join ${jp.target.entity} onto ${jp.base.entity}${on ? ` on ${on}` : ''} without fan-out. Aggregating ${jp.base.entity} per ${jp.target.entity} fans out — GROUP BY the ${jp.target.entity} key.`,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
source: 'datalex_manifest',
|
|
396
|
+
note: 'Modeled join semantics from DataLex. Join each concept on its canonical key and respect fan-out to keep results grain-safe.',
|
|
397
|
+
entities,
|
|
398
|
+
joins: joins.slice(0, 25),
|
|
399
|
+
...(unresolved.length > 0 ? { unresolved } : {}),
|
|
400
|
+
};
|
|
401
|
+
}
|
|
290
402
|
function normalizeMetadataResearchIntent(value, question) {
|
|
291
403
|
if (value === 'diagnose_change'
|
|
292
404
|
|| value === 'driver_breakdown'
|