@duckcodeailabs/dql-agent 1.8.3 → 1.8.4
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/agentic/answer-contract.d.ts +1 -0
- package/dist/agentic/answer-contract.d.ts.map +1 -1
- package/dist/agentic/answer-contract.js.map +1 -1
- package/dist/agentic/toolset.d.ts +7 -0
- package/dist/agentic/toolset.d.ts.map +1 -1
- package/dist/agentic/toolset.js +24 -3
- package/dist/agentic/toolset.js.map +1 -1
- package/dist/answer-loop.d.ts +10 -3
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +275 -14
- package/dist/answer-loop.js.map +1 -1
- package/dist/grounding/context-ledger.d.ts +2 -0
- package/dist/grounding/context-ledger.d.ts.map +1 -1
- package/dist/grounding/context-ledger.js +5 -1
- package/dist/grounding/context-ledger.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metadata/block-fit.js +10 -2
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/dbt-first-safety.d.ts +1 -1
- package/dist/metadata/dbt-first-safety.d.ts.map +1 -1
- package/dist/metadata/dbt-first-safety.js +2 -2
- package/dist/metadata/dbt-first-safety.js.map +1 -1
- package/dist/metadata/sql-context-validation.d.ts +2 -0
- package/dist/metadata/sql-context-validation.d.ts.map +1 -1
- package/dist/metadata/sql-context-validation.js +4 -4
- package/dist/metadata/sql-context-validation.js.map +1 -1
- package/dist/metadata/sql-grounding.d.ts +1 -1
- package/dist/metadata/sql-grounding.d.ts.map +1 -1
- package/dist/metadata/sql-grounding.js +2 -2
- package/dist/metadata/sql-grounding.js.map +1 -1
- package/dist/semantic-bridge/compose.d.ts +10 -0
- package/dist/semantic-bridge/compose.d.ts.map +1 -1
- package/dist/semantic-bridge/compose.js +50 -18
- package/dist/semantic-bridge/compose.js.map +1 -1
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +5 -0
- package/dist/tools/registry.js.map +1 -1
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -29,7 +29,7 @@ import { evaluateDbtFirstGeneratedSql } from './metadata/dbt-first-safety.js';
|
|
|
29
29
|
import { planAnalyticalPath, humanizeAnalyticalEntityId, analyticalPolicyUserFacingReason, } from './metadata/analytical-policy.js';
|
|
30
30
|
import { planCertifiedAdaptation } from './metadata/block-adapt.js';
|
|
31
31
|
import { compactSqlSnippet, extractSimpleSelectShape, selectExpressionOutputName, } from './metadata/sql-shape.js';
|
|
32
|
-
import { composeSemanticQueryForQuestion, composeSemanticQueryFromMembers, } from './semantic-bridge/compose.js';
|
|
32
|
+
import { composeSemanticQueryForQuestion, composeSemanticQueryFromCompiledMembers, composeSemanticQueryFromMembers, } from './semantic-bridge/compose.js';
|
|
33
33
|
import { runAgenticToolLoop } from './agentic/tool-loop.js';
|
|
34
34
|
import { buildSemanticStageTools } from './agentic/toolset.js';
|
|
35
35
|
import { deriveAgenticTrust } from './agentic/answer-contract.js';
|
|
@@ -125,6 +125,93 @@ export function resolveAgentFilterValueBindings(value, schemaContext) {
|
|
|
125
125
|
&& normalizeValueIndexText(candidate.canonicalValue) === normalizeValueIndexText(best.canonicalValue))
|
|
126
126
|
.map(({ distance: _distance, ...binding }) => binding);
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* AGT-005/AGT-012 — turn bounded value-index evidence into typed question
|
|
130
|
+
* state before route compatibility is evaluated. The value index, never an
|
|
131
|
+
* LLM or a metadata-token match, owns the canonical warehouse member. A value
|
|
132
|
+
* that matches multiple dimensions is intentionally left unbound so the
|
|
133
|
+
* certified lane cannot silently run with the wrong parameter.
|
|
134
|
+
*/
|
|
135
|
+
function questionPlanWithResolvedMemberBindings(plan, schemaContext) {
|
|
136
|
+
const existing = plan.requestedShape.memberBindings ?? [];
|
|
137
|
+
const existingValues = new Set(existing.flatMap((binding) => binding.values.map(normalizeValueIndexText)));
|
|
138
|
+
const candidates = Array.from(new Set([
|
|
139
|
+
...(plan.valueMentions ?? []).map((mention) => mention.text.trim()),
|
|
140
|
+
...(plan.requestedShape.filters ?? []).map((value) => value.trim()),
|
|
141
|
+
].filter(Boolean)));
|
|
142
|
+
const resolved = candidates.flatMap((value) => {
|
|
143
|
+
if (existingValues.has(normalizeValueIndexText(value)))
|
|
144
|
+
return [];
|
|
145
|
+
const bindings = resolveAgentFilterValueBindings(value, schemaContext);
|
|
146
|
+
const dimensions = Array.from(new Set(bindings.map((binding) => binding.column.trim()).filter(Boolean)));
|
|
147
|
+
const canonicalValues = Array.from(new Set(bindings.map((binding) => binding.canonicalValue.trim()).filter(Boolean)));
|
|
148
|
+
if (dimensions.length !== 1 || canonicalValues.length !== 1)
|
|
149
|
+
return [];
|
|
150
|
+
const binding = bindings[0];
|
|
151
|
+
return [{
|
|
152
|
+
dimension: dimensions[0],
|
|
153
|
+
values: [canonicalValues[0]],
|
|
154
|
+
source: 'question',
|
|
155
|
+
confidence: binding.match === 'fuzzy' ? 'unique_partial' : 'exact',
|
|
156
|
+
}];
|
|
157
|
+
});
|
|
158
|
+
if (resolved.length === 0)
|
|
159
|
+
return plan;
|
|
160
|
+
const byIdentity = new Map();
|
|
161
|
+
for (const binding of [...existing, ...resolved]) {
|
|
162
|
+
const key = `${normalizeValueIndexText(binding.dimension)}\0${binding.values.map(normalizeValueIndexText).join('\0')}`;
|
|
163
|
+
if (!byIdentity.has(key))
|
|
164
|
+
byIdentity.set(key, binding);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
...plan,
|
|
168
|
+
requestedShape: {
|
|
169
|
+
...plan.requestedShape,
|
|
170
|
+
memberBindings: Array.from(byIdentity.values()),
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Map typed member bindings to a block's declared parameter contract. The
|
|
176
|
+
* mapping is identifier-based (parameter name, semantic field, or declared
|
|
177
|
+
* filter binding) and must select exactly one parameter. Structural SQL is
|
|
178
|
+
* never accepted as a value.
|
|
179
|
+
*/
|
|
180
|
+
function certifiedInvocationInputs(block, plan) {
|
|
181
|
+
const definitions = block.parameters ?? [];
|
|
182
|
+
if (definitions.length === 0)
|
|
183
|
+
return {};
|
|
184
|
+
const parameters = {};
|
|
185
|
+
const parameterSources = {};
|
|
186
|
+
const declaredFilterBindings = new Map((block.filterBindings ?? []).map((entry) => [entry.filter, entry.binding]));
|
|
187
|
+
const topN = plan.requestedShape.topN?.n;
|
|
188
|
+
if (topN) {
|
|
189
|
+
const limitCandidates = definitions.filter((definition) => definition.name === 'top_n' || definition.binding?.kind === 'limit');
|
|
190
|
+
if (limitCandidates.length === 1) {
|
|
191
|
+
parameters[limitCandidates[0].name] = topN;
|
|
192
|
+
parameterSources[limitCandidates[0].name] = 'question';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
for (const member of plan.requestedShape.memberBindings ?? []) {
|
|
196
|
+
const candidates = definitions.filter((definition) => {
|
|
197
|
+
if (Object.prototype.hasOwnProperty.call(parameters, definition.name))
|
|
198
|
+
return false;
|
|
199
|
+
const target = definition.binding?.kind === 'semantic_filter'
|
|
200
|
+
? definition.binding.field
|
|
201
|
+
: declaredFilterBindings.get(definition.name) ?? definition.name;
|
|
202
|
+
return outputConceptMatches(target, member.dimension);
|
|
203
|
+
});
|
|
204
|
+
if (candidates.length !== 1)
|
|
205
|
+
continue;
|
|
206
|
+
const definition = candidates[0];
|
|
207
|
+
const values = Array.from(new Set(member.values.map((value) => value.trim()).filter(Boolean)));
|
|
208
|
+
if (values.length === 0 || (!definition.type.endsWith('[]') && values.length !== 1))
|
|
209
|
+
continue;
|
|
210
|
+
parameters[definition.name] = definition.type.endsWith('[]') ? values : values[0];
|
|
211
|
+
parameterSources[definition.name] = member.source === 'prior_result' ? 'prior_result' : 'question';
|
|
212
|
+
}
|
|
213
|
+
return Object.keys(parameters).length > 0 ? { parameters, parameterSources } : {};
|
|
214
|
+
}
|
|
128
215
|
function damerauLevenshteinDistance(left, right) {
|
|
129
216
|
const rows = left.length + 1;
|
|
130
217
|
const columns = right.length + 1;
|
|
@@ -181,6 +268,12 @@ function renderContextValidationRefusalForUser(code, machineError, memberBinding
|
|
|
181
268
|
case 'unsafe_sql':
|
|
182
269
|
return 'The drafted query used a statement type that is not allowed in this governed preview, so I did not run it.';
|
|
183
270
|
case 'insufficient_context':
|
|
271
|
+
if (/could not be parsed|parse error|syntax/i.test(machineError)) {
|
|
272
|
+
return 'I drafted a query, but its SQL syntax did not match the connected warehouse, so I did not run it. The failed draft is available in Inspect; retrying will generate a warehouse-specific query.';
|
|
273
|
+
}
|
|
274
|
+
return machineError.trim().length > 0 && !/inspect_metadata_context/i.test(machineError)
|
|
275
|
+
? `I could not prepare a governed query yet: ${machineError.replace(/\s*Use inspect_metadata_context[^.]*\.\s*$/i, '').trim()}`
|
|
276
|
+
: 'I could not prepare a governed query from the retrieved metadata. Name the specific metric or table and how to break it down, and I can generate a review-required draft.';
|
|
184
277
|
default:
|
|
185
278
|
return machineError.trim().length > 0 && !/inspect_metadata_context/i.test(machineError)
|
|
186
279
|
? `I could not prepare a governed query yet: ${machineError.replace(/\s*Use inspect_metadata_context[^.]*\.\s*$/i, '').trim()}`
|
|
@@ -498,9 +591,10 @@ async function runAnswerLoop(input) {
|
|
|
498
591
|
const considered = mergeHits(artifactHits, semanticHits, manifestHits, kg.search({ query: question, domain, limit: 10 })).slice(0, 30);
|
|
499
592
|
const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack, scopedContextPack), scopedContextPack);
|
|
500
593
|
const catalogRoute = input.contextPack?.routeDecision;
|
|
501
|
-
const
|
|
594
|
+
const baseQuestionPlan = input.contextPack?.questionPlan?.requestedShape
|
|
502
595
|
? input.contextPack.questionPlan
|
|
503
596
|
: buildAnalysisQuestionPlan(question, input.followUp);
|
|
597
|
+
const questionPlan = questionPlanWithResolvedMemberBindings(baseQuestionPlan, schemaContext);
|
|
504
598
|
// Retrieval may surface a high-trust block because its source tables and
|
|
505
599
|
// vocabulary overlap the question even when its output contract does not.
|
|
506
600
|
// Keep such candidates in the audit trail, but do not put their SQL or a stale
|
|
@@ -546,7 +640,15 @@ async function runAnswerLoop(input) {
|
|
|
546
640
|
kg,
|
|
547
641
|
})
|
|
548
642
|
: null;
|
|
549
|
-
const
|
|
643
|
+
const explicitlyRequestedCertifiedBlock = effectiveBlockHints.some((hint) => {
|
|
644
|
+
const node = kg.getNode(`block:${hint}`);
|
|
645
|
+
return Boolean(node
|
|
646
|
+
&& node.status === 'certified'
|
|
647
|
+
&& objectNameInQuestion(question, node)
|
|
648
|
+
&& /\b(run|use|open|show|execute|certified|saved|block)\b/i.test(question));
|
|
649
|
+
});
|
|
650
|
+
const shouldTryCertifiedRoute = shouldUseCertifiedRoute(catalogRoute, intent)
|
|
651
|
+
|| explicitlyRequestedCertifiedBlock;
|
|
550
652
|
const catalogCertifiedHit = shouldTryCertifiedRoute
|
|
551
653
|
? certifiedHitFromContextPack(input.contextPack, kg)
|
|
552
654
|
: null;
|
|
@@ -613,7 +715,10 @@ async function runAnswerLoop(input) {
|
|
|
613
715
|
let executionError;
|
|
614
716
|
if (artifactHit.node.kind === 'block' && input.executeCertifiedBlock) {
|
|
615
717
|
try {
|
|
616
|
-
result = await input.executeCertifiedBlock(artifactHit.node, {
|
|
718
|
+
result = await input.executeCertifiedBlock(artifactHit.node, {
|
|
719
|
+
question,
|
|
720
|
+
...certifiedInvocationInputs(artifactHit.node, questionPlan),
|
|
721
|
+
});
|
|
617
722
|
result = trimResultToRequestedTopN(result, questionPlan);
|
|
618
723
|
}
|
|
619
724
|
catch (err) {
|
|
@@ -928,6 +1033,8 @@ async function runAnswerLoop(input) {
|
|
|
928
1033
|
let governedMetricAnswer = false;
|
|
929
1034
|
const semanticBridgeToolCalls = [];
|
|
930
1035
|
let semanticBridgeAnswer;
|
|
1036
|
+
let semanticRuntimeFailure;
|
|
1037
|
+
let semanticRuntimeCompiledAnswer = false;
|
|
931
1038
|
if (input.semanticLayer && semanticMetricMatch) {
|
|
932
1039
|
semanticBridgeAnswer = composeSemanticQueryForQuestion({
|
|
933
1040
|
semanticLayer: input.semanticLayer,
|
|
@@ -939,6 +1046,44 @@ async function runAnswerLoop(input) {
|
|
|
939
1046
|
...(input.semanticDriver ? { driver: input.semanticDriver } : {}),
|
|
940
1047
|
...(input.semanticTableMapping ? { tableMapping: input.semanticTableMapping } : {}),
|
|
941
1048
|
});
|
|
1049
|
+
// Exact scalar metrics need no AI member selection. If the native compiler
|
|
1050
|
+
// cannot express a derived/ratio/cumulative metric, hand the identifier to
|
|
1051
|
+
// the host's shared dbt Cloud/MetricFlow adapter immediately.
|
|
1052
|
+
if (!semanticBridgeAnswer
|
|
1053
|
+
&& input.semanticQueryCompiler
|
|
1054
|
+
&& questionPlan.requestedShape.dimensions.length === 0
|
|
1055
|
+
&& questionPlan.dimensionTerms.length === 0
|
|
1056
|
+
&& questionPlan.requestedShape.filters.length === 0
|
|
1057
|
+
&& questionPlan.timeTerms.length === 0) {
|
|
1058
|
+
const matchedName = input.semanticLayer.listMetrics().find((metric) => metric.name === semanticMetricMatch.metric.name
|
|
1059
|
+
|| metric.name.endsWith(`.${semanticMetricMatch.metric.name}`)
|
|
1060
|
+
|| semanticMetricMatch.metric.name.endsWith(`.${metric.name}`))?.name;
|
|
1061
|
+
if (matchedName) {
|
|
1062
|
+
const selection = { metrics: [matchedName], dimensions: [] };
|
|
1063
|
+
try {
|
|
1064
|
+
const compiled = await input.semanticQueryCompiler(selection);
|
|
1065
|
+
semanticBridgeAnswer = composeSemanticQueryFromCompiledMembers({
|
|
1066
|
+
semanticLayer: input.semanticLayer,
|
|
1067
|
+
question,
|
|
1068
|
+
selection,
|
|
1069
|
+
sql: compiled.sql,
|
|
1070
|
+
});
|
|
1071
|
+
if (semanticBridgeAnswer) {
|
|
1072
|
+
semanticRuntimeCompiledAnswer = compiled.engine !== 'native';
|
|
1073
|
+
semanticBridgeToolCalls.push({
|
|
1074
|
+
name: 'compile_semantic_query',
|
|
1075
|
+
status: 'checked',
|
|
1076
|
+
inputSummary: `metric: ${matchedName}`,
|
|
1077
|
+
outputSummary: `Compiled through ${compiled.engine}`,
|
|
1078
|
+
order: semanticBridgeToolCalls.length + 1,
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
catch (error) {
|
|
1083
|
+
semanticRuntimeFailure = error instanceof Error ? error.message : String(error);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
942
1087
|
// Lane-2 LLM fallback: the deterministic token matcher missed, but a metric
|
|
943
1088
|
// matched so the semantic layer IS relevant. Spend ONE call to have the model
|
|
944
1089
|
// pick members (the query_semantic_model contract); the compiler still owns the
|
|
@@ -953,13 +1098,37 @@ async function runAnswerLoop(input) {
|
|
|
953
1098
|
reasoningEffort: input.reasoningEffort,
|
|
954
1099
|
});
|
|
955
1100
|
if (selection) {
|
|
956
|
-
|
|
1101
|
+
let composed = composeSemanticQueryFromMembers({
|
|
957
1102
|
semanticLayer: input.semanticLayer,
|
|
958
1103
|
question,
|
|
959
1104
|
selection,
|
|
960
1105
|
...(input.semanticDriver ? { driver: input.semanticDriver } : {}),
|
|
961
1106
|
...(input.semanticTableMapping ? { tableMapping: input.semanticTableMapping } : {}),
|
|
962
1107
|
});
|
|
1108
|
+
if (!composed && input.semanticQueryCompiler) {
|
|
1109
|
+
try {
|
|
1110
|
+
const compiled = await input.semanticQueryCompiler(selection);
|
|
1111
|
+
composed = composeSemanticQueryFromCompiledMembers({
|
|
1112
|
+
semanticLayer: input.semanticLayer,
|
|
1113
|
+
question,
|
|
1114
|
+
selection,
|
|
1115
|
+
sql: compiled.sql,
|
|
1116
|
+
});
|
|
1117
|
+
if (composed) {
|
|
1118
|
+
semanticRuntimeCompiledAnswer = compiled.engine !== 'native';
|
|
1119
|
+
semanticBridgeToolCalls.push({
|
|
1120
|
+
name: 'compile_semantic_query',
|
|
1121
|
+
status: 'checked',
|
|
1122
|
+
inputSummary: `metrics: ${selection.metrics.join(', ')}${selection.dimensions?.length ? `; by ${selection.dimensions.join(', ')}` : ''}`,
|
|
1123
|
+
outputSummary: `Compiled through ${compiled.engine}`,
|
|
1124
|
+
order: semanticBridgeToolCalls.length + 1,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
catch (error) {
|
|
1129
|
+
semanticRuntimeFailure = error instanceof Error ? error.message : String(error);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
963
1132
|
// Coverage guard: if the question asked for a breakdown but the LLM
|
|
964
1133
|
// selection produced none, the governed answer would silently DROP the
|
|
965
1134
|
// requested grouping (governed-but-wrong). Fall through to Lane-3
|
|
@@ -984,6 +1153,36 @@ async function runAnswerLoop(input) {
|
|
|
984
1153
|
}
|
|
985
1154
|
}
|
|
986
1155
|
}
|
|
1156
|
+
if (!semanticBridgeAnswer && semanticRuntimeFailure) {
|
|
1157
|
+
const text = `The governed semantic metric was found, but its semantic runtime could not compile the request: ${semanticRuntimeFailure}`;
|
|
1158
|
+
return {
|
|
1159
|
+
kind: 'no_answer',
|
|
1160
|
+
sourceTier: 'no_answer',
|
|
1161
|
+
certification: 'analyst_review_required',
|
|
1162
|
+
reviewStatus: 'none',
|
|
1163
|
+
confidence: 0,
|
|
1164
|
+
text,
|
|
1165
|
+
answer: text,
|
|
1166
|
+
refusalCode: 'modeling_gap',
|
|
1167
|
+
refusalDetails: { code: 'semantic_runtime_required', message: text },
|
|
1168
|
+
citations: [],
|
|
1169
|
+
memoryContext: input.memoryContext,
|
|
1170
|
+
evidence: buildNoAnswerEvidence({
|
|
1171
|
+
question,
|
|
1172
|
+
reason: text,
|
|
1173
|
+
artifactHits,
|
|
1174
|
+
businessHits,
|
|
1175
|
+
semanticHits,
|
|
1176
|
+
manifestHits,
|
|
1177
|
+
considered,
|
|
1178
|
+
memoryContext: input.memoryContext ?? [],
|
|
1179
|
+
toolCalls: semanticBridgeToolCalls,
|
|
1180
|
+
}),
|
|
1181
|
+
contextPack: input.contextPack,
|
|
1182
|
+
considered,
|
|
1183
|
+
providerUsed: provider.name,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
987
1186
|
const metricFirst = semanticMetricMatch
|
|
988
1187
|
? buildGovernedMetricFirstSql({
|
|
989
1188
|
metric: semanticMetricMatch.metric,
|
|
@@ -1000,6 +1199,7 @@ async function runAnswerLoop(input) {
|
|
|
1000
1199
|
let contextLedger = createContextLedger({
|
|
1001
1200
|
contextPack: input.contextPack,
|
|
1002
1201
|
schemaContext,
|
|
1202
|
+
dialect: input.semanticDriver,
|
|
1003
1203
|
});
|
|
1004
1204
|
// W2.2 — certified-block adaptation lane. When a certified block is context-only
|
|
1005
1205
|
// ONLY because the question adds exactly one filter whose value maps to a column
|
|
@@ -1042,6 +1242,7 @@ async function runAnswerLoop(input) {
|
|
|
1042
1242
|
kg,
|
|
1043
1243
|
driver: input.semanticDriver,
|
|
1044
1244
|
tableMapping: input.semanticTableMapping,
|
|
1245
|
+
semanticQueryCompiler: input.semanticQueryCompiler,
|
|
1045
1246
|
onCompiled: (record) => compiledSemanticRecords.push(record),
|
|
1046
1247
|
}),
|
|
1047
1248
|
];
|
|
@@ -1156,6 +1357,7 @@ async function runAnswerLoop(input) {
|
|
|
1156
1357
|
if (trust.tier === 'semantic_metric' && trust.compiled) {
|
|
1157
1358
|
governedMetricAnswer = true;
|
|
1158
1359
|
const compiled = trust.compiled;
|
|
1360
|
+
semanticRuntimeCompiledAnswer = compiled.engine === 'metricflow-cli' || compiled.engine === 'dbt-cloud';
|
|
1159
1361
|
const governedNote = `Answered from governed semantic metric${compiled.metrics.length === 1 ? '' : 's'} ${compiled.metrics.join(', ')}${compiled.dimensions.length > 0 ? ` by ${compiled.dimensions.join(', ')}` : ''} (compiled via the semantic layer). Reusable block certification remains a separate review.`;
|
|
1160
1362
|
parsed = { ...parsed, text: parsed.text ? `${parsed.text}\n\n${governedNote}` : governedNote };
|
|
1161
1363
|
proposalToolCalls.push({
|
|
@@ -1352,13 +1554,19 @@ async function runAnswerLoop(input) {
|
|
|
1352
1554
|
&& questionPlan.requestedShape.filters.length === 0
|
|
1353
1555
|
&& !questionPlan.requestedShape.topN;
|
|
1354
1556
|
let contextValidation;
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1557
|
+
// dbt Cloud/MetricFlow SQL has already been compiled against the selected
|
|
1558
|
+
// semantic environment. A thin local retrieval pack cannot safely reject it
|
|
1559
|
+
// and replace it with a guessed leaf-measure SQL. Warehouse execution remains
|
|
1560
|
+
// the final binder/runtime check.
|
|
1561
|
+
const initialValidation = semanticRuntimeCompiledAnswer
|
|
1562
|
+
? { ok: true, warnings: ['SQL compiled by the configured semantic runtime.'] }
|
|
1563
|
+
: contextLedger.validateSql(parsed.sql, {
|
|
1564
|
+
question,
|
|
1565
|
+
intent,
|
|
1566
|
+
filterValues: input.followUp?.filters,
|
|
1567
|
+
trustedFilterValues: trustedFollowUpFilterValues(input.followUp),
|
|
1568
|
+
memberBindings: input.followUp?.memberBindings,
|
|
1569
|
+
});
|
|
1362
1570
|
const rankedGrainGap = missingRankedGrainOutput(questionPlan, parsed.sql, semanticBridgeAnswer?.dimensions);
|
|
1363
1571
|
contextValidation = initialValidation.ok && rankedGrainGap
|
|
1364
1572
|
? {
|
|
@@ -1654,7 +1862,7 @@ async function runAnswerLoop(input) {
|
|
|
1654
1862
|
};
|
|
1655
1863
|
}
|
|
1656
1864
|
const dbtFirstJoinSafety = input.manifest
|
|
1657
|
-
? evaluateDbtFirstGeneratedSql(parsed.sql, input.manifest, input.domainContext?.purpose, input.domainContext)
|
|
1865
|
+
? evaluateDbtFirstGeneratedSql(parsed.sql, input.manifest, input.domainContext?.purpose, input.domainContext, input.semanticDriver)
|
|
1658
1866
|
: undefined;
|
|
1659
1867
|
if (dbtFirstJoinSafety && !dbtFirstJoinSafety.safe) {
|
|
1660
1868
|
// Business-language text for the chat surface; the machine policy message
|
|
@@ -3162,6 +3370,15 @@ function applyParsedProposalMetadata(target, source) {
|
|
|
3162
3370
|
function buildCertifiedBlockDqlArtifact(block, result) {
|
|
3163
3371
|
if (block.kind !== 'block')
|
|
3164
3372
|
return undefined;
|
|
3373
|
+
const parameters = block.parameters ?? [];
|
|
3374
|
+
const parameterValues = Object.fromEntries((result?.parameters ?? []).map((entry) => [entry.name, entry.value]));
|
|
3375
|
+
const parameterContract = renderCertifiedParameterContract(block);
|
|
3376
|
+
const artifactRuntime = {
|
|
3377
|
+
...(parameters.length > 0 ? { parameters } : {}),
|
|
3378
|
+
...(Object.keys(parameterValues).length > 0 ? { parameterValues } : {}),
|
|
3379
|
+
persistence: 'saved',
|
|
3380
|
+
trustState: 'certified',
|
|
3381
|
+
};
|
|
3165
3382
|
const sql = block.sql?.trim() ?? result?.sql?.trim() ?? block.examples?.find((example) => example.sql?.trim())?.sql?.trim();
|
|
3166
3383
|
if (!sql) {
|
|
3167
3384
|
// The block answered but its SQL was not inlined in the index (navigation /
|
|
@@ -3177,6 +3394,7 @@ function buildCertifiedBlockDqlArtifact(block, result) {
|
|
|
3177
3394
|
source: `// Certified DQL block "${escapeDqlArtifactString(block.name)}"`
|
|
3178
3395
|
+ `${block.sourcePath ? `\n// source: ${block.sourcePath}` : ''}`
|
|
3179
3396
|
+ `\n// SQL is not inlined in the index — open the source file to view or edit the query.`,
|
|
3397
|
+
...artifactRuntime,
|
|
3180
3398
|
};
|
|
3181
3399
|
}
|
|
3182
3400
|
const domain = block.domain ?? 'misc';
|
|
@@ -3186,7 +3404,7 @@ function buildCertifiedBlockDqlArtifact(block, result) {
|
|
|
3186
3404
|
domain = "${escapeDqlArtifactString(domain)}"
|
|
3187
3405
|
type = "custom"
|
|
3188
3406
|
status = "certified"${block.owner ? `\n owner = "${escapeDqlArtifactString(block.owner)}"` : ''}
|
|
3189
|
-
description = """${escapeDqlArtifactTripleString(description)}"""${sourcePathComment}
|
|
3407
|
+
description = """${escapeDqlArtifactTripleString(description)}"""${sourcePathComment}${parameterContract}
|
|
3190
3408
|
|
|
3191
3409
|
query = """
|
|
3192
3410
|
${sql.replace(/"""/g, '\\"\\"\\"').split('\n').join('\n ')}
|
|
@@ -3198,8 +3416,51 @@ function buildCertifiedBlockDqlArtifact(block, result) {
|
|
|
3198
3416
|
name: block.name,
|
|
3199
3417
|
sourcePath: block.sourcePath,
|
|
3200
3418
|
source,
|
|
3419
|
+
compiledSql: result?.sql ?? sql,
|
|
3420
|
+
...artifactRuntime,
|
|
3201
3421
|
};
|
|
3202
3422
|
}
|
|
3423
|
+
function renderCertifiedParameterContract(block) {
|
|
3424
|
+
const parameters = block.parameters ?? [];
|
|
3425
|
+
if (parameters.length === 0)
|
|
3426
|
+
return '';
|
|
3427
|
+
const lines = ['\n\n params {'];
|
|
3428
|
+
for (const parameter of parameters) {
|
|
3429
|
+
const defaultValue = parameter.default === undefined ? '' : ` = ${renderCertifiedDqlParameterValue(parameter.default)}`;
|
|
3430
|
+
lines.push(` ${parameter.name}: ${parameter.type}${defaultValue}`);
|
|
3431
|
+
}
|
|
3432
|
+
lines.push(' }', ' parameterPolicy {');
|
|
3433
|
+
for (const parameter of parameters)
|
|
3434
|
+
lines.push(` ${parameter.name} = "${parameter.policy}"`);
|
|
3435
|
+
lines.push(' }');
|
|
3436
|
+
const explicitBindings = new Map((block.filterBindings ?? []).map((entry) => [entry.filter, entry.binding]));
|
|
3437
|
+
const bindings = parameters.flatMap((parameter) => {
|
|
3438
|
+
const explicit = explicitBindings.get(parameter.name);
|
|
3439
|
+
if (explicit)
|
|
3440
|
+
return [{ name: parameter.name, value: explicit }];
|
|
3441
|
+
if (parameter.binding?.kind === 'semantic_filter')
|
|
3442
|
+
return [{ name: parameter.name, value: parameter.binding.field }];
|
|
3443
|
+
if (parameter.binding?.kind === 'limit')
|
|
3444
|
+
return [{ name: parameter.name, value: 'limit' }];
|
|
3445
|
+
return [];
|
|
3446
|
+
});
|
|
3447
|
+
if (bindings.length > 0) {
|
|
3448
|
+
lines.push(' filterBindings {');
|
|
3449
|
+
for (const binding of bindings)
|
|
3450
|
+
lines.push(` ${binding.name} = "${escapeDqlArtifactString(binding.value)}"`);
|
|
3451
|
+
lines.push(' }');
|
|
3452
|
+
}
|
|
3453
|
+
return lines.join('\n');
|
|
3454
|
+
}
|
|
3455
|
+
function renderCertifiedDqlParameterValue(value) {
|
|
3456
|
+
if (Array.isArray(value))
|
|
3457
|
+
return `[${value.map(renderCertifiedDqlParameterValue).join(', ')}]`;
|
|
3458
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
3459
|
+
return String(value);
|
|
3460
|
+
if (typeof value === 'boolean')
|
|
3461
|
+
return value ? 'true' : 'false';
|
|
3462
|
+
return `"${escapeDqlArtifactString(String(value ?? ''))}"`;
|
|
3463
|
+
}
|
|
3203
3464
|
function buildGeneratedSqlDqlArtifact(input) {
|
|
3204
3465
|
const sql = input.sql?.trim();
|
|
3205
3466
|
if (!sql)
|