@duckcodeailabs/dql-agent 1.6.21 → 1.6.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/dist/agent-run-engine.d.ts +287 -0
  2. package/dist/agent-run-engine.d.ts.map +1 -0
  3. package/dist/agent-run-engine.js +916 -0
  4. package/dist/agent-run-engine.js.map +1 -0
  5. package/dist/agent-run-gates.d.ts +26 -0
  6. package/dist/agent-run-gates.d.ts.map +1 -0
  7. package/dist/agent-run-gates.js +170 -0
  8. package/dist/agent-run-gates.js.map +1 -0
  9. package/dist/agent-run-planner.d.ts +32 -0
  10. package/dist/agent-run-planner.d.ts.map +1 -0
  11. package/dist/agent-run-planner.js +280 -0
  12. package/dist/agent-run-planner.js.map +1 -0
  13. package/dist/answer-loop.d.ts +49 -0
  14. package/dist/answer-loop.d.ts.map +1 -1
  15. package/dist/answer-loop.js +292 -17
  16. package/dist/answer-loop.js.map +1 -1
  17. package/dist/app-builder.d.ts +6 -0
  18. package/dist/app-builder.d.ts.map +1 -1
  19. package/dist/app-builder.js +95 -2
  20. package/dist/app-builder.js.map +1 -1
  21. package/dist/app-planner.d.ts +78 -0
  22. package/dist/app-planner.d.ts.map +1 -0
  23. package/dist/app-planner.js +145 -0
  24. package/dist/app-planner.js.map +1 -0
  25. package/dist/embeddings/provider.d.ts +66 -0
  26. package/dist/embeddings/provider.d.ts.map +1 -0
  27. package/dist/embeddings/provider.js +105 -0
  28. package/dist/embeddings/provider.js.map +1 -0
  29. package/dist/hints/git-store.d.ts +69 -0
  30. package/dist/hints/git-store.d.ts.map +1 -0
  31. package/dist/hints/git-store.js +244 -0
  32. package/dist/hints/git-store.js.map +1 -0
  33. package/dist/hints/retrieval.d.ts +49 -0
  34. package/dist/hints/retrieval.d.ts.map +1 -0
  35. package/dist/hints/retrieval.js +60 -0
  36. package/dist/hints/retrieval.js.map +1 -0
  37. package/dist/hints/store.d.ts +39 -0
  38. package/dist/hints/store.d.ts.map +1 -0
  39. package/dist/hints/store.js +279 -0
  40. package/dist/hints/store.js.map +1 -0
  41. package/dist/hints/types.d.ts +147 -0
  42. package/dist/hints/types.d.ts.map +1 -0
  43. package/dist/hints/types.js +92 -0
  44. package/dist/hints/types.js.map +1 -0
  45. package/dist/index.d.ts +43 -6
  46. package/dist/index.d.ts.map +1 -1
  47. package/dist/index.js +35 -3
  48. package/dist/index.js.map +1 -1
  49. package/dist/intent-controller.d.ts +63 -0
  50. package/dist/intent-controller.d.ts.map +1 -0
  51. package/dist/intent-controller.js +150 -0
  52. package/dist/intent-controller.js.map +1 -0
  53. package/dist/kg/build.d.ts.map +1 -1
  54. package/dist/kg/build.js +2 -0
  55. package/dist/kg/build.js.map +1 -1
  56. package/dist/kg/sqlite-fts.js +4 -0
  57. package/dist/kg/sqlite-fts.js.map +1 -1
  58. package/dist/kg/types.d.ts +10 -0
  59. package/dist/kg/types.d.ts.map +1 -1
  60. package/dist/metadata/catalog.d.ts +81 -4
  61. package/dist/metadata/catalog.d.ts.map +1 -1
  62. package/dist/metadata/catalog.js +208 -1
  63. package/dist/metadata/catalog.js.map +1 -1
  64. package/dist/metadata/drafts.d.ts +22 -0
  65. package/dist/metadata/drafts.d.ts.map +1 -1
  66. package/dist/metadata/drafts.js +126 -2
  67. package/dist/metadata/drafts.js.map +1 -1
  68. package/dist/metadata/grain-gate.d.ts +61 -0
  69. package/dist/metadata/grain-gate.d.ts.map +1 -0
  70. package/dist/metadata/grain-gate.js +265 -0
  71. package/dist/metadata/grain-gate.js.map +1 -0
  72. package/dist/metadata/identity.d.ts +47 -0
  73. package/dist/metadata/identity.d.ts.map +1 -0
  74. package/dist/metadata/identity.js +147 -0
  75. package/dist/metadata/identity.js.map +1 -0
  76. package/dist/metadata/metric-match.d.ts +93 -0
  77. package/dist/metadata/metric-match.d.ts.map +1 -0
  78. package/dist/metadata/metric-match.js +233 -0
  79. package/dist/metadata/metric-match.js.map +1 -0
  80. package/dist/metadata/sql-grounding.d.ts +149 -0
  81. package/dist/metadata/sql-grounding.d.ts.map +1 -0
  82. package/dist/metadata/sql-grounding.js +397 -0
  83. package/dist/metadata/sql-grounding.js.map +1 -0
  84. package/dist/metadata/sql-retrieval.d.ts +29 -0
  85. package/dist/metadata/sql-retrieval.d.ts.map +1 -0
  86. package/dist/metadata/sql-retrieval.js +79 -0
  87. package/dist/metadata/sql-retrieval.js.map +1 -0
  88. package/dist/narrate.d.ts +52 -0
  89. package/dist/narrate.d.ts.map +1 -0
  90. package/dist/narrate.js +263 -0
  91. package/dist/narrate.js.map +1 -0
  92. package/dist/propose/build-from-prompt.d.ts +160 -0
  93. package/dist/propose/build-from-prompt.d.ts.map +1 -0
  94. package/dist/propose/build-from-prompt.js +815 -0
  95. package/dist/propose/build-from-prompt.js.map +1 -0
  96. package/dist/propose/classify.d.ts +55 -0
  97. package/dist/propose/classify.d.ts.map +1 -0
  98. package/dist/propose/classify.js +167 -0
  99. package/dist/propose/classify.js.map +1 -0
  100. package/dist/propose/config.d.ts +39 -0
  101. package/dist/propose/config.d.ts.map +1 -0
  102. package/dist/propose/config.js +52 -0
  103. package/dist/propose/config.js.map +1 -0
  104. package/dist/propose/dbt-artifacts.d.ts +134 -0
  105. package/dist/propose/dbt-artifacts.d.ts.map +1 -0
  106. package/dist/propose/dbt-artifacts.js +320 -0
  107. package/dist/propose/dbt-artifacts.js.map +1 -0
  108. package/dist/propose/edit-block.d.ts +74 -0
  109. package/dist/propose/edit-block.d.ts.map +1 -0
  110. package/dist/propose/edit-block.js +133 -0
  111. package/dist/propose/edit-block.js.map +1 -0
  112. package/dist/propose/enrich.d.ts +48 -0
  113. package/dist/propose/enrich.d.ts.map +1 -0
  114. package/dist/propose/enrich.js +125 -0
  115. package/dist/propose/enrich.js.map +1 -0
  116. package/dist/propose/generate-sql.d.ts +61 -0
  117. package/dist/propose/generate-sql.d.ts.map +1 -0
  118. package/dist/propose/generate-sql.js +180 -0
  119. package/dist/propose/generate-sql.js.map +1 -0
  120. package/dist/propose/index.d.ts +24 -0
  121. package/dist/propose/index.d.ts.map +1 -0
  122. package/dist/propose/index.js +15 -0
  123. package/dist/propose/index.js.map +1 -0
  124. package/dist/propose/propose.d.ts +226 -0
  125. package/dist/propose/propose.d.ts.map +1 -0
  126. package/dist/propose/propose.js +668 -0
  127. package/dist/propose/propose.js.map +1 -0
  128. package/dist/propose/reflect-block.d.ts +99 -0
  129. package/dist/propose/reflect-block.d.ts.map +1 -0
  130. package/dist/propose/reflect-block.js +220 -0
  131. package/dist/propose/reflect-block.js.map +1 -0
  132. package/dist/propose/write-draft.d.ts +90 -0
  133. package/dist/propose/write-draft.d.ts.map +1 -0
  134. package/dist/propose/write-draft.js +185 -0
  135. package/dist/propose/write-draft.js.map +1 -0
  136. package/dist/providers/claude.d.ts +13 -1
  137. package/dist/providers/claude.d.ts.map +1 -1
  138. package/dist/providers/claude.js +20 -2
  139. package/dist/providers/claude.js.map +1 -1
  140. package/dist/providers/gemini.d.ts +13 -1
  141. package/dist/providers/gemini.d.ts.map +1 -1
  142. package/dist/providers/gemini.js +24 -3
  143. package/dist/providers/gemini.js.map +1 -1
  144. package/dist/providers/index.d.ts +3 -3
  145. package/dist/providers/index.d.ts.map +1 -1
  146. package/dist/providers/index.js +3 -3
  147. package/dist/providers/index.js.map +1 -1
  148. package/dist/research-loop.d.ts +62 -0
  149. package/dist/research-loop.d.ts.map +1 -0
  150. package/dist/research-loop.js +210 -0
  151. package/dist/research-loop.js.map +1 -0
  152. package/dist/skills/defaults.d.ts +37 -0
  153. package/dist/skills/defaults.d.ts.map +1 -0
  154. package/dist/skills/defaults.js +152 -0
  155. package/dist/skills/defaults.js.map +1 -0
  156. package/dist/skills/loader.d.ts +60 -0
  157. package/dist/skills/loader.d.ts.map +1 -1
  158. package/dist/skills/loader.js +164 -2
  159. package/dist/skills/loader.js.map +1 -1
  160. package/package.json +7 -5
@@ -15,6 +15,8 @@ import { buildKGFromManifest, buildKGFromSemanticLayer } from '../kg/build.js';
15
15
  import { buildBlockBusinessFingerprint, buildBlockSqlFingerprints } from './block-fingerprints.js';
16
16
  import { buildAnalysisQuestionPlan, certifiedApplicabilityForObject, scoreAllowedSqlRelationWithAnalysisPlan, scoreMetadataObjectWithAnalysisPlan, sortAllowedSqlContextForAnalysisPlan, } from './analysis-planner.js';
17
17
  import { extractSimpleSelectShape, sourceSqlShapeColumns } from './sql-shape.js';
18
+ import { grainMatches, requestedGrainFromPlan, } from './grain-gate.js';
19
+ import { retrieveScopedHints } from '../hints/retrieval.js';
18
20
  const require = createRequire(import.meta.url);
19
21
  let databaseCtor = null;
20
22
  function loadDatabase() {
@@ -169,6 +171,7 @@ export async function buildLocalContextPack(projectRoot, request) {
169
171
  limit: request.limit ?? 120,
170
172
  });
171
173
  const conflicts = buildCandidateConflicts(reranked.ranked);
174
+ const compileConflicts = catalog.compileConflicts();
172
175
  const routeDecision = planContextPackRoute({
173
176
  request,
174
177
  objects,
@@ -177,7 +180,17 @@ export async function buildLocalContextPack(projectRoot, request) {
177
180
  diagnostics,
178
181
  trustLabel,
179
182
  questionPlan,
183
+ compileConflicts,
184
+ rankedObjects: reranked.ranked,
180
185
  });
186
+ // Approved scoped correction hints — folded in AFTER certified routing so
187
+ // they never override a certified answer. On the `certified` route we still
188
+ // surface in-scope hints as advisory context, but never to change the route.
189
+ const questionScope = deriveQuestionScope(request, questionPlan, objects, routeDecision);
190
+ const hintResult = await retrieveScopedHints(projectRoot, {
191
+ questionScope,
192
+ limit: 6,
193
+ }).catch(() => ({ applied: [], conflicts: [] }));
181
194
  const payload = {
182
195
  id: '',
183
196
  question: request.question,
@@ -197,6 +210,8 @@ export async function buildLocalContextPack(projectRoot, request) {
197
210
  allowedSqlContext,
198
211
  missingContext: routeDecision.missingContext,
199
212
  conflicts,
213
+ appliedHints: hintResult.applied,
214
+ hintConflicts: hintResult.conflicts,
200
215
  retrievalDiagnostics: {
201
216
  strategy: 'sqlite_fts',
202
217
  selectedObjects: objects.length,
@@ -309,12 +324,16 @@ export function buildMetadataSnapshot(projectRoot, manifest, semanticLayer) {
309
324
  edges.set(key, normalized);
310
325
  }
311
326
  addProjectDiagnostics(manifest, semanticLayer, diagnostics);
327
+ const compileConflicts = (manifest.diagnostics ?? [])
328
+ .filter((diagnostic) => diagnostic.kind === 'conflict' && Boolean(diagnostic.conflict))
329
+ .map((diagnostic) => diagnostic.conflict);
312
330
  const snapshot = {
313
331
  projectRoot,
314
332
  manifest,
315
333
  objects: Array.from(objects.values()).sort((a, b) => a.objectKey.localeCompare(b.objectKey)),
316
334
  edges: Array.from(edges.values()).sort((a, b) => `${a.edgeType}|${a.fromKey}|${a.toKey}`.localeCompare(`${b.edgeType}|${b.fromKey}|${b.toKey}`)),
317
335
  diagnostics,
336
+ compileConflicts,
318
337
  generatedAt: new Date().toISOString(),
319
338
  fingerprint: '',
320
339
  };
@@ -514,6 +533,7 @@ export class MetadataCatalog {
514
533
  setState.run('source_fingerprint_count', String(sourceFingerprints.length));
515
534
  setState.run('domain_shard_count', String(domainShards.length));
516
535
  setState.run('diagnostics_json', JSON.stringify(snapshot.diagnostics));
536
+ setState.run('compile_conflicts_json', JSON.stringify(snapshot.compileConflicts ?? []));
517
537
  setState.run('manifest_generated_at', snapshot.manifest.generatedAt);
518
538
  });
519
539
  txn();
@@ -738,6 +758,14 @@ export class MetadataCatalog {
738
758
  filePath: row.file_path ?? undefined,
739
759
  }));
740
760
  }
761
+ /** Structured compile-time trust conflicts persisted with the last rebuild. */
762
+ compileConflicts() {
763
+ const raw = this.state('compile_conflicts_json');
764
+ if (!raw)
765
+ return [];
766
+ const parsed = safeJson(raw, null);
767
+ return Array.isArray(parsed) ? parsed : [];
768
+ }
741
769
  sourceFingerprints(limit = 500) {
742
770
  const rows = this.db.prepare(`
743
771
  SELECT source_path, fingerprint, object_count, updated_at
@@ -1459,8 +1487,71 @@ function addProjectDiagnostics(manifest, semanticLayer, diagnostics) {
1459
1487
  });
1460
1488
  }
1461
1489
  }
1490
+ /**
1491
+ * Resolve the question's scope (metric / dbt model / domain / term / block) for
1492
+ * scoped-hint matching. Drawn from the route's chosen objects + question plan so
1493
+ * a hint only applies inside the same metric/model/domain it was approved for.
1494
+ * Dialect is intentionally left undefined here (unknown at this layer); the hint
1495
+ * scope matcher tolerates an unknown question dialect.
1496
+ */
1497
+ function deriveQuestionScope(request, questionPlan, objects, routeDecision) {
1498
+ const focusKey = routeDecision.exactObjectKey ?? request.focusObjectKey;
1499
+ const focus = focusKey ? objects.find((object) => object.objectKey === focusKey) : undefined;
1500
+ const topByType = (type) => objects.find((object) => object.objectType === type);
1501
+ const metricObject = (focus && (focus.objectType === 'semantic_metric' || focus.objectType === 'dql_block') ? focus : undefined) ??
1502
+ topByType('semantic_metric');
1503
+ const dbtObject = topByType('dbt_model');
1504
+ const metric = firstNonEmpty(metricObject?.objectType === 'semantic_metric' ? metricObject.name : undefined, questionPlan.metricTerms[0]);
1505
+ const domain = firstNonEmpty(focus?.domain, metricObject?.domain, dbtObject?.domain, ...objects.map((o) => o.domain));
1506
+ const dbtModel = dbtObject?.name;
1507
+ const block = focus?.objectType === 'dql_block' ? focus.name : undefined;
1508
+ const term = focus?.objectType === 'dql_term' ? focus.name : topByType('dql_term')?.name;
1509
+ return {
1510
+ metric: lowerOrUndef(metric),
1511
+ dbtModel: lowerOrUndef(dbtModel),
1512
+ domain: lowerOrUndef(domain),
1513
+ term: lowerOrUndef(term),
1514
+ block,
1515
+ text: request.question,
1516
+ };
1517
+ }
1518
+ function firstNonEmpty(...values) {
1519
+ for (const value of values) {
1520
+ if (value && value.trim().length > 0)
1521
+ return value;
1522
+ }
1523
+ return undefined;
1524
+ }
1525
+ function lowerOrUndef(value) {
1526
+ return value ? value.trim().toLowerCase() : undefined;
1527
+ }
1462
1528
  function planContextPackRoute(input) {
1463
1529
  const intent = input.request.intent ?? input.questionPlan.routeIntent ?? classifyMetadataIntent(input.request.question, input.request.followUp);
1530
+ // Conflict route (additive, evaluated first): if the question's top governed
1531
+ // candidates include BOTH sides of a compile-time trust conflict, refuse to
1532
+ // silently pick one. Surface both definitions + owners + a disambiguation
1533
+ // prompt. Only fires when both conflicting sides are actually among the
1534
+ // selected candidates for THIS question, so non-conflicting asks are
1535
+ // unaffected even when a conflict exists elsewhere in the project.
1536
+ const routeConflict = pickRouteConflict(input.compileConflicts ?? [], input.rankedObjects ?? [], input.objects);
1537
+ if (routeConflict) {
1538
+ const selectedEvidenceForConflict = input.evidenceRoles.slice(0, 16);
1539
+ return {
1540
+ route: 'conflict',
1541
+ intent,
1542
+ reason: `Two certified ${routeConflict.objectType}s claim "${routeConflict.concept}" but disagree (${routeConflict.reason}). DQL refuses to guess which is authoritative.`,
1543
+ trustLabel: 'conflict',
1544
+ reviewStatus: 'conflict',
1545
+ selectedEvidence: selectedEvidenceForConflict,
1546
+ missingContext: [{
1547
+ kind: 'metadata',
1548
+ severity: 'blocking',
1549
+ message: routeConflict.prompt,
1550
+ }],
1551
+ followUps: [routeConflict.prompt],
1552
+ routeConflict,
1553
+ };
1554
+ }
1464
1555
  const applicabilityByKey = certifiedApplicabilities(input.objects, input.questionPlan);
1465
1556
  const exactByApplicability = [...applicabilityByKey.values()]
1466
1557
  .filter((item) => item.kind === 'exact_answer' || item.kind === 'safe_parameterized')
@@ -1477,7 +1568,35 @@ function planContextPackRoute(input) {
1477
1568
  const exactExampleMatch = exact ? hasExactExampleQuestion(input.request.question, exact) : false;
1478
1569
  const missingContext = buildMissingContext(input.request, intent, input.objects, input.allowedSqlContext);
1479
1570
  const selectedEvidence = input.evidenceRoles.slice(0, 16);
1480
- if (exact && (certifiedApplicability?.kind === 'exact_answer'
1571
+ // Grain / contract gate (refinement of the existing certified→generated
1572
+ // demotion). The candidate certified block is only served at Tier 1 when its
1573
+ // declared grain actually satisfies the question's requested grain. An
1574
+ // explicit example/name match bypasses the gate — the user is naming the
1575
+ // block directly, so grain is implicitly accepted.
1576
+ const requestedGrain = requestedGrainFromPlan(input.questionPlan);
1577
+ const grainGate = exact && !exactExampleMatch && intent !== 'definition_lookup'
1578
+ ? grainMatches(exact, requestedGrain)
1579
+ : undefined;
1580
+ const grainGateInfo = exact && grainGate
1581
+ ? {
1582
+ blockObjectKey: exact.objectKey,
1583
+ blockName: exact.name,
1584
+ allow: grainGate.allow,
1585
+ kind: grainGate.kind,
1586
+ requestedGrain: grainGate.requestedGrainLabel,
1587
+ blockGrain: grainGate.blockGrainLabel,
1588
+ reason: grainGate.reason,
1589
+ }
1590
+ : undefined;
1591
+ // A genuine grain/entity mismatch demotes the candidate to Tier 2 — but only
1592
+ // when generated SQL is actually possible. If nothing else can answer, we do
1593
+ // not strand the user; the existing certified fallback still applies (and
1594
+ // query_via_block enforces the gate defensively at execution time).
1595
+ const canGenerateFromContext = input.allowedSqlContext.relations.length > 0 ||
1596
+ input.allowedSqlContext.sourceBlockSql.length > 0 ||
1597
+ input.objects.some((object) => object.objectType.startsWith('semantic_'));
1598
+ const grainGateDemotes = Boolean(grainGate && !grainGate.allow && canGenerateFromContext);
1599
+ if (!grainGateDemotes && exact && (certifiedApplicability?.kind === 'exact_answer'
1481
1600
  || certifiedApplicability?.kind === 'safe_parameterized'
1482
1601
  || intent === 'exact_certified_lookup'
1483
1602
  || intent === 'definition_lookup'
@@ -1487,6 +1606,8 @@ function planContextPackRoute(input) {
1487
1606
  route: 'certified',
1488
1607
  intent,
1489
1608
  reason: `Certified ${exact.objectType.replace(/_/g, ' ')} "${exact.name}" exactly matches the requested artifact, definition, or direct KPI grain.`,
1609
+ routeReason: grainGateInfo?.reason,
1610
+ grainGate: grainGateInfo,
1490
1611
  trustLabel: 'certified',
1491
1612
  reviewStatus: 'certified',
1492
1613
  exactObjectKey: exact.objectKey,
@@ -1496,6 +1617,26 @@ function planContextPackRoute(input) {
1496
1617
  followUps: buildMetadataFollowUps(intent, input.allowedSqlContext),
1497
1618
  };
1498
1619
  }
1620
+ // Grain-gated demotion: the best certified candidate is close but answers a
1621
+ // different grain. Serve Tier 2 (generated from context) with the block kept
1622
+ // as context only, instead of a confidently-wrong governed answer.
1623
+ if (grainGateDemotes && exact && grainGate) {
1624
+ return {
1625
+ route: 'generated_sql',
1626
+ intent: isGeneratedMetadataIntent(intent) ? intent : 'ad_hoc_ranking',
1627
+ reason: `Certified block "${exact.name}" is close but answers a different grain than the question, so it is context only and SQL is generated for the requested grain.`,
1628
+ routeReason: grainGate.reason,
1629
+ grainGate: grainGateInfo,
1630
+ trustLabel: input.trustLabel === 'certified' ? 'mixed' : input.trustLabel,
1631
+ reviewStatus: 'draft_ready',
1632
+ certifiedApplicability: certifiedApplicability
1633
+ ? { ...certifiedApplicability, kind: 'context_only' }
1634
+ : contextApplicability,
1635
+ selectedEvidence,
1636
+ missingContext,
1637
+ followUps: buildMetadataFollowUps(isGeneratedMetadataIntent(intent) ? intent : 'ad_hoc_ranking', input.allowedSqlContext),
1638
+ };
1639
+ }
1499
1640
  if (intent === 'trust_gap_review') {
1500
1641
  if (input.objects.length === 0) {
1501
1642
  return clarifyDecision(intent, input.trustLabel, selectedEvidence, [{
@@ -2200,9 +2341,28 @@ function buildAllowedSqlContext(objects, edges) {
2200
2341
  name: object.name,
2201
2342
  status: object.status,
2202
2343
  sql: String(object.payload?.sql ?? ''),
2344
+ description: typeof object.payload?.description === 'string' ? object.payload.description : undefined,
2345
+ exampleQuestion: firstExampleQuestion(object.payload?.examples),
2346
+ grain: typeof object.payload?.grain === 'string' ? object.payload.grain : undefined,
2203
2347
  })),
2204
2348
  };
2205
2349
  }
2350
+ // Pull the first natural-language example question off a block payload so the
2351
+ // certified block can serve as a question→SQL few-shot exemplar.
2352
+ function firstExampleQuestion(examples) {
2353
+ if (!Array.isArray(examples))
2354
+ return undefined;
2355
+ for (const example of examples) {
2356
+ if (typeof example === 'string' && example.trim())
2357
+ return example.trim();
2358
+ if (example && typeof example === 'object') {
2359
+ const question = example.question;
2360
+ if (typeof question === 'string' && question.trim())
2361
+ return question.trim();
2362
+ }
2363
+ }
2364
+ return undefined;
2365
+ }
2206
2366
  function buildSelectedJoinPaths(allowedSqlContext) {
2207
2367
  const relations = allowedSqlContext.relations
2208
2368
  .filter((relation) => relation.columns.some((column) => isJoinKeyColumnForCatalog(column.name)))
@@ -3028,6 +3188,53 @@ function buildCandidateConflicts(ranked) {
3028
3188
  function isGovernedCandidate(row) {
3029
3189
  return row.status === 'certified' || row.status === 'approved';
3030
3190
  }
3191
+ /**
3192
+ * Select a compile-time trust conflict to route on, but only when BOTH of its
3193
+ * sides appear among the question's top candidates. This keeps the conflict
3194
+ * route scoped to questions the conflicting pair actually answers — a conflict
3195
+ * elsewhere in the project never hijacks an unrelated question.
3196
+ */
3197
+ function pickRouteConflict(compileConflicts, rankedObjects, fallbackObjects) {
3198
+ if (compileConflicts.length === 0)
3199
+ return undefined;
3200
+ // Names of the top candidate objects for this question (bounded — only the
3201
+ // strongest matches count as "top candidates").
3202
+ const topNames = new Set();
3203
+ const ordered = rankedObjects.length > 0
3204
+ ? rankedObjects.slice(0, 12).map((item) => item.row)
3205
+ : fallbackObjects.slice(0, 12);
3206
+ for (const row of ordered) {
3207
+ const norm = normalizeConflictName(row.name);
3208
+ if (norm)
3209
+ topNames.add(norm);
3210
+ }
3211
+ for (const conflict of compileConflicts) {
3212
+ const sideNames = conflict.sides.map((side) => normalizeConflictName(side.name)).filter(Boolean);
3213
+ if (sideNames.length < 2)
3214
+ continue;
3215
+ const presentSides = sideNames.filter((name) => topNames.has(name));
3216
+ if (presentSides.length < 2)
3217
+ continue;
3218
+ return {
3219
+ objectType: conflict.objectType,
3220
+ concept: conflict.concept,
3221
+ reason: conflict.reason,
3222
+ prompt: conflict.prompt,
3223
+ sides: conflict.sides.map((side) => ({
3224
+ name: side.name,
3225
+ owner: side.owner,
3226
+ domain: side.domain,
3227
+ filePath: side.filePath,
3228
+ definition: side.definition,
3229
+ businessRules: side.businessRules,
3230
+ })),
3231
+ };
3232
+ }
3233
+ return undefined;
3234
+ }
3235
+ function normalizeConflictName(value) {
3236
+ return (value ?? '').toLowerCase().replace(/[^a-z0-9]+/g, ' ').trim();
3237
+ }
3031
3238
  function tokenize(text) {
3032
3239
  return Array.from(new Set(text.toLowerCase().replace(/[^a-z0-9_ ]+/g, ' ').split(/\s+/).filter((term) => term.length >= 3))).slice(0, 24);
3033
3240
  }