@duckcodeailabs/dql-agent 1.8.2 → 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/agent-run-engine.d.ts +1 -1
- package/dist/agent-run-engine.d.ts.map +1 -1
- package/dist/agent-run-gates.d.ts.map +1 -1
- package/dist/agent-run-gates.js +2 -1
- package/dist/agent-run-gates.js.map +1 -1
- package/dist/agent-run-store.d.ts +33 -0
- package/dist/agent-run-store.d.ts.map +1 -0
- package/dist/agent-run-store.js +167 -0
- package/dist/agent-run-store.js.map +1 -0
- 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 +20 -5
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +384 -26
- 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 +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/metadata/analytical-policy.d.ts +40 -2
- package/dist/metadata/analytical-policy.d.ts.map +1 -1
- package/dist/metadata/analytical-policy.js +276 -53
- package/dist/metadata/analytical-policy.js.map +1 -1
- package/dist/metadata/block-fit.js +18 -2
- package/dist/metadata/block-fit.js.map +1 -1
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +10 -3
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/dbt-first-safety.d.ts +9 -2
- package/dist/metadata/dbt-first-safety.d.ts.map +1 -1
- package/dist/metadata/dbt-first-safety.js +8 -3
- package/dist/metadata/dbt-first-safety.js.map +1 -1
- package/dist/metadata/health.d.ts +19 -0
- package/dist/metadata/health.d.ts.map +1 -0
- package/dist/metadata/health.js +72 -0
- package/dist/metadata/health.js.map +1 -0
- 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 +31 -5
- 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 +14 -9
- package/dist/tools/registry.js.map +1 -1
- package/package.json +4 -4
package/dist/answer-loop.js
CHANGED
|
@@ -26,10 +26,10 @@ import { createContextLedger } from './grounding/context-ledger.js';
|
|
|
26
26
|
import { validateAnswerResultShape } from './answer-shape.js';
|
|
27
27
|
import { fanoutWarningsForSql } from './metadata/grain-ledger.js';
|
|
28
28
|
import { evaluateDbtFirstGeneratedSql } from './metadata/dbt-first-safety.js';
|
|
29
|
-
import { planAnalyticalPath } from './metadata/analytical-policy.js';
|
|
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;
|
|
@@ -158,6 +245,41 @@ const BUSINESS_CONTEXT_KINDS = ['term', 'business_view', 'domain', 'skill', 'rel
|
|
|
158
245
|
const ARTIFACT_KINDS = [...EXECUTABLE_ARTIFACT_KINDS, ...BUSINESS_CONTEXT_KINDS];
|
|
159
246
|
const SEMANTIC_KINDS = ['metric', 'dimension', 'measure', 'entity', 'semantic_model', 'saved_query'];
|
|
160
247
|
const MANIFEST_KINDS = ['dbt_model', 'dbt_source'];
|
|
248
|
+
/**
|
|
249
|
+
* Business-language explanation for a failed SQL context validation. The chat
|
|
250
|
+
* surface shows THIS; the validator's machine message (relation ids, repair
|
|
251
|
+
* tool guidance) belongs in refusalDetails/validationWarnings only.
|
|
252
|
+
*/
|
|
253
|
+
function renderContextValidationRefusalForUser(code, machineError, memberBindings) {
|
|
254
|
+
switch (code) {
|
|
255
|
+
case 'unknown_relation':
|
|
256
|
+
return 'I drafted a query, but it uses a table that was not part of the metadata retrieved for this question, so I did not run it. Ask again (retrieval usually finds it on a follow-up), name the table or metric explicitly, or re-sync the dbt metadata.';
|
|
257
|
+
case 'unknown_column':
|
|
258
|
+
return 'I drafted a query, but it references a column that is not in the inspected metadata for those tables, so I did not run it. Name the exact field you mean, or re-sync the dbt metadata if the column is new.';
|
|
259
|
+
case 'misbound_filter': {
|
|
260
|
+
const binding = memberBindings?.[0];
|
|
261
|
+
const target = binding ? `${humanizeAnalyticalEntityId(binding.dimension)} "${binding.values[0] ?? ''}"` : 'the requested value';
|
|
262
|
+
return `I need to filter this answer to ${target}, but the query I drafted did not apply that filter to a matching column, so I did not run it. Tell me which column identifies ${binding ? humanizeAnalyticalEntityId(binding.dimension) : 'that value'}, or rephrase with the exact field name.`;
|
|
263
|
+
}
|
|
264
|
+
case 'ambiguous_filter':
|
|
265
|
+
return 'The value you asked about matches more than one column in the inspected data, so I did not guess. Tell me which field it belongs to and I will run it.';
|
|
266
|
+
case 'missing_baseline':
|
|
267
|
+
return 'This comparison needs a baseline period or value that the drafted query did not include. Say what to compare against (for example, the prior month) and I will run it.';
|
|
268
|
+
case 'unsafe_sql':
|
|
269
|
+
return 'The drafted query used a statement type that is not allowed in this governed preview, so I did not run it.';
|
|
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.';
|
|
277
|
+
default:
|
|
278
|
+
return machineError.trim().length > 0 && !/inspect_metadata_context/i.test(machineError)
|
|
279
|
+
? `I could not prepare a governed query yet: ${machineError.replace(/\s*Use inspect_metadata_context[^.]*\.\s*$/i, '').trim()}`
|
|
280
|
+
: '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.';
|
|
281
|
+
}
|
|
282
|
+
}
|
|
161
283
|
function refusalCodeForValidation(code) {
|
|
162
284
|
if (code === 'unknown_relation' || code === 'unknown_column' || code === 'insufficient_context' || code === 'missing_baseline') {
|
|
163
285
|
return 'grounding_gap';
|
|
@@ -187,6 +309,22 @@ function refusalCodeForAnalyticalPolicy(code, hasExploratoryCandidate = false) {
|
|
|
187
309
|
* denials and must remain terminal in this loop.
|
|
188
310
|
*/
|
|
189
311
|
function exploratoryCandidateFromDbtFirstGuard(sql, decision) {
|
|
312
|
+
// The policy service's disposition is authoritative: a declared draft path
|
|
313
|
+
// re-bound to this SQL's actual joins enters the bounded lane directly.
|
|
314
|
+
if (decision.disposition === 'exploratory_candidate') {
|
|
315
|
+
return {
|
|
316
|
+
kind: 'dbt_grounded_exploration',
|
|
317
|
+
reason: 'relationship_not_certified',
|
|
318
|
+
sql,
|
|
319
|
+
message: decision.technicalDetail ?? decision.message
|
|
320
|
+
?? 'This query follows a declared but uncertified DQL relationship path.',
|
|
321
|
+
userFacingReason: decision.userFacingReason,
|
|
322
|
+
modeledEntityIds: decision.entities,
|
|
323
|
+
relationshipIds: decision.relationshipIds,
|
|
324
|
+
exploratoryPath: decision.exploratoryPath,
|
|
325
|
+
executionStatus: 'not_executed',
|
|
326
|
+
};
|
|
327
|
+
}
|
|
190
328
|
const reason = decision.code;
|
|
191
329
|
const missingPathWithoutUnsafeProof = reason === 'unsafe_relationship' && decision.relationshipIds.length === 0;
|
|
192
330
|
if (decision.safe || (!missingPathWithoutUnsafeProof
|
|
@@ -207,6 +345,7 @@ function exploratoryCandidateFromDbtFirstGuard(sql, decision) {
|
|
|
207
345
|
sql,
|
|
208
346
|
message: decision.message
|
|
209
347
|
?? 'This query is grounded in dbt metadata but has no certified DQL relationship path yet.',
|
|
348
|
+
userFacingReason: decision.userFacingReason,
|
|
210
349
|
modeledEntityIds: decision.entities,
|
|
211
350
|
relationshipIds: decision.relationshipIds,
|
|
212
351
|
executionStatus: 'not_executed',
|
|
@@ -452,9 +591,10 @@ async function runAnswerLoop(input) {
|
|
|
452
591
|
const considered = mergeHits(artifactHits, semanticHits, manifestHits, kg.search({ query: question, domain, limit: 10 })).slice(0, 30);
|
|
453
592
|
const schemaContext = schemaContextWithAllowedSqlContext(schemaContextWithinQuestionScope(input.schemaContext ?? [], input.contextPack, scopedContextPack), scopedContextPack);
|
|
454
593
|
const catalogRoute = input.contextPack?.routeDecision;
|
|
455
|
-
const
|
|
594
|
+
const baseQuestionPlan = input.contextPack?.questionPlan?.requestedShape
|
|
456
595
|
? input.contextPack.questionPlan
|
|
457
596
|
: buildAnalysisQuestionPlan(question, input.followUp);
|
|
597
|
+
const questionPlan = questionPlanWithResolvedMemberBindings(baseQuestionPlan, schemaContext);
|
|
458
598
|
// Retrieval may surface a high-trust block because its source tables and
|
|
459
599
|
// vocabulary overlap the question even when its output contract does not.
|
|
460
600
|
// Keep such candidates in the audit trail, but do not put their SQL or a stale
|
|
@@ -500,7 +640,15 @@ async function runAnswerLoop(input) {
|
|
|
500
640
|
kg,
|
|
501
641
|
})
|
|
502
642
|
: null;
|
|
503
|
-
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;
|
|
504
652
|
const catalogCertifiedHit = shouldTryCertifiedRoute
|
|
505
653
|
? certifiedHitFromContextPack(input.contextPack, kg)
|
|
506
654
|
: null;
|
|
@@ -567,7 +715,10 @@ async function runAnswerLoop(input) {
|
|
|
567
715
|
let executionError;
|
|
568
716
|
if (artifactHit.node.kind === 'block' && input.executeCertifiedBlock) {
|
|
569
717
|
try {
|
|
570
|
-
result = await input.executeCertifiedBlock(artifactHit.node, {
|
|
718
|
+
result = await input.executeCertifiedBlock(artifactHit.node, {
|
|
719
|
+
question,
|
|
720
|
+
...certifiedInvocationInputs(artifactHit.node, questionPlan),
|
|
721
|
+
});
|
|
571
722
|
result = trimResultToRequestedTopN(result, questionPlan);
|
|
572
723
|
}
|
|
573
724
|
catch (err) {
|
|
@@ -882,6 +1033,8 @@ async function runAnswerLoop(input) {
|
|
|
882
1033
|
let governedMetricAnswer = false;
|
|
883
1034
|
const semanticBridgeToolCalls = [];
|
|
884
1035
|
let semanticBridgeAnswer;
|
|
1036
|
+
let semanticRuntimeFailure;
|
|
1037
|
+
let semanticRuntimeCompiledAnswer = false;
|
|
885
1038
|
if (input.semanticLayer && semanticMetricMatch) {
|
|
886
1039
|
semanticBridgeAnswer = composeSemanticQueryForQuestion({
|
|
887
1040
|
semanticLayer: input.semanticLayer,
|
|
@@ -893,6 +1046,44 @@ async function runAnswerLoop(input) {
|
|
|
893
1046
|
...(input.semanticDriver ? { driver: input.semanticDriver } : {}),
|
|
894
1047
|
...(input.semanticTableMapping ? { tableMapping: input.semanticTableMapping } : {}),
|
|
895
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
|
+
}
|
|
896
1087
|
// Lane-2 LLM fallback: the deterministic token matcher missed, but a metric
|
|
897
1088
|
// matched so the semantic layer IS relevant. Spend ONE call to have the model
|
|
898
1089
|
// pick members (the query_semantic_model contract); the compiler still owns the
|
|
@@ -907,13 +1098,37 @@ async function runAnswerLoop(input) {
|
|
|
907
1098
|
reasoningEffort: input.reasoningEffort,
|
|
908
1099
|
});
|
|
909
1100
|
if (selection) {
|
|
910
|
-
|
|
1101
|
+
let composed = composeSemanticQueryFromMembers({
|
|
911
1102
|
semanticLayer: input.semanticLayer,
|
|
912
1103
|
question,
|
|
913
1104
|
selection,
|
|
914
1105
|
...(input.semanticDriver ? { driver: input.semanticDriver } : {}),
|
|
915
1106
|
...(input.semanticTableMapping ? { tableMapping: input.semanticTableMapping } : {}),
|
|
916
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
|
+
}
|
|
917
1132
|
// Coverage guard: if the question asked for a breakdown but the LLM
|
|
918
1133
|
// selection produced none, the governed answer would silently DROP the
|
|
919
1134
|
// requested grouping (governed-but-wrong). Fall through to Lane-3
|
|
@@ -938,6 +1153,36 @@ async function runAnswerLoop(input) {
|
|
|
938
1153
|
}
|
|
939
1154
|
}
|
|
940
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
|
+
}
|
|
941
1186
|
const metricFirst = semanticMetricMatch
|
|
942
1187
|
? buildGovernedMetricFirstSql({
|
|
943
1188
|
metric: semanticMetricMatch.metric,
|
|
@@ -954,6 +1199,7 @@ async function runAnswerLoop(input) {
|
|
|
954
1199
|
let contextLedger = createContextLedger({
|
|
955
1200
|
contextPack: input.contextPack,
|
|
956
1201
|
schemaContext,
|
|
1202
|
+
dialect: input.semanticDriver,
|
|
957
1203
|
});
|
|
958
1204
|
// W2.2 — certified-block adaptation lane. When a certified block is context-only
|
|
959
1205
|
// ONLY because the question adds exactly one filter whose value maps to a column
|
|
@@ -996,6 +1242,7 @@ async function runAnswerLoop(input) {
|
|
|
996
1242
|
kg,
|
|
997
1243
|
driver: input.semanticDriver,
|
|
998
1244
|
tableMapping: input.semanticTableMapping,
|
|
1245
|
+
semanticQueryCompiler: input.semanticQueryCompiler,
|
|
999
1246
|
onCompiled: (record) => compiledSemanticRecords.push(record),
|
|
1000
1247
|
}),
|
|
1001
1248
|
];
|
|
@@ -1110,6 +1357,7 @@ async function runAnswerLoop(input) {
|
|
|
1110
1357
|
if (trust.tier === 'semantic_metric' && trust.compiled) {
|
|
1111
1358
|
governedMetricAnswer = true;
|
|
1112
1359
|
const compiled = trust.compiled;
|
|
1360
|
+
semanticRuntimeCompiledAnswer = compiled.engine === 'metricflow-cli' || compiled.engine === 'dbt-cloud';
|
|
1113
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.`;
|
|
1114
1362
|
parsed = { ...parsed, text: parsed.text ? `${parsed.text}\n\n${governedNote}` : governedNote };
|
|
1115
1363
|
proposalToolCalls.push({
|
|
@@ -1209,7 +1457,12 @@ async function runAnswerLoop(input) {
|
|
|
1209
1457
|
});
|
|
1210
1458
|
parsed = parseProposal(proposed);
|
|
1211
1459
|
}
|
|
1212
|
-
catch {
|
|
1460
|
+
catch (err) {
|
|
1461
|
+
// Deadline/cancellation must surface as itself, never as a policy refusal.
|
|
1462
|
+
if (input.signal?.aborted)
|
|
1463
|
+
throw input.signal.reason ?? err;
|
|
1464
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'))
|
|
1465
|
+
throw err;
|
|
1213
1466
|
// keep the original decline; fall through to the honest no_answer.
|
|
1214
1467
|
}
|
|
1215
1468
|
}
|
|
@@ -1222,9 +1475,15 @@ async function runAnswerLoop(input) {
|
|
|
1222
1475
|
// consistent, actionable message so the same question yields the same outcome
|
|
1223
1476
|
// every run and every surface — instead of passing through the model's varying
|
|
1224
1477
|
// text. A genuinely context-less ask keeps the plain honest message.
|
|
1478
|
+
// The user-facing text is BUSINESS language; the machine policy detail
|
|
1479
|
+
// (qualified ids, codes) lives only in refusalDetails + validationWarnings.
|
|
1225
1480
|
const declinedDespiteContext = wantsGeneratedData && hasGeneratableContext;
|
|
1481
|
+
const policyDetail = analyticalPlan?.safe === false
|
|
1482
|
+
? analyticalPlan.technicalDetail ?? analyticalPlan.message ?? 'DQL could not prove a safe analytical relationship path.'
|
|
1483
|
+
: undefined;
|
|
1226
1484
|
const text = analyticalPlan?.safe === false
|
|
1227
|
-
? analyticalPlan.
|
|
1485
|
+
? analyticalPlan.userFacingReason
|
|
1486
|
+
?? analyticalPolicyUserFacingReason(analyticalPlan.code ?? 'unsafe_relationship', analyticalPlan.entities)
|
|
1228
1487
|
: declinedDespiteContext
|
|
1229
1488
|
? 'I could not compose a governed query for this from the available tables and metrics. This usually needs a clearer join path or an explicit metric and grouping — name the specific measure and how to break it down, and I can generate a review-required draft.'
|
|
1230
1489
|
: parsed.text || 'No answer (the model declined to propose SQL).';
|
|
@@ -1236,14 +1495,17 @@ async function runAnswerLoop(input) {
|
|
|
1236
1495
|
confidence: 0.1,
|
|
1237
1496
|
text,
|
|
1238
1497
|
refusalCode: analyticalPlan?.safe === false
|
|
1239
|
-
?
|
|
1498
|
+
? analyticalPlan.disposition === 'exploratory_candidate'
|
|
1499
|
+
? 'modeling_gap'
|
|
1500
|
+
: refusalCodeForAnalyticalPolicy(analyticalPlan.code, analyticalPlanAllowsDbtExploration(analyticalPlan))
|
|
1240
1501
|
: 'model_declined',
|
|
1241
1502
|
refusalDetails: {
|
|
1242
1503
|
code: analyticalPlan?.safe === false
|
|
1243
1504
|
? analyticalPlan.code ?? 'unsafe_relationship'
|
|
1244
1505
|
: 'model_declined',
|
|
1245
|
-
message: text,
|
|
1506
|
+
message: policyDetail ?? text,
|
|
1246
1507
|
},
|
|
1508
|
+
validationWarnings: policyDetail ? [`Analytical policy detail: ${policyDetail}`] : undefined,
|
|
1247
1509
|
answer: text,
|
|
1248
1510
|
citations: [],
|
|
1249
1511
|
memoryContext: input.memoryContext,
|
|
@@ -1292,13 +1554,19 @@ async function runAnswerLoop(input) {
|
|
|
1292
1554
|
&& questionPlan.requestedShape.filters.length === 0
|
|
1293
1555
|
&& !questionPlan.requestedShape.topN;
|
|
1294
1556
|
let contextValidation;
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
+
});
|
|
1302
1570
|
const rankedGrainGap = missingRankedGrainOutput(questionPlan, parsed.sql, semanticBridgeAnswer?.dimensions);
|
|
1303
1571
|
contextValidation = initialValidation.ok && rankedGrainGap
|
|
1304
1572
|
? {
|
|
@@ -1448,7 +1716,11 @@ async function runAnswerLoop(input) {
|
|
|
1448
1716
|
};
|
|
1449
1717
|
}
|
|
1450
1718
|
}
|
|
1451
|
-
catch {
|
|
1719
|
+
catch (err) {
|
|
1720
|
+
if (input.signal?.aborted)
|
|
1721
|
+
throw input.signal.reason ?? err;
|
|
1722
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'))
|
|
1723
|
+
throw err;
|
|
1452
1724
|
// Re-grounding is best-effort; the bounded self-repair below still runs.
|
|
1453
1725
|
}
|
|
1454
1726
|
}
|
|
@@ -1503,7 +1775,11 @@ async function runAnswerLoop(input) {
|
|
|
1503
1775
|
}
|
|
1504
1776
|
}
|
|
1505
1777
|
}
|
|
1506
|
-
catch {
|
|
1778
|
+
catch (err) {
|
|
1779
|
+
if (input.signal?.aborted)
|
|
1780
|
+
throw input.signal.reason ?? err;
|
|
1781
|
+
if (err instanceof Error && (err.name === 'AbortError' || err.name === 'TimeoutError'))
|
|
1782
|
+
throw err;
|
|
1507
1783
|
// Repair is best-effort — the refusal below stays the honest fallback.
|
|
1508
1784
|
}
|
|
1509
1785
|
}
|
|
@@ -1523,7 +1799,10 @@ async function runAnswerLoop(input) {
|
|
|
1523
1799
|
}
|
|
1524
1800
|
}
|
|
1525
1801
|
if (!contextValidation.ok) {
|
|
1526
|
-
|
|
1802
|
+
// Business-language chat text; the validator's machine message (with
|
|
1803
|
+
// relation ids and tool guidance for the repair prompt) stays in
|
|
1804
|
+
// refusalDetails + validationWarnings for the Inspect surface.
|
|
1805
|
+
const text = renderContextValidationRefusalForUser(contextValidation.code, contextValidation.error, input.followUp?.memberBindings);
|
|
1527
1806
|
const analysisPlan = buildAnalysisPlan({
|
|
1528
1807
|
question,
|
|
1529
1808
|
intent,
|
|
@@ -1557,7 +1836,11 @@ async function runAnswerLoop(input) {
|
|
|
1557
1836
|
trustLabel: input.contextPack?.trustLabel,
|
|
1558
1837
|
sourceCertifiedBlock: followUpSourceBlock?.name ?? input.followUp?.sourceBlockName,
|
|
1559
1838
|
contextPackId: input.contextPack?.id,
|
|
1560
|
-
validationWarnings: [
|
|
1839
|
+
validationWarnings: [
|
|
1840
|
+
...contextValidation.warnings,
|
|
1841
|
+
`SQL context validation detail: ${contextValidation.error}`,
|
|
1842
|
+
...deepCandidateNotes,
|
|
1843
|
+
],
|
|
1561
1844
|
selectedEvidence: input.contextPack?.evidenceRoles?.slice(0, 12),
|
|
1562
1845
|
citations: [],
|
|
1563
1846
|
memoryContext: input.memoryContext,
|
|
@@ -1579,11 +1862,15 @@ async function runAnswerLoop(input) {
|
|
|
1579
1862
|
};
|
|
1580
1863
|
}
|
|
1581
1864
|
const dbtFirstJoinSafety = input.manifest
|
|
1582
|
-
? evaluateDbtFirstGeneratedSql(parsed.sql, input.manifest, input.domainContext?.purpose, input.domainContext)
|
|
1865
|
+
? evaluateDbtFirstGeneratedSql(parsed.sql, input.manifest, input.domainContext?.purpose, input.domainContext, input.semanticDriver)
|
|
1583
1866
|
: undefined;
|
|
1584
1867
|
if (dbtFirstJoinSafety && !dbtFirstJoinSafety.safe) {
|
|
1585
|
-
|
|
1868
|
+
// Business-language text for the chat surface; the machine policy message
|
|
1869
|
+
// stays in refusalDetails + validationWarnings for Inspect.
|
|
1870
|
+
const policyDetail = dbtFirstJoinSafety.technicalDetail ?? dbtFirstJoinSafety.message
|
|
1586
1871
|
?? 'DQL could not prove this generated join from certified analytical relationships.';
|
|
1872
|
+
const text = dbtFirstJoinSafety.userFacingReason
|
|
1873
|
+
?? analyticalPolicyUserFacingReason(dbtFirstJoinSafety.code ?? 'unsafe_relationship', dbtFirstJoinSafety.entities);
|
|
1587
1874
|
const exploratoryCandidate = exploratoryCandidateFromDbtFirstGuard(parsed.sql, dbtFirstJoinSafety);
|
|
1588
1875
|
const analysisPlan = buildAnalysisPlan({
|
|
1589
1876
|
question,
|
|
@@ -1611,13 +1898,16 @@ async function runAnswerLoop(input) {
|
|
|
1611
1898
|
proposedSql: parsed.sql,
|
|
1612
1899
|
sql: parsed.sql,
|
|
1613
1900
|
...(exploratoryCandidate ? { exploratoryCandidate } : {}),
|
|
1614
|
-
refusalCode:
|
|
1901
|
+
refusalCode: dbtFirstJoinSafety.disposition === 'exploratory_candidate'
|
|
1902
|
+
? 'modeling_gap'
|
|
1903
|
+
: refusalCodeForAnalyticalPolicy(dbtFirstJoinSafety.code, Boolean(exploratoryCandidate)),
|
|
1615
1904
|
refusalDetails: {
|
|
1616
1905
|
code: dbtFirstJoinSafety.code ?? 'unsafe_relationship',
|
|
1617
|
-
message:
|
|
1906
|
+
message: policyDetail,
|
|
1618
1907
|
},
|
|
1619
1908
|
validationWarnings: [
|
|
1620
1909
|
...contextValidation.warnings,
|
|
1910
|
+
`Analytical policy detail: ${policyDetail}`,
|
|
1621
1911
|
`DQL v3 relationship guard: ${dbtFirstJoinSafety.code ?? 'unsafe relationship'}.`,
|
|
1622
1912
|
...(exploratoryCandidate
|
|
1623
1913
|
? ['A bounded DBT-grounded exploratory route may validate this missing modeled relationship; governed SQL was not executed.']
|
|
@@ -2272,10 +2562,21 @@ function renderAnalyticalPlanPrompt(plan) {
|
|
|
2272
2562
|
return undefined;
|
|
2273
2563
|
if (!plan.safe) {
|
|
2274
2564
|
if (analyticalPlanAllowsDbtExploration(plan)) {
|
|
2565
|
+
const declaredPath = plan.exploratoryPath?.edges.length
|
|
2566
|
+
? [
|
|
2567
|
+
'DECLARED (UNCERTIFIED) DRAFT JOIN PATH — suggestion only, NOT authorization:',
|
|
2568
|
+
...plan.exploratoryPath.edges.map((edge, index) => [
|
|
2569
|
+
`${index + 1}. ${edge.fromEntity} (${edge.fromRelation ?? 'bound dbt model'}) -> ${edge.toEntity} (${edge.toRelation ?? 'bound dbt model'})`,
|
|
2570
|
+
` relationship=${edge.relationshipId}; keys=${edge.keys.map((key) => `${key.from}=${key.to}`).join(', ')}; cardinality=${edge.cardinality}; fanout=${edge.fanout}; status=${edge.lifecycle}`,
|
|
2571
|
+
].join('\n')),
|
|
2572
|
+
'Prefer these declared relations and exact key pairs over inferring joins from column names. The result remains analyst-review-required.',
|
|
2573
|
+
]
|
|
2574
|
+
: [];
|
|
2275
2575
|
return [
|
|
2276
2576
|
'DQL GOVERNED RELATIONSHIP COVERAGE: MISSING.',
|
|
2277
2577
|
plan.message ?? 'No certified analytical relationship path is available.',
|
|
2278
2578
|
`Policy code: ${plan.code ?? 'unsafe_relationship'}.`,
|
|
2579
|
+
...declaredPath,
|
|
2279
2580
|
'This is not authorization for governed SQL. You MAY propose one bounded, read-only DBT-grounded exploratory SELECT/WITH using only the inspected relations and columns supplied below.',
|
|
2280
2581
|
'Use explicit aliases and equality join keys visible in the inspected schema. Preserve the requested member bindings exactly. Do not introduce an uninspected relation, a cross-domain path, an attribution/allocation rule, or more than four joins.',
|
|
2281
2582
|
'The host will independently parse, bound, probe, validate, and execute the candidate as analyst-review-required. If the inspected evidence cannot express the request, return no SQL.',
|
|
@@ -2301,8 +2602,12 @@ function renderAnalyticalPlanPrompt(plan) {
|
|
|
2301
2602
|
/**
|
|
2302
2603
|
* Missing modeling may enter the narrow EXP-001 host-validated lane. Explicit
|
|
2303
2604
|
* unsafe, cross-domain, attribution, expiry, purpose, and proof failures never do.
|
|
2605
|
+
* The planner's `disposition` is authoritative; the code checks remain only for
|
|
2606
|
+
* plans produced by older manifests/paths that predate the disposition contract.
|
|
2304
2607
|
*/
|
|
2305
2608
|
function analyticalPlanAllowsDbtExploration(plan) {
|
|
2609
|
+
if (plan.disposition === 'exploratory_candidate')
|
|
2610
|
+
return true;
|
|
2306
2611
|
if (plan.safe)
|
|
2307
2612
|
return false;
|
|
2308
2613
|
if (plan.code === 'unbound_relation' || plan.code === 'relationship_not_certified')
|
|
@@ -3065,6 +3370,15 @@ function applyParsedProposalMetadata(target, source) {
|
|
|
3065
3370
|
function buildCertifiedBlockDqlArtifact(block, result) {
|
|
3066
3371
|
if (block.kind !== 'block')
|
|
3067
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
|
+
};
|
|
3068
3382
|
const sql = block.sql?.trim() ?? result?.sql?.trim() ?? block.examples?.find((example) => example.sql?.trim())?.sql?.trim();
|
|
3069
3383
|
if (!sql) {
|
|
3070
3384
|
// The block answered but its SQL was not inlined in the index (navigation /
|
|
@@ -3080,6 +3394,7 @@ function buildCertifiedBlockDqlArtifact(block, result) {
|
|
|
3080
3394
|
source: `// Certified DQL block "${escapeDqlArtifactString(block.name)}"`
|
|
3081
3395
|
+ `${block.sourcePath ? `\n// source: ${block.sourcePath}` : ''}`
|
|
3082
3396
|
+ `\n// SQL is not inlined in the index — open the source file to view or edit the query.`,
|
|
3397
|
+
...artifactRuntime,
|
|
3083
3398
|
};
|
|
3084
3399
|
}
|
|
3085
3400
|
const domain = block.domain ?? 'misc';
|
|
@@ -3089,7 +3404,7 @@ function buildCertifiedBlockDqlArtifact(block, result) {
|
|
|
3089
3404
|
domain = "${escapeDqlArtifactString(domain)}"
|
|
3090
3405
|
type = "custom"
|
|
3091
3406
|
status = "certified"${block.owner ? `\n owner = "${escapeDqlArtifactString(block.owner)}"` : ''}
|
|
3092
|
-
description = """${escapeDqlArtifactTripleString(description)}"""${sourcePathComment}
|
|
3407
|
+
description = """${escapeDqlArtifactTripleString(description)}"""${sourcePathComment}${parameterContract}
|
|
3093
3408
|
|
|
3094
3409
|
query = """
|
|
3095
3410
|
${sql.replace(/"""/g, '\\"\\"\\"').split('\n').join('\n ')}
|
|
@@ -3101,8 +3416,51 @@ function buildCertifiedBlockDqlArtifact(block, result) {
|
|
|
3101
3416
|
name: block.name,
|
|
3102
3417
|
sourcePath: block.sourcePath,
|
|
3103
3418
|
source,
|
|
3419
|
+
compiledSql: result?.sql ?? sql,
|
|
3420
|
+
...artifactRuntime,
|
|
3104
3421
|
};
|
|
3105
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
|
+
}
|
|
3106
3464
|
function buildGeneratedSqlDqlArtifact(input) {
|
|
3107
3465
|
const sql = input.sql?.trim();
|
|
3108
3466
|
if (!sql)
|