diff --git a/packages/shared/src/automation/brief.test.ts b/packages/shared/src/automation/brief.test.ts index 1962382..06275e1 100644 --- a/packages/shared/src/automation/brief.test.ts +++ b/packages/shared/src/automation/brief.test.ts @@ -78,22 +78,41 @@ describe('buildBrief', () => { expect(brief.quiet).toEqual({ count: 0, message: 'No monitored securities.' }) }) - it('every item carries an explainable source', () => { + it('every item carries an explainable source, provenance references, and context version when applicable', () => { const brief = buildBrief( inputs({ - runs: [run('r1', 2, true)], - alerts: [alert('a1', 'AAPL.US', 'AAPL price alert')], - diffs: [diff('NVDA.US', true, 'strengthened')], - portfolio: [{ label: 'AAPL.US ยท 12.4% of portfolio', detail: 'Single-stock exposure 12.4%' }], - movers: [mover('TSLA.US', 7.2)], + diffs: [ + { + ...diff('NVDA.US', true, 'strengthened'), + changes: [ + { + category: 'valuation', + label: 'PE ratio', + before: 50, + after: 40, + direction: 'improved', + material: true, + evidence: ['capability:company.valuation run:run-1 fetchedAt:1700000000'], + }, + ], + }, + ], + runs: [run('run-auto', 1, true)], }), 1_700_000_000_000 ) - // portfolio(1) + watchlist mover(1) + watchlist diff(1) + thesis(1) + alert(1) + automation(1) - expect(brief.items.length).toBe(6) - for (const item of brief.items) { - expect(['Portfolio', 'Watchlist', 'Thesis', 'Alert', 'Automation']).toContain(item.source) - } + expect(brief.items.length).toBeGreaterThan(0) + const diffItem = brief.items.find((item) => item.id === 'diff-NVDA.US') + expect(diffItem?.provenanceRefs).toContain('capability:company.valuation run:run-1 fetchedAt:1700000000') + expect(diffItem?.contextVersion).toBe('diff-diff-NVDA.US') + + const thesisItem = brief.items.find((item) => item.id === 'thesis-NVDA.US') + expect(thesisItem?.provenanceRefs).toContain('capability:company.valuation run:run-1 fetchedAt:1700000000') + expect(thesisItem?.contextVersion).toBe('diff-diff-NVDA.US') + + const autoItem = brief.items.find((item) => item.id === 'automation-run-auto') + expect(autoItem?.provenanceRefs).toContain('automation-run:run-auto:material') + expect(autoItem?.contextVersion).toBe('run-run-auto') }) it('flags only material movers as watchlist changes', () => { diff --git a/packages/shared/src/automation/brief.ts b/packages/shared/src/automation/brief.ts index 01375be..d2d0fa9 100644 --- a/packages/shared/src/automation/brief.ts +++ b/packages/shared/src/automation/brief.ts @@ -26,6 +26,10 @@ export interface BriefItem { severity: BriefSeverity /** Structured explainability payload for "Why am I seeing this?". */ payload?: Record + /** Provenance and correlation anchors backing this brief conclusion (Issue #103). */ + provenanceRefs?: string[] + /** Versioned context / snapshot id this item was derived from. */ + contextVersion?: string } export interface BriefQuietState { @@ -151,15 +155,20 @@ function watchlistItems(inputs: BriefInputs): BriefItem[] { const diffItems: BriefItem[] = inputs.diffs .filter((diff) => diff.material) - .map((diff) => ({ - id: `watchlist-${diff.symbol}-diff`, - symbol: diff.symbol, - title: `${diff.symbol} material research change`, - message: diff.summary ?? `${diff.changes.length} material change(s) vs the previous report.`, - source: 'Watchlist', - severity: 'warning', - payload: { diffId: diff.id, changes: diff.changes.length }, - })) + .map((diff) => { + const provenanceRefs = diff.changes.flatMap((c) => c.evidence) + return { + id: `diff-${diff.symbol}`, + symbol: diff.symbol, + title: `${diff.symbol} research changed`, + message: diff.summary ?? 'Material research update.', + source: 'Watchlist', + severity: 'warning', + payload: { diffId: diff.id, changes: diff.changes.length }, + ...(provenanceRefs.length > 0 ? { provenanceRefs } : {}), + contextVersion: `diff-${diff.id}`, + } + }) return [...moverItems, ...diffItems] } @@ -167,15 +176,20 @@ function watchlistItems(inputs: BriefInputs): BriefItem[] { function thesisItems(inputs: BriefInputs): BriefItem[] { return inputs.diffs .filter((diff) => diff.thesisImpact !== undefined && diff.thesisImpact.direction !== 'unchanged') - .map((diff) => ({ - id: `thesis-${diff.symbol}`, - symbol: diff.symbol, - title: `${diff.symbol} thesis ${diff.thesisImpact?.direction ?? 'unchanged'}`, - message: diff.thesisImpact?.summary ?? 'Thesis impact detected.', - source: 'Thesis', - severity: diff.thesisImpact?.direction === 'invalidated' ? 'critical' : 'warning', - payload: { diffId: diff.id, direction: diff.thesisImpact?.direction }, - })) + .map((diff) => { + const provenanceRefs = diff.changes.flatMap((c) => c.evidence) + return { + id: `thesis-${diff.symbol}`, + symbol: diff.symbol, + title: `${diff.symbol} thesis ${diff.thesisImpact?.direction ?? 'unchanged'}`, + message: diff.thesisImpact?.summary ?? 'Thesis impact detected.', + source: 'Thesis', + severity: diff.thesisImpact?.direction === 'invalidated' ? 'critical' : 'warning', + payload: { diffId: diff.id, direction: diff.thesisImpact?.direction }, + ...(provenanceRefs.length > 0 ? { provenanceRefs } : {}), + contextVersion: `diff-${diff.id}`, + } + }) } function alertItems(inputs: BriefInputs): BriefItem[] { @@ -193,22 +207,31 @@ function alertItems(inputs: BriefInputs): BriefItem[] { function automationItems(inputs: BriefInputs): BriefItem[] { return inputs.runs .filter((run) => run.materialChanges > 0 || run.notified) - .map((run) => ({ - id: `automation-${run.id}`, - title: - run.materialChanges > 0 - ? `Automation: ${run.materialChanges} material change${run.materialChanges === 1 ? '' : 's'}` - : `Automation: ${run.ruleId} completed`, - message: `Evaluated ${run.evaluated} securities, analyzed ${run.analyzed}.`, - source: 'Automation', - severity: run.materialChanges > 0 ? 'warning' : 'info', - payload: { - ruleId: run.ruleId, - evaluated: run.evaluated, - analyzed: run.analyzed, - failures: run.failures, - }, - })) + .map((run) => { + const isMaterial = run.materialChanges > 0 + const provenanceRefs: string[] = [] + if (isMaterial) { + provenanceRefs.push(`automation-run:${run.id}:material`) + } + return { + id: `automation-${run.id}`, + title: + isMaterial + ? `Automation: ${run.materialChanges} material change${run.materialChanges === 1 ? '' : 's'}` + : `Automation: ${run.ruleId} completed`, + message: `Evaluated ${run.evaluated} securities, analyzed ${run.analyzed}.`, + source: 'Automation', + severity: isMaterial ? 'warning' : 'info', + payload: { + ruleId: run.ruleId, + evaluated: run.evaluated, + analyzed: run.analyzed, + failures: run.failures, + }, + ...(provenanceRefs.length > 0 ? { provenanceRefs } : {}), + ...(run.id ? { contextVersion: `run-${run.id}` } : {}), + } + }) } function compareItems(a: BriefItem, b: BriefItem): number {