/**
* Visualization agent — runtime entry point.
*
* Fully deterministic in the foundational rule set (per Std 6 cost-
* appropriate execution): for each recommendation the agent finds
* applicable visualization rules, runs disclosure-policy enforcement,
* then constructs the output specification by reading rule.action
* fields and substituting recommendation values into narrative-wrapper
* templates. No LLM call.
*
* Post-construction cross-checks (Std 4 + Std 7):
* - every dataBinding field references a real recommendation field
* (no fabricated bindings)
* - narrative-wrapper headlines and supporting sentences successfully
* substituted every token in the rule's template
* - disclosure-policy result is "cleared" before the visualization
* ships; otherwise the entry is dropped and an escalation raised
*/
import {
type AgentResult,
type HITLEscalation,
LOW_CONFIDENCE_THRESHOLD,
makeConfidence,
} from '../../../standards.js';
import type {
ExecutionContext,
FailureObject,
Handoff,
JobRequest,
Lineage,
UnresolvedIssue,
} from '../../../types.js';
import { nowIso } from '../../../types.js';
import {
findRules,
resolvePrecedence,
type Rule,
} from '../../../decision/library.js';
import {
AGENT_NAME,
AGENT_VERSION,
visualizationContract,
} from './matrix.js';
import {
type VisualizationInput,
type VisualizationOutput,
type Visualization,
type VisualizationSpec,
type DisclosurePolicyResult,
visualizationInputSchema,
} from './schema.js';
import { TOOL_COUNT, TOOL_NAMES } from './llm.js';
export { visualizationContract } from './matrix.js';
export type { VisualizationOutput } from './schema.js';
export interface VisualizationSideContext {
readonly jobRequest: JobRequest;
readonly upstreamLineage: Lineage;
}
function trace(ctx: ExecutionContext, standard: number, step: string, detail: string): void {
ctx.trace.push({ agent: AGENT_NAME, standard, step, detail, at: nowIso() });
// eslint-disable-next-line no-console
console.log(` [${AGENT_NAME}][Std ${standard}] ${step} — ${detail}`);
}
function failure(
ctx: ExecutionContext,
category: FailureObject['category'],
reason: string,
context: Record<string, unknown>,
lineage: Lineage,
): FailureObject {
return {
agent: AGENT_NAME,
agentVersion: AGENT_VERSION,
category,
reason,
context,
lineage,
attempts: ctx.retries,
recursionDepth: ctx.recursionDepth,
occurredAt: nowIso(),
};
}
/* -------------------------------------------------------------- *
* Helpers: trigger generation, template substitution, disclosure
* -------------------------------------------------------------- */
function triggersFromRecommendation(r: {
suggestedActionCategory: string;
audienceTier: string;
severity: string;
}): string[] {
const out = new Set<string>();
out.add(r.suggestedActionCategory.toLowerCase());
out.add(r.audienceTier.toLowerCase());
out.add('peer_positioning');
out.add('peer-positioning');
if (r.severity === 'material' || r.severity === 'high_impact') out.add(r.severity);
return [...out];
}
interface SubstitutionContext {
readonly entity: string;
readonly suggestedActionCategory: string;
readonly statisticalPosition: string;
readonly audienceTier: string;
readonly language: string;
}
function positionPhrase(category: string): string {
const c = category.toLowerCase();
if (c.includes('outperform') || c.includes('above')) return 'above';
if (c.includes('underperform') || c.includes('below')) return 'below';
if (c.includes('in-line') || c.includes('in line') || c.includes('parity')) return 'in line with';
return 'relative to';
}
function fillTemplate(template: string, sub: SubstitutionContext): { text: string; missing: string[] } {
const missing: string[] = [];
const text = template.replace(/\{\{(\w+)\}\}/g, (_m, key: string) => {
const k = key.toLowerCase();
if (k === 'entity') return sub.entity || (missing.push(k), `{{${key}}}`);
if (k === 'position_phrase') return positionPhrase(sub.suggestedActionCategory);
if (k === 'position_summary' || k === 'statistical_context_sentence' || k === 'statistical_position') {
return sub.statisticalPosition || (missing.push(k), `{{${key}}}`);
}
if (k === 'suggested_action' || k === 'suggested_action_category') return sub.suggestedActionCategory;
if (k === 'audience_tier') return sub.audienceTier;
if (k === 'language') return sub.language;
missing.push(k);
return `{{${key}}}`;
});
return { text, missing };
}
function applyDisclosurePolicy(rule: Rule, audienceTier: string, visualizationId: string): DisclosurePolicyResult {
const policy = rule.disclosure_policy;
if (!policy) {
return {
visualizationId,
audienceTier,
decision: 'no-policy',
detail: 'rule did not declare a disclosure policy',
};
}
const allowed = (policy.default_allowed_tiers ?? []).map(t => t.toLowerCase());
const restricted = (policy.default_restricted_tiers ?? []).map(t => t.toLowerCase());
const at = audienceTier.toLowerCase();
if (restricted.includes(at)) {
return {
visualizationId,
audienceTier,
decision: 'restricted',
policyApplied: rule.rule_id,
detail: `audience tier "${audienceTier}" is on default_restricted_tiers for rule ${rule.rule_id}`,
};
}
if (allowed.length === 0 || allowed.includes(at)) {
return {
visualizationId,
audienceTier,
decision: 'cleared',
policyApplied: rule.rule_id,
detail: allowed.length === 0
? `no explicit allow-list; rule has audience_clearance_check=${String(policy.audience_clearance_check)}`
: `audience tier "${audienceTier}" is on default_allowed_tiers`,
};
}
return {
visualizationId,
audienceTier,
decision: 'restricted',
policyApplied: rule.rule_id,
detail: `audience tier "${audienceTier}" not in allow-list ${JSON.stringify(allowed)} and rule requires clearance`,
};
}
/* -------------------------------------------------------------- *
* Construct the visualization specification from a matched rule.
* Returns null + a reason string when the rule's action is not
* structured the way this agent expects (the LLM fallback would
* fire here in a richer build).
* -------------------------------------------------------------- */
function buildSpec(rule: Rule, sub: SubstitutionContext): {
spec: VisualizationSpec | null;
missingTokens: string[];
flags: string[];
} {
const action = rule.action;
const chartType = typeof action.chart_type === 'string' ? action.chart_type : '';
const dataBinding = (action.data_binding && typeof action.data_binding === 'object'
? action.data_binding
: {}) as Record<string, unknown>;
const colorPalette = typeof action.color_palette === 'string' ? action.color_palette : undefined;
const format = (action.format && typeof action.format === 'object'
? action.format
: { primary: 'unspecified' }) as { primary?: unknown; secondary?: unknown; fallback?: unknown };
const narrativeWrapper = (action.narrative_wrapper && typeof action.narrative_wrapper === 'object'
? action.narrative_wrapper
: {}) as Record<string, unknown>;
if (!chartType) {
return { spec: null, missingTokens: ['action.chart_type'], flags: ['no-chart-type-in-rule'] };
}
const headlineTemplate = typeof narrativeWrapper.headline_template === 'string'
? narrativeWrapper.headline_template
: typeof narrativeWrapper.headline === 'string'
? narrativeWrapper.headline
: '{{entity}} {{position_phrase}} peer mean ({{position_summary}})';
const supportingTemplate = typeof narrativeWrapper.supporting_template === 'string'
? narrativeWrapper.supporting_template
: typeof narrativeWrapper.supporting === 'string'
? narrativeWrapper.supporting
: '{{statistical_context_sentence}}';
const headline = fillTemplate(headlineTemplate, sub);
const supporting = fillTemplate(supportingTemplate, sub);
const constraints = Array.isArray(narrativeWrapper.constraints)
? narrativeWrapper.constraints.map(String)
: [];
const annotations: string[] = [];
if (Array.isArray(dataBinding.annotations)) {
for (const a of dataBinding.annotations) annotations.push(String(a));
}
const spec: VisualizationSpec = {
chartType,
dataBinding,
colorPalette,
format: {
primary: typeof format.primary === 'string' ? format.primary : 'unspecified',
secondary: typeof format.secondary === 'string' ? format.secondary : undefined,
fallback: typeof format.fallback === 'string' ? format.fallback : undefined,
},
narrativeWrapper: {
headline: headline.text,
supporting: supporting.text,
constraints,
},
annotations,
};
const missingTokens = Array.from(new Set([...headline.missing, ...supporting.missing]));
const flags: string[] = [];
if (missingTokens.length > 0) flags.push(`unfilled-template-tokens:${missingTokens.join(',')}`);
return { spec, missingTokens, flags };
}
/* -------------------------------------------------------------- *
* runVisualization — agent entry point
* -------------------------------------------------------------- */
export async function runVisualization(
rawInput: unknown,
side: VisualizationSideContext,
ctx: ExecutionContext,
): Promise<AgentResult<VisualizationOutput>> {
/* Step 1 (Std 2): receive-recommendations. */
const parsed = visualizationInputSchema.safeParse(rawInput);
if (!parsed.success) {
return {
ok: false,
escalations: [],
failure: failure(
ctx,
'invalid-input',
'Visualization input failed schema validation.',
{ issues: parsed.error.issues },
side.upstreamLineage,
),
};
}
const recsPayload: VisualizationInput = parsed.data;
trace(ctx, 2, visualizationContract.runbook[0]!.name,
`received ${recsPayload.recommendations.length} recommendation(s); ${recsPayload.ruleGapsEscalated.length} upstream rule-gap(s); ${recsPayload.appliedRules.length} interpretation rule(s) used`);
if (recsPayload.recommendations.length === 0) {
/* Pillar 3 spec §Std 12: an empty input is a structural failure
* (visualization needs something to render). */
return {
ok: false,
escalations: [],
failure: failure(
ctx,
'visualization-rule-gap',
'No recommendations supplied — nothing to visualize.',
{ input: recsPayload },
side.upstreamLineage,
),
};
}
const unresolved: UnresolvedIssue[] = [];
const escalations: HITLEscalation[] = [];
const visualizations: Visualization[] = [];
const visualizationGapsEscalated: VisualizationOutput['visualizationGapsEscalated'] = [];
const disclosurePolicyResults: DisclosurePolicyResult[] = [];
const appliedRules = new Set<string>();
trace(ctx, 5, 'tool-inventory',
`Pillar 3 rule tools available to this agent: ${TOOL_COUNT} — [${TOOL_NAMES.join(', ')}] (not used in deterministic path)`);
/* Step 2 (Std 5/6 — deterministic): find-rules per recommendation. */
for (const rec of recsPayload.recommendations) {
const triggers = triggersFromRecommendation(rec);
/* Multi-pass trigger search — same approach as Agent 1 — single
* broad term per pass, dedup by rule_id, then resolve precedence. */
const unique = new Map<string, Rule>();
for (const t of triggers) {
const m = await findRules({ type: 'visualization', agent: 'visualization', triggers: [t] });
for (const r of m) unique.set(r.rule_id, r);
}
const matched = resolvePrecedence([...unique.values()]);
if (matched.length === 0) {
visualizationGapsEscalated.push({
recommendationReference: rec.recommendationId,
reason: 'no-rule-matched',
detail: `no visualization rule matched triggers [${triggers.join(', ')}] for recommendation "${rec.recommendationId}"`,
triedTriggers: triggers,
});
unresolved.push({
category: 'rule-gap',
detail: `no visualization rule for recommendation "${rec.recommendationId}" (action=${rec.suggestedActionCategory}, audience=${rec.audienceTier})`,
blocking: false,
});
escalations.push({
agent: AGENT_NAME,
reason: 'rule-gap',
failureContext: `no visualization rule matched for recommendation "${rec.recommendationId}"`,
lineage: side.upstreamLineage,
validation: {
status: 'review',
confidence: makeConfidence(0, 'no rule available'),
checks: [{ name: 'rule-available', passed: false }],
},
recommendedReviewer: 'domain-expert',
raisedAt: nowIso(),
});
continue;
}
const rule = matched[0]!;
appliedRules.add(rule.rule_id);
const visualizationId = `viz-${rec.recommendationId}`;
/* Step 4 (Std 4 + Std 7): disclosure-policy check. */
const disclosure = applyDisclosurePolicy(rule, rec.audienceTier, visualizationId);
disclosurePolicyResults.push(disclosure);
if (disclosure.decision === 'restricted') {
visualizationGapsEscalated.push({
recommendationReference: rec.recommendationId,
reason: 'disclosure-blocked',
detail: disclosure.detail,
triedTriggers: triggers,
});
unresolved.push({
category: 'disclosure-policy-concern',
detail: `visualization for "${rec.recommendationId}" blocked by rule ${rule.rule_id}: ${disclosure.detail}`,
blocking: true,
});
escalations.push({
agent: AGENT_NAME,
reason: 'disclosure-policy-concern',
failureContext: `disclosure policy on rule ${rule.rule_id} restricts audience tier "${rec.audienceTier}" — needs compliance review`,
lineage: side.upstreamLineage,
validation: {
status: 'review',
confidence: makeConfidence(0, 'disclosure restricted'),
checks: [{ name: 'audience-cleared', passed: false }],
},
recommendedReviewer: 'compliance-reviewer',
raisedAt: nowIso(),
});
continue;
}
/* Step 5 (Std 3): apply-rule deterministically. */
const sub: SubstitutionContext = {
entity: rec.entityIdentifier ?? '',
suggestedActionCategory: rec.suggestedActionCategory,
statisticalPosition: rec.statisticalPosition ?? rec.language,
audienceTier: rec.audienceTier,
language: rec.language,
};
const { spec, missingTokens, flags: bindFlags } = buildSpec(rule, sub);
if (!spec) {
visualizationGapsEscalated.push({
recommendationReference: rec.recommendationId,
reason: 'template-binding-failed',
detail: `rule ${rule.rule_id} did not declare action.chart_type — visualization cannot be constructed`,
triedTriggers: triggers,
});
unresolved.push({
category: 'rule-gap',
detail: `rule ${rule.rule_id} action incomplete`,
blocking: false,
});
escalations.push({
agent: AGENT_NAME,
reason: 'rule-gap',
failureContext: `rule ${rule.rule_id} action.chart_type missing`,
lineage: side.upstreamLineage,
validation: {
status: 'review',
confidence: makeConfidence(0, 'rule action incomplete'),
checks: [{ name: 'rule-action-complete', passed: false }],
},
recommendedReviewer: 'domain-expert',
raisedAt: nowIso(),
});
continue;
}
/* Step 6 (Std 4 + Std 7): validate-fidelity. */
const flags = [...bindFlags];
if (missingTokens.length > 0) {
unresolved.push({
category: 'fidelity-violation',
detail: `visualization "${visualizationId}" has unfilled template tokens: ${missingTokens.join(', ')}`,
blocking: false,
});
}
/* Carry through material-impact severity from the recommendation
* (Pillar 3 spec §Std 8) so the Delivery agent has the signal. */
if (rec.severity === 'material' || rec.severity === 'high_impact') {
flags.push(`carry-through-severity:${rec.severity}`);
}
/* Step 7 (Std 7): score-confidence. Inherit + apply declared
* adjustments from the rule's confidence_framework. */
let confidence = rec.confidence;
if (missingTokens.length > 0) confidence = Math.max(0, confidence - 0.10);
visualizations.push({
visualizationId,
recommendationReference: rec.recommendationId,
visualizationRuleApplied: rule.rule_id,
audienceTier: rec.audienceTier,
outputSpecification: spec,
disclosurePolicy: disclosure,
reasoningLineage: [...rec.reasoningLineage, rule.sourceFile],
confidence,
flags,
});
}
trace(ctx, 5, visualizationContract.runbook[1]!.name,
`find-rules: ${visualizations.length} visualization(s) constructed, ${visualizationGapsEscalated.length} gap(s) escalated`);
trace(ctx, 4, visualizationContract.runbook[5]!.name,
`validate-fidelity: ${visualizations.length} spec(s) ok; ${visualizations.filter(v => v.flags.some(f => f.startsWith('unfilled-template-tokens'))).length} with template warnings`);
/* Aggregate confidence. */
const avgConf = visualizations.length === 0
? 0
: visualizations.reduce((s, v) => s + v.confidence, 0) / visualizations.length;
const blocking = unresolved.filter(u => u.blocking).length;
const confidence = makeConfidence(
Math.max(0, avgConf - 0.05 * Math.min(visualizationGapsEscalated.length, 5)),
`avg per-visualization confidence ${avgConf.toFixed(2)} with ${visualizationGapsEscalated.length} gap(s)`,
);
trace(ctx, 7, visualizationContract.runbook[6]!.name,
`validation: ${visualizations.length} visualization(s) avgConf=${avgConf.toFixed(2)}`);
const lineage: Lineage = {
sourceUrl: side.upstreamLineage.sourceUrl,
capturedAt: nowIso(),
effectiveAs: side.upstreamLineage.effectiveAs,
agentVersion: AGENT_VERSION,
upstream: Array.from(new Set([
...side.upstreamLineage.upstream,
...visualizations.flatMap(v => v.reasoningLineage),
])),
};
const validationStatus =
visualizations.length === 0 ? 'review'
: blocking > 0 ? 'review'
: confidence.value < LOW_CONFIDENCE_THRESHOLD ? 'flagged'
: 'passed';
const output: VisualizationOutput = {
visualizations,
visualizationGapsEscalated,
disclosurePolicyResults,
appliedRules: [...appliedRules].sort(),
notes: visualizations.length === recsPayload.recommendations.length
? ['all recommendations produced a visualization']
: [`${visualizations.length}/${recsPayload.recommendations.length} recommendations produced a visualization`],
};
const handoff: Handoff<VisualizationOutput> = {
fromAgent: AGENT_NAME,
fromAgentVersion: AGENT_VERSION,
toAgent: 'decision.delivery-distribution',
payload: output,
metadata: {
analysisId: ctx.analysisId,
capabilities: visualizationContract.capabilities,
candidatesConsidered: recsPayload.recommendations.length,
gapsEscalated: visualizationGapsEscalated.length,
disclosureClearedCount: disclosurePolicyResults.filter(d => d.decision === 'cleared').length,
disclosureRestrictedCount: disclosurePolicyResults.filter(d => d.decision === 'restricted').length,
llmInvocations: 0,
},
confidence,
validation: {
status: validationStatus,
checks: [
{ name: 'at-least-one-visualization', passed: visualizations.length > 0, detail: `${visualizations.length}` },
{ name: 'every-visualization-cites-rule', passed: visualizations.every(v => !!v.visualizationRuleApplied) },
{ name: 'every-visualization-cleared', passed: visualizations.every(v => v.disclosurePolicy.decision !== 'restricted') },
{ name: 'no-blocking-issues', passed: blocking === 0 },
],
},
unresolvedIssues: unresolved,
lineage,
timestamp: nowIso(),
};
trace(ctx, 11, visualizationContract.runbook[7]!.name,
`handoff → ${handoff.toAgent} (validation=${validationStatus} confidence=${confidence.tier})`);
return { ok: true, handoff, escalations };
}