@duckcodeailabs/dql-agent 1.8.3 → 1.8.5

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.
Files changed (41) hide show
  1. package/dist/agentic/answer-contract.d.ts +1 -0
  2. package/dist/agentic/answer-contract.d.ts.map +1 -1
  3. package/dist/agentic/answer-contract.js.map +1 -1
  4. package/dist/agentic/toolset.d.ts +7 -0
  5. package/dist/agentic/toolset.d.ts.map +1 -1
  6. package/dist/agentic/toolset.js +24 -3
  7. package/dist/agentic/toolset.js.map +1 -1
  8. package/dist/answer-loop.d.ts +10 -3
  9. package/dist/answer-loop.d.ts.map +1 -1
  10. package/dist/answer-loop.js +275 -14
  11. package/dist/answer-loop.js.map +1 -1
  12. package/dist/grounding/context-ledger.d.ts +2 -0
  13. package/dist/grounding/context-ledger.d.ts.map +1 -1
  14. package/dist/grounding/context-ledger.js +5 -1
  15. package/dist/grounding/context-ledger.js.map +1 -1
  16. package/dist/index.d.ts +2 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +1 -1
  19. package/dist/index.js.map +1 -1
  20. package/dist/metadata/block-fit.js +10 -2
  21. package/dist/metadata/block-fit.js.map +1 -1
  22. package/dist/metadata/dbt-first-safety.d.ts +1 -1
  23. package/dist/metadata/dbt-first-safety.d.ts.map +1 -1
  24. package/dist/metadata/dbt-first-safety.js +2 -2
  25. package/dist/metadata/dbt-first-safety.js.map +1 -1
  26. package/dist/metadata/sql-context-validation.d.ts +2 -0
  27. package/dist/metadata/sql-context-validation.d.ts.map +1 -1
  28. package/dist/metadata/sql-context-validation.js +4 -4
  29. package/dist/metadata/sql-context-validation.js.map +1 -1
  30. package/dist/metadata/sql-grounding.d.ts +1 -1
  31. package/dist/metadata/sql-grounding.d.ts.map +1 -1
  32. package/dist/metadata/sql-grounding.js +2 -2
  33. package/dist/metadata/sql-grounding.js.map +1 -1
  34. package/dist/semantic-bridge/compose.d.ts +10 -0
  35. package/dist/semantic-bridge/compose.d.ts.map +1 -1
  36. package/dist/semantic-bridge/compose.js +50 -18
  37. package/dist/semantic-bridge/compose.js.map +1 -1
  38. package/dist/tools/registry.d.ts.map +1 -1
  39. package/dist/tools/registry.js +5 -0
  40. package/dist/tools/registry.js.map +1 -1
  41. package/package.json +4 -4
@@ -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 questionPlan = input.contextPack?.questionPlan?.requestedShape
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 shouldTryCertifiedRoute = shouldUseCertifiedRoute(catalogRoute, intent);
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, { question });
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
- const composed = composeSemanticQueryFromMembers({
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
- const initialValidation = contextLedger.validateSql(parsed.sql, {
1356
- question,
1357
- intent,
1358
- filterValues: input.followUp?.filters,
1359
- trustedFilterValues: trustedFollowUpFilterValues(input.followUp),
1360
- memberBindings: input.followUp?.memberBindings,
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)