@duckcodeailabs/dql-agent 1.4.4 → 1.5.1

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.
@@ -17,47 +17,81 @@
17
17
  import { buildSkillsPrompt } from './skills/loader.js';
18
18
  const CERTIFIED_HIT_THRESHOLD = 0.18;
19
19
  const HARD_NEGATIVE_RATIO = 0.5;
20
+ const ARTIFACT_KINDS = ['block', 'dashboard', 'app', 'notebook'];
21
+ const SEMANTIC_KINDS = ['metric', 'dimension', 'measure', 'entity', 'semantic_model', 'saved_query'];
22
+ const MANIFEST_KINDS = ['dbt_model', 'dbt_source'];
20
23
  export async function answer(input) {
21
24
  const { question, userId, domain, provider, kg, skills = [], blockHints = [] } = input;
22
- const considered = kg.search({
23
- query: question,
24
- domain,
25
- limit: 10,
26
- });
27
- // Stage 1: certified-block match
28
- const blockHits = considered.filter((h) => h.node.kind === 'block');
29
- const blockHit = pickCertifiedBlock(blockHits, blockHints, kg);
30
- if (blockHit) {
25
+ const artifactHits = kg.search({ query: question, domain, kinds: ARTIFACT_KINDS, limit: 10 });
26
+ const semanticHits = kg.search({ query: question, domain, kinds: SEMANTIC_KINDS, limit: 12 });
27
+ const manifestHits = kg.search({ query: question, domain, kinds: MANIFEST_KINDS, limit: 12 });
28
+ const considered = mergeHits(artifactHits, semanticHits, manifestHits, kg.search({ query: question, domain, limit: 10 })).slice(0, 30);
29
+ // Stage 1: certified artifact match. Blocks can be executed; dashboards,
30
+ // Apps, and notebooks are returned as governed citations/navigation targets.
31
+ const artifactHit = pickCertifiedArtifact(artifactHits, blockHints, kg);
32
+ if (artifactHit) {
31
33
  let result;
32
34
  let executionError;
33
- if (input.executeCertifiedBlock) {
35
+ if (artifactHit.node.kind === 'block' && input.executeCertifiedBlock) {
34
36
  try {
35
- result = await input.executeCertifiedBlock(blockHit.node);
37
+ result = await input.executeCertifiedBlock(artifactHit.node);
36
38
  }
37
39
  catch (err) {
38
40
  executionError = err instanceof Error ? err.message : String(err);
39
41
  }
40
42
  }
43
+ const text = composeCertifiedAnswer(artifactHit.node, question, result, executionError);
44
+ const citations = [
45
+ {
46
+ nodeId: artifactHit.node.nodeId,
47
+ kind: artifactHit.node.kind,
48
+ name: artifactHit.node.name,
49
+ gitSha: artifactHit.node.gitSha,
50
+ sourceTier: 'certified_artifact',
51
+ provenance: artifactHit.node.provenance,
52
+ },
53
+ ];
41
54
  return {
42
55
  kind: 'certified',
43
- text: composeCertifiedAnswer(blockHit.node, question, result, executionError),
44
- block: blockHit.node,
56
+ sourceTier: 'certified_artifact',
57
+ certification: 'certified',
58
+ reviewStatus: 'certified',
59
+ confidence: 0.95,
60
+ text,
61
+ answer: text,
62
+ block: artifactHit.node.kind === 'block' ? artifactHit.node : undefined,
45
63
  result,
46
64
  executionError,
47
- citations: [
48
- {
49
- nodeId: blockHit.node.nodeId,
50
- kind: blockHit.node.kind,
51
- name: blockHit.node.name,
52
- gitSha: blockHit.node.gitSha,
53
- },
54
- ],
65
+ sql: result?.sql,
66
+ citations,
67
+ memoryContext: input.memoryContext,
68
+ evidence: buildCertifiedEvidence({
69
+ question,
70
+ artifact: artifactHit.node,
71
+ semanticHits,
72
+ manifestHits,
73
+ considered,
74
+ result,
75
+ executionError,
76
+ executorWasAvailable: Boolean(input.executeCertifiedBlock),
77
+ citations,
78
+ memoryContext: input.memoryContext ?? [],
79
+ }),
55
80
  considered,
56
81
  providerUsed: provider.name,
57
82
  };
58
83
  }
59
- // Stage 2: uncertified gather context, ask the LLM
60
- const contextNodes = considered.slice(0, 6).map((h) => h.node);
84
+ // Stage 2/3: generate only after certified artifacts miss. Semantic context
85
+ // wins over raw dbt manifest context; memory is appended last as advisory.
86
+ const activeTier = semanticHits.length > 0
87
+ ? 'semantic_layer'
88
+ : manifestHits.length > 0
89
+ ? 'dbt_manifest'
90
+ : 'dbt_manifest';
91
+ const contextHits = activeTier === 'semantic_layer'
92
+ ? [...semanticHits, ...manifestHits].slice(0, 10)
93
+ : manifestHits.slice(0, 10);
94
+ const contextNodes = (contextHits.length > 0 ? contextHits : considered.slice(0, 6)).map((h) => h.node);
61
95
  const contextBlocks = contextNodes.filter((n) => n.kind === 'block');
62
96
  const contextOther = contextNodes.filter((n) => n.kind !== 'block');
63
97
  const messages = [
@@ -68,7 +102,7 @@ export async function answer(input) {
68
102
  messages.push({ role: 'system', content: skillsPrompt });
69
103
  messages.push({
70
104
  role: 'system',
71
- content: renderContextPrompt(contextBlocks, contextOther),
105
+ content: renderContextPrompt(contextBlocks, contextOther, activeTier, input.memoryContext ?? [], input.extraContext),
72
106
  });
73
107
  messages.push({ role: 'user', content: question });
74
108
  let proposed = '';
@@ -76,35 +110,96 @@ export async function answer(input) {
76
110
  proposed = await provider.generate(messages, { signal: input.signal });
77
111
  }
78
112
  catch (err) {
113
+ const text = `Provider error: ${err.message}`;
79
114
  return {
80
115
  kind: 'no_answer',
81
- text: `Provider error: ${err.message}`,
116
+ sourceTier: 'no_answer',
117
+ certification: 'analyst_review_required',
118
+ reviewStatus: 'none',
119
+ confidence: 0,
120
+ text,
121
+ answer: text,
82
122
  citations: [],
123
+ memoryContext: input.memoryContext,
124
+ evidence: buildNoAnswerEvidence({
125
+ question,
126
+ reason: text,
127
+ artifactHits,
128
+ semanticHits,
129
+ manifestHits,
130
+ considered,
131
+ memoryContext: input.memoryContext ?? [],
132
+ }),
83
133
  considered,
84
134
  providerUsed: provider.name,
85
135
  };
86
136
  }
87
137
  const parsed = parseProposal(proposed);
88
138
  if (!parsed.sql) {
139
+ const text = parsed.text || 'No answer (the model declined to propose SQL).';
89
140
  return {
90
141
  kind: 'no_answer',
91
- text: parsed.text || 'No answer (the model declined to propose SQL).',
142
+ sourceTier: 'no_answer',
143
+ certification: 'analyst_review_required',
144
+ reviewStatus: 'none',
145
+ confidence: 0.1,
146
+ text,
147
+ answer: text,
92
148
  citations: [],
149
+ memoryContext: input.memoryContext,
150
+ evidence: buildNoAnswerEvidence({
151
+ question,
152
+ reason: text,
153
+ artifactHits,
154
+ semanticHits,
155
+ manifestHits,
156
+ considered,
157
+ memoryContext: input.memoryContext ?? [],
158
+ }),
93
159
  considered,
94
160
  providerUsed: provider.name,
95
161
  };
96
162
  }
97
- return {
98
- kind: 'uncertified',
99
- text: parsed.text,
100
- proposedSql: parsed.sql,
101
- suggestedViz: parsed.viz ?? 'table',
102
- citations: contextNodes.slice(0, 4).map((n) => ({
163
+ const generatedCitations = [
164
+ ...contextNodes.slice(0, 4).map((n) => ({
103
165
  nodeId: n.nodeId,
104
166
  kind: n.kind,
105
167
  name: n.name,
106
168
  gitSha: n.gitSha,
169
+ sourceTier: activeTier,
170
+ provenance: n.provenance,
107
171
  })),
172
+ ...(input.memoryContext ?? []).slice(0, 2).map((m) => ({
173
+ nodeId: m.id,
174
+ kind: 'memory',
175
+ name: m.title,
176
+ sourceTier: 'memory',
177
+ provenance: m.source,
178
+ })),
179
+ ];
180
+ return {
181
+ kind: 'uncertified',
182
+ sourceTier: activeTier,
183
+ certification: 'ai_generated',
184
+ reviewStatus: 'analyst_review_required',
185
+ confidence: activeTier === 'semantic_layer' ? 0.72 : 0.55,
186
+ text: parsed.text,
187
+ answer: parsed.text,
188
+ proposedSql: parsed.sql,
189
+ sql: parsed.sql,
190
+ suggestedViz: parsed.viz ?? 'table',
191
+ citations: generatedCitations,
192
+ memoryContext: input.memoryContext,
193
+ evidence: buildGeneratedEvidence({
194
+ question,
195
+ activeTier,
196
+ contextNodes,
197
+ semanticHits,
198
+ manifestHits,
199
+ considered,
200
+ citations: generatedCitations,
201
+ memoryContext: input.memoryContext ?? [],
202
+ }),
108
203
  considered,
109
204
  providerUsed: provider.name,
110
205
  };
@@ -120,18 +215,27 @@ Rules:
120
215
  line, bar, area, pie, single_value, table, pivot, kpi.
121
216
  5. NEVER fabricate column names that are not present in the supplied schema context.
122
217
  6. If the schema is insufficient to answer, say so explicitly and ask a clarifying question.`;
123
- function renderContextPrompt(blocks, others) {
218
+ function renderContextPrompt(blocks, others, activeTier, memoryContext, extraContext) {
124
219
  const blockSection = blocks.length > 0
125
220
  ? `## Certified blocks the user already has\n\n${blocks
126
221
  .map((b) => `- \`${b.nodeId}\` (${b.domain ?? 'unscoped'}): ${b.description ?? b.llmContext ?? '(no description)'}`)
127
222
  .join('\n')}`
128
223
  : '## Certified blocks: (none matched)';
129
224
  const otherSection = others.length > 0
130
- ? `\n\n## Related semantic + warehouse context\n\n${others
131
- .map((n) => `- ${n.kind} \`${n.name}\`${n.domain ? ` (domain: ${n.domain})` : ''}${n.description ? ` — ${n.description}` : ''}`)
225
+ ? `\n\n## Related ${activeTier === 'semantic_layer' ? 'semantic layer' : 'dbt manifest'} context\n\n${others
226
+ .map((n) => `- ${n.kind} \`${n.name}\`${n.domain ? ` (domain: ${n.domain})` : ''}${n.description ? ` — ${n.description}` : ''}${n.llmContext ? `\n ${n.llmContext.replace(/\n/g, '\n ')}` : ''}`)
132
227
  .join('\n')}`
133
228
  : '';
134
- return `${blockSection}${otherSection}`;
229
+ const memorySection = memoryContext.length > 0
230
+ ? `\n\n## Advisory local memory\n\nMemory can clarify business language but MUST NOT override certified artifacts, semantic metrics, or dbt metadata.\n${memoryContext
231
+ .slice(0, 6)
232
+ .map((m) => `- ${m.scope}${m.scopeId ? `:${m.scopeId}` : ''} \`${m.title}\` (${m.source}, confidence ${m.confidence}): ${m.content}`)
233
+ .join('\n')}`
234
+ : '';
235
+ const extraSection = extraContext?.trim()
236
+ ? `\n\n## Current notebook/app context\n\nThis context may help interpret the user's request, but it MUST NOT override certified artifacts, semantic metrics, dbt metadata, or generated SQL validation.\n\n${extraContext.trim()}`
237
+ : '';
238
+ return `${blockSection}${otherSection}${memorySection}${extraSection}`;
135
239
  }
136
240
  /**
137
241
  * Public for tests. Pulls the first ```sql block and an optional Viz: line
@@ -149,7 +253,7 @@ export function parseProposal(raw) {
149
253
  .trim();
150
254
  return { text, sql, viz };
151
255
  }
152
- function pickCertifiedBlock(blockHits, blockHints, kg) {
256
+ function pickCertifiedArtifact(artifactHits, blockHints, kg) {
153
257
  // Hint match wins immediately: the active Skill's vocabulary points the
154
258
  // user at a specific block. We still validate it's certified.
155
259
  for (const hint of blockHints) {
@@ -160,28 +264,335 @@ function pickCertifiedBlock(blockHits, blockHints, kg) {
160
264
  }
161
265
  // Otherwise: top FTS5 hit must be certified, exceed the score threshold,
162
266
  // and not have a hard negative ratio in feedback.
163
- for (const hit of blockHits) {
267
+ for (const hit of artifactHits) {
164
268
  if (hit.score < CERTIFIED_HIT_THRESHOLD)
165
269
  break;
166
- if (hit.node.status !== 'certified')
167
- continue;
168
- const fb = kg.blockFeedbackScore(hit.node.nodeId);
169
- const total = fb.up + fb.down;
170
- if (total > 0 && fb.down / total > HARD_NEGATIVE_RATIO)
270
+ if (hit.node.kind === 'block') {
271
+ if (hit.node.status !== 'certified')
272
+ continue;
273
+ const fb = kg.blockFeedbackScore(hit.node.nodeId);
274
+ const total = fb.up + fb.down;
275
+ if (total > 0 && fb.down / total > HARD_NEGATIVE_RATIO)
276
+ continue;
277
+ }
278
+ else if (hit.node.status !== 'certified' && hit.node.certification !== 'certified') {
171
279
  continue;
280
+ }
172
281
  return hit;
173
282
  }
174
283
  return null;
175
284
  }
176
- function composeCertifiedAnswer(block, question, result, executionError) {
177
- const desc = block.description ?? block.llmContext ?? '';
178
- const tag = block.gitSha ? ` · ${block.gitSha.slice(0, 8)}` : '';
285
+ function composeCertifiedAnswer(artifact, question, result, executionError) {
286
+ const desc = artifact.description ?? artifact.llmContext ?? '';
287
+ const tag = artifact.gitSha ? ` · ${artifact.gitSha.slice(0, 8)}` : '';
179
288
  const resultText = result
180
289
  ? `Returned ${result.rowCount} row${result.rowCount === 1 ? '' : 's'}.`
181
290
  : executionError
182
291
  ? `The certified block matched, but governed execution failed: ${executionError}`
183
- : 'Governed execution was not requested by this host.';
184
- return `Answered by certified block **${block.name}**${tag}.\n\n${desc ? `${desc}\n\n${resultText}` : resultText}`
292
+ : artifact.kind === 'block'
293
+ ? 'Governed execution was not requested by this host.'
294
+ : `Matched certified ${artifact.kind.replace('_', ' ')} context.`;
295
+ return `Answered by certified ${artifact.kind.replace('_', ' ')} **${artifact.name}**${tag}.\n\n${desc ? `${desc}\n\n${resultText}` : resultText}`
185
296
  + `\n\n_Question:_ ${question}`;
186
297
  }
298
+ function mergeHits(...groups) {
299
+ const byId = new Map();
300
+ for (const group of groups) {
301
+ for (const hit of group) {
302
+ const existing = byId.get(hit.node.nodeId);
303
+ if (!existing || hit.score > existing.score)
304
+ byId.set(hit.node.nodeId, hit);
305
+ }
306
+ }
307
+ return Array.from(byId.values()).sort((a, b) => b.score - a.score);
308
+ }
309
+ function buildCertifiedEvidence(input) {
310
+ const semanticObjects = uniqueAssets(input.semanticHits.map((hit) => assetFromNode(hit.node))).slice(0, 6);
311
+ const sourceTables = uniqueAssets(input.manifestHits.map((hit) => assetFromNode(hit.node))).slice(0, 6);
312
+ const relatedConsumers = input.considered
313
+ .map((hit) => hit.node)
314
+ .filter((node) => node.nodeId !== input.artifact.nodeId && ARTIFACT_KINDS.includes(node.kind))
315
+ .slice(0, 4);
316
+ return {
317
+ route: [
318
+ {
319
+ tool: 'search_certified_artifacts',
320
+ status: 'selected',
321
+ label: `Selected certified ${input.artifact.kind.replace('_', ' ')}`,
322
+ detail: input.artifact.name,
323
+ },
324
+ {
325
+ tool: 'query_certified_artifact',
326
+ status: input.executionError ? 'failed' : input.result ? 'selected' : input.executorWasAvailable ? 'checked' : 'skipped',
327
+ label: input.executionError
328
+ ? 'Certified execution failed'
329
+ : input.result
330
+ ? 'Executed certified block'
331
+ : input.artifact.kind === 'block'
332
+ ? 'Certified block was not executed by this host'
333
+ : 'Certified navigation artifact selected',
334
+ detail: input.executionError ?? (input.result ? `${input.result.rowCount} rows` : undefined),
335
+ },
336
+ {
337
+ tool: 'search_semantic_layer',
338
+ status: input.semanticHits.length > 0 ? 'checked' : 'skipped',
339
+ label: input.semanticHits.length > 0 ? 'Semantic context attached' : 'No semantic context needed',
340
+ },
341
+ {
342
+ tool: 'search_dbt_manifest',
343
+ status: input.manifestHits.length > 0 ? 'checked' : 'skipped',
344
+ label: input.manifestHits.length > 0 ? 'dbt/source context attached' : 'No dbt fallback needed',
345
+ },
346
+ ],
347
+ lineage: [
348
+ questionLineageNode(input.question),
349
+ { ...assetFromNode(input.artifact), role: 'selected_asset' },
350
+ ...semanticObjects.map((asset) => ({ ...asset, role: 'semantic_object' })),
351
+ ...sourceTables.map((asset) => ({ ...asset, role: 'source_table' })),
352
+ ...relatedConsumers.map((node) => ({ ...assetFromNode(node), role: 'consumer' })),
353
+ ],
354
+ businessContext: [
355
+ ...businessContextForNode(input.artifact),
356
+ ...input.memoryContext.slice(0, 3).map((memory) => ({
357
+ label: 'Memory advisory',
358
+ value: `${memory.title}: ${memory.content}`,
359
+ source: memory.source,
360
+ })),
361
+ ],
362
+ outcome: outcomeForNode(input.artifact),
363
+ selectedAssets: [assetFromNode(input.artifact)],
364
+ sourceTables,
365
+ semanticObjects,
366
+ validation: {
367
+ status: input.executionError ? 'failed' : 'passed',
368
+ message: input.executionError
369
+ ? 'The certified artifact matched, but execution returned an error.'
370
+ : 'Certified artifact routing passed; no generated SQL was promoted.',
371
+ },
372
+ execution: executionEvidence(input.artifact, input.result, input.executionError, input.executorWasAvailable),
373
+ citations: input.citations,
374
+ };
375
+ }
376
+ function buildGeneratedEvidence(input) {
377
+ const selectedNodes = input.contextNodes.slice(0, 4);
378
+ const semanticObjects = uniqueAssets([...input.contextNodes, ...input.semanticHits.map((hit) => hit.node)]
379
+ .filter((node) => SEMANTIC_KINDS.includes(node.kind))
380
+ .map(assetFromNode)).slice(0, 6);
381
+ const sourceTables = uniqueAssets([...input.contextNodes, ...input.manifestHits.map((hit) => hit.node)]
382
+ .filter((node) => MANIFEST_KINDS.includes(node.kind))
383
+ .map(assetFromNode)).slice(0, 6);
384
+ const selectedAssets = uniqueAssets(selectedNodes.map(assetFromNode)).slice(0, 4);
385
+ const selectedSemantic = input.activeTier === 'semantic_layer' && semanticObjects.length > 0;
386
+ return {
387
+ route: [
388
+ {
389
+ tool: 'search_certified_artifacts',
390
+ status: 'checked',
391
+ label: 'No certified artifact was strong enough for this question',
392
+ },
393
+ {
394
+ tool: 'search_semantic_layer',
395
+ status: selectedSemantic ? 'selected' : input.semanticHits.length > 0 ? 'checked' : 'skipped',
396
+ label: selectedSemantic ? 'Selected semantic context' : input.semanticHits.length > 0 ? 'Semantic context considered' : 'No semantic match',
397
+ },
398
+ {
399
+ tool: input.activeTier === 'semantic_layer' ? 'compose_semantic_query' : 'search_dbt_manifest',
400
+ status: 'selected',
401
+ label: input.activeTier === 'semantic_layer' ? 'Composed SQL from semantic context' : 'Composed SQL from dbt manifest context',
402
+ },
403
+ {
404
+ tool: 'validate_sql',
405
+ status: 'checked',
406
+ label: 'SQL is generated and requires host validation before certification',
407
+ },
408
+ {
409
+ tool: 'create_draft_block',
410
+ status: 'skipped',
411
+ label: 'Draft block can be created for analyst review',
412
+ },
413
+ ],
414
+ lineage: [
415
+ questionLineageNode(input.question),
416
+ ...selectedAssets.map((asset) => ({ ...asset, role: selectedSemantic ? 'semantic_object' : 'source_table' })),
417
+ ...sourceTables.map((asset) => ({ ...asset, role: 'source_table' })),
418
+ ...semanticObjects
419
+ .filter((asset) => !selectedAssets.some((selected) => selected.nodeId === asset.nodeId))
420
+ .map((asset) => ({ ...asset, role: 'semantic_object' })),
421
+ ],
422
+ businessContext: [
423
+ ...selectedNodes.flatMap(businessContextForNode),
424
+ ...input.memoryContext.slice(0, 3).map((memory) => ({
425
+ label: 'Memory advisory',
426
+ value: `${memory.title}: ${memory.content}`,
427
+ source: memory.source,
428
+ })),
429
+ ],
430
+ outcome: outcomeForNode(selectedNodes[0]),
431
+ selectedAssets,
432
+ sourceTables,
433
+ semanticObjects,
434
+ validation: {
435
+ status: 'warning',
436
+ message: 'Generated SQL is not certified. It should be validated, reviewed, and promoted only after analyst approval.',
437
+ },
438
+ execution: {
439
+ status: 'not_requested',
440
+ message: 'Generated SQL was returned for review; execution is handled by the host after validation.',
441
+ },
442
+ citations: input.citations,
443
+ };
444
+ }
445
+ function buildNoAnswerEvidence(input) {
446
+ return {
447
+ route: [
448
+ {
449
+ tool: 'search_certified_artifacts',
450
+ status: input.artifactHits.length > 0 ? 'checked' : 'skipped',
451
+ label: input.artifactHits.length > 0 ? 'Certified artifacts considered but not selected' : 'No certified artifact match',
452
+ },
453
+ {
454
+ tool: 'search_semantic_layer',
455
+ status: input.semanticHits.length > 0 ? 'checked' : 'skipped',
456
+ label: input.semanticHits.length > 0 ? 'Semantic context considered' : 'No semantic match',
457
+ },
458
+ {
459
+ tool: 'search_dbt_manifest',
460
+ status: input.manifestHits.length > 0 ? 'checked' : 'skipped',
461
+ label: input.manifestHits.length > 0 ? 'dbt context considered' : 'No dbt match',
462
+ },
463
+ {
464
+ tool: 'validate_sql',
465
+ status: 'failed',
466
+ label: input.reason,
467
+ },
468
+ ],
469
+ lineage: [
470
+ questionLineageNode(input.question),
471
+ ...input.considered.slice(0, 6).map((hit) => ({ ...assetFromNode(hit.node), role: 'selected_asset' })),
472
+ ],
473
+ businessContext: input.memoryContext.slice(0, 3).map((memory) => ({
474
+ label: 'Memory advisory',
475
+ value: `${memory.title}: ${memory.content}`,
476
+ source: memory.source,
477
+ })),
478
+ selectedAssets: [],
479
+ sourceTables: uniqueAssets(input.manifestHits.map((hit) => assetFromNode(hit.node))).slice(0, 6),
480
+ semanticObjects: uniqueAssets(input.semanticHits.map((hit) => assetFromNode(hit.node))).slice(0, 6),
481
+ validation: {
482
+ status: 'failed',
483
+ message: input.reason,
484
+ },
485
+ execution: {
486
+ status: 'not_applicable',
487
+ message: 'No SQL or certified block was executed.',
488
+ },
489
+ citations: [],
490
+ };
491
+ }
492
+ function questionLineageNode(question) {
493
+ return {
494
+ nodeId: 'question',
495
+ kind: 'question',
496
+ name: question,
497
+ role: 'question',
498
+ };
499
+ }
500
+ function assetFromNode(node) {
501
+ return {
502
+ nodeId: node.nodeId,
503
+ kind: node.kind,
504
+ name: node.name,
505
+ description: node.description,
506
+ sourceTier: node.sourceTier === 'business_context' ? 'project' : node.sourceTier,
507
+ certification: certificationForNode(node),
508
+ provenance: node.provenance,
509
+ sourcePath: node.sourcePath,
510
+ owner: node.owner,
511
+ domain: node.domain,
512
+ status: node.status,
513
+ };
514
+ }
515
+ function certificationForNode(node) {
516
+ if (node.status === 'certified' || node.certification === 'certified')
517
+ return 'certified';
518
+ if (node.certification === 'analyst_review_required')
519
+ return 'analyst_review_required';
520
+ if (node.certification === 'ai_generated' || node.certification === 'uncertified')
521
+ return 'ai_generated';
522
+ return undefined;
523
+ }
524
+ function businessContextForNode(node) {
525
+ const items = [];
526
+ if (node.description)
527
+ items.push({ label: 'Definition', value: node.description, source: node.provenance });
528
+ if (node.llmContext)
529
+ items.push({ label: 'Business rule', value: node.llmContext, source: node.provenance });
530
+ if (node.businessOutcome)
531
+ items.push({ label: 'Business outcome', value: node.businessOutcome, source: node.provenance });
532
+ if (node.decisionUse)
533
+ items.push({ label: 'Decision use', value: node.decisionUse, source: node.provenance });
534
+ if (node.owner)
535
+ items.push({ label: 'Owner', value: node.owner, source: node.provenance });
536
+ if (node.businessOwner && node.businessOwner !== node.owner)
537
+ items.push({ label: 'Business owner', value: node.businessOwner, source: node.provenance });
538
+ if (node.domain)
539
+ items.push({ label: 'Domain', value: node.domain, source: node.provenance });
540
+ if (node.status)
541
+ items.push({ label: 'Certification status', value: node.status, source: node.provenance });
542
+ if (node.reviewCadence)
543
+ items.push({ label: 'Review cadence', value: node.reviewCadence, source: node.provenance });
544
+ if (node.freshness)
545
+ items.push({ label: 'Freshness', value: node.freshness, source: node.provenance });
546
+ for (const rule of node.businessRules ?? [])
547
+ items.push({ label: 'Business rule', value: rule, source: node.provenance });
548
+ for (const caveat of node.caveats ?? [])
549
+ items.push({ label: 'Caveat', value: caveat, source: node.provenance });
550
+ return items;
551
+ }
552
+ function outcomeForNode(node) {
553
+ if (!node)
554
+ return undefined;
555
+ const outcome = {
556
+ name: node.businessOutcome,
557
+ owner: node.businessOwner ?? node.owner,
558
+ decisionUse: node.decisionUse,
559
+ reviewCadence: node.reviewCadence,
560
+ caveats: node.caveats,
561
+ };
562
+ return Object.values(outcome).some((value) => Array.isArray(value) ? value.length > 0 : Boolean(value)) ? outcome : undefined;
563
+ }
564
+ function executionEvidence(artifact, result, executionError, executorWasAvailable) {
565
+ if (result) {
566
+ return {
567
+ status: 'executed',
568
+ message: `Executed certified block ${artifact.name}.`,
569
+ rowCount: result.rowCount,
570
+ executionTime: result.executionTime,
571
+ };
572
+ }
573
+ if (executionError) {
574
+ return {
575
+ status: 'failed',
576
+ message: executionError,
577
+ };
578
+ }
579
+ if (artifact.kind === 'block' && !executorWasAvailable) {
580
+ return {
581
+ status: 'not_requested',
582
+ message: 'The host selected the certified block but did not request governed execution.',
583
+ };
584
+ }
585
+ return {
586
+ status: 'not_applicable',
587
+ message: `Selected certified ${artifact.kind.replace('_', ' ')} context.`,
588
+ };
589
+ }
590
+ function uniqueAssets(assets) {
591
+ const byId = new Map();
592
+ for (const asset of assets) {
593
+ if (!byId.has(asset.nodeId))
594
+ byId.set(asset.nodeId, asset);
595
+ }
596
+ return Array.from(byId.values());
597
+ }
187
598
  //# sourceMappingURL=answer-loop.js.map