@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.
- package/dist/agent-run-engine.d.ts +287 -0
- package/dist/agent-run-engine.d.ts.map +1 -0
- package/dist/agent-run-engine.js +916 -0
- package/dist/agent-run-engine.js.map +1 -0
- package/dist/agent-run-gates.d.ts +26 -0
- package/dist/agent-run-gates.d.ts.map +1 -0
- package/dist/agent-run-gates.js +170 -0
- package/dist/agent-run-gates.js.map +1 -0
- package/dist/agent-run-planner.d.ts +32 -0
- package/dist/agent-run-planner.d.ts.map +1 -0
- package/dist/agent-run-planner.js +280 -0
- package/dist/agent-run-planner.js.map +1 -0
- package/dist/answer-loop.d.ts +49 -0
- package/dist/answer-loop.d.ts.map +1 -1
- package/dist/answer-loop.js +292 -17
- package/dist/answer-loop.js.map +1 -1
- package/dist/app-builder.d.ts +6 -0
- package/dist/app-builder.d.ts.map +1 -1
- package/dist/app-builder.js +95 -2
- package/dist/app-builder.js.map +1 -1
- package/dist/app-planner.d.ts +78 -0
- package/dist/app-planner.d.ts.map +1 -0
- package/dist/app-planner.js +145 -0
- package/dist/app-planner.js.map +1 -0
- package/dist/embeddings/provider.d.ts +66 -0
- package/dist/embeddings/provider.d.ts.map +1 -0
- package/dist/embeddings/provider.js +105 -0
- package/dist/embeddings/provider.js.map +1 -0
- package/dist/hints/git-store.d.ts +69 -0
- package/dist/hints/git-store.d.ts.map +1 -0
- package/dist/hints/git-store.js +244 -0
- package/dist/hints/git-store.js.map +1 -0
- package/dist/hints/retrieval.d.ts +49 -0
- package/dist/hints/retrieval.d.ts.map +1 -0
- package/dist/hints/retrieval.js +60 -0
- package/dist/hints/retrieval.js.map +1 -0
- package/dist/hints/store.d.ts +39 -0
- package/dist/hints/store.d.ts.map +1 -0
- package/dist/hints/store.js +279 -0
- package/dist/hints/store.js.map +1 -0
- package/dist/hints/types.d.ts +147 -0
- package/dist/hints/types.d.ts.map +1 -0
- package/dist/hints/types.js +92 -0
- package/dist/hints/types.js.map +1 -0
- package/dist/index.d.ts +43 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +35 -3
- package/dist/index.js.map +1 -1
- package/dist/intent-controller.d.ts +63 -0
- package/dist/intent-controller.d.ts.map +1 -0
- package/dist/intent-controller.js +150 -0
- package/dist/intent-controller.js.map +1 -0
- package/dist/kg/build.d.ts.map +1 -1
- package/dist/kg/build.js +2 -0
- package/dist/kg/build.js.map +1 -1
- package/dist/kg/sqlite-fts.js +4 -0
- package/dist/kg/sqlite-fts.js.map +1 -1
- package/dist/kg/types.d.ts +10 -0
- package/dist/kg/types.d.ts.map +1 -1
- package/dist/metadata/catalog.d.ts +81 -4
- package/dist/metadata/catalog.d.ts.map +1 -1
- package/dist/metadata/catalog.js +208 -1
- package/dist/metadata/catalog.js.map +1 -1
- package/dist/metadata/drafts.d.ts +22 -0
- package/dist/metadata/drafts.d.ts.map +1 -1
- package/dist/metadata/drafts.js +126 -2
- package/dist/metadata/drafts.js.map +1 -1
- package/dist/metadata/grain-gate.d.ts +61 -0
- package/dist/metadata/grain-gate.d.ts.map +1 -0
- package/dist/metadata/grain-gate.js +265 -0
- package/dist/metadata/grain-gate.js.map +1 -0
- package/dist/metadata/identity.d.ts +47 -0
- package/dist/metadata/identity.d.ts.map +1 -0
- package/dist/metadata/identity.js +147 -0
- package/dist/metadata/identity.js.map +1 -0
- package/dist/metadata/metric-match.d.ts +93 -0
- package/dist/metadata/metric-match.d.ts.map +1 -0
- package/dist/metadata/metric-match.js +233 -0
- package/dist/metadata/metric-match.js.map +1 -0
- package/dist/metadata/sql-grounding.d.ts +149 -0
- package/dist/metadata/sql-grounding.d.ts.map +1 -0
- package/dist/metadata/sql-grounding.js +397 -0
- package/dist/metadata/sql-grounding.js.map +1 -0
- package/dist/metadata/sql-retrieval.d.ts +29 -0
- package/dist/metadata/sql-retrieval.d.ts.map +1 -0
- package/dist/metadata/sql-retrieval.js +79 -0
- package/dist/metadata/sql-retrieval.js.map +1 -0
- package/dist/narrate.d.ts +52 -0
- package/dist/narrate.d.ts.map +1 -0
- package/dist/narrate.js +263 -0
- package/dist/narrate.js.map +1 -0
- package/dist/propose/build-from-prompt.d.ts +160 -0
- package/dist/propose/build-from-prompt.d.ts.map +1 -0
- package/dist/propose/build-from-prompt.js +815 -0
- package/dist/propose/build-from-prompt.js.map +1 -0
- package/dist/propose/classify.d.ts +55 -0
- package/dist/propose/classify.d.ts.map +1 -0
- package/dist/propose/classify.js +167 -0
- package/dist/propose/classify.js.map +1 -0
- package/dist/propose/config.d.ts +39 -0
- package/dist/propose/config.d.ts.map +1 -0
- package/dist/propose/config.js +52 -0
- package/dist/propose/config.js.map +1 -0
- package/dist/propose/dbt-artifacts.d.ts +134 -0
- package/dist/propose/dbt-artifacts.d.ts.map +1 -0
- package/dist/propose/dbt-artifacts.js +320 -0
- package/dist/propose/dbt-artifacts.js.map +1 -0
- package/dist/propose/edit-block.d.ts +74 -0
- package/dist/propose/edit-block.d.ts.map +1 -0
- package/dist/propose/edit-block.js +133 -0
- package/dist/propose/edit-block.js.map +1 -0
- package/dist/propose/enrich.d.ts +48 -0
- package/dist/propose/enrich.d.ts.map +1 -0
- package/dist/propose/enrich.js +125 -0
- package/dist/propose/enrich.js.map +1 -0
- package/dist/propose/generate-sql.d.ts +61 -0
- package/dist/propose/generate-sql.d.ts.map +1 -0
- package/dist/propose/generate-sql.js +180 -0
- package/dist/propose/generate-sql.js.map +1 -0
- package/dist/propose/index.d.ts +24 -0
- package/dist/propose/index.d.ts.map +1 -0
- package/dist/propose/index.js +15 -0
- package/dist/propose/index.js.map +1 -0
- package/dist/propose/propose.d.ts +226 -0
- package/dist/propose/propose.d.ts.map +1 -0
- package/dist/propose/propose.js +668 -0
- package/dist/propose/propose.js.map +1 -0
- package/dist/propose/reflect-block.d.ts +99 -0
- package/dist/propose/reflect-block.d.ts.map +1 -0
- package/dist/propose/reflect-block.js +220 -0
- package/dist/propose/reflect-block.js.map +1 -0
- package/dist/propose/write-draft.d.ts +90 -0
- package/dist/propose/write-draft.d.ts.map +1 -0
- package/dist/propose/write-draft.js +185 -0
- package/dist/propose/write-draft.js.map +1 -0
- package/dist/providers/claude.d.ts +13 -1
- package/dist/providers/claude.d.ts.map +1 -1
- package/dist/providers/claude.js +20 -2
- package/dist/providers/claude.js.map +1 -1
- package/dist/providers/gemini.d.ts +13 -1
- package/dist/providers/gemini.d.ts.map +1 -1
- package/dist/providers/gemini.js +24 -3
- package/dist/providers/gemini.js.map +1 -1
- package/dist/providers/index.d.ts +3 -3
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js +3 -3
- package/dist/providers/index.js.map +1 -1
- package/dist/research-loop.d.ts +62 -0
- package/dist/research-loop.d.ts.map +1 -0
- package/dist/research-loop.js +210 -0
- package/dist/research-loop.js.map +1 -0
- package/dist/skills/defaults.d.ts +37 -0
- package/dist/skills/defaults.d.ts.map +1 -0
- package/dist/skills/defaults.js +152 -0
- package/dist/skills/defaults.js.map +1 -0
- package/dist/skills/loader.d.ts +60 -0
- package/dist/skills/loader.d.ts.map +1 -1
- package/dist/skills/loader.js +164 -2
- package/dist/skills/loader.js.map +1 -1
- package/package.json +7 -5
package/dist/metadata/catalog.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|