From bbcb4de755ca98aeab2546156bca101f4d3b549c Mon Sep 17 00:00:00 2001 From: nanhanq1 <157583847+nanhanq1@users.noreply.github.com> Date: Mon, 28 Sep 2026 21:58:01 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat(research):=20=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E8=AF=81=E6=8D=AE=E7=9A=84=E5=8E=9F=E5=A7=8B=20passage=20?= =?UTF-8?q?=E4=BD=8D=E7=BD=AE=EF=BC=8C=E9=87=8D=E8=BD=BD=E5=90=8E=E4=BB=8D?= =?UTF-8?q?=E5=8F=AF=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Issue #13 要求 evidence 定位到原始 source 的具体 passage/span,而不是只保存 run/URL 级引用。此前 EvidenceRef 只记录产出该证据的 capability run,而一条 research.news 的 run 会带回多条新闻,从 claim 跳过去只能落到来源集合。 - 新增 SourcePassage / PassageLocation 契约:source/document identity、 逐字 excerpt、以及 start/end 字符偏移与 1-based line/column - 新增确定性定位器 locatePassage:只容忍空白差异,其余逐字匹配;定位不到 返回 undefined(fail-closed),绝不回退到相近偏移 - relocatePassage 在重载后按当前文本复算位置;来源变了如实返回 undefined - buildCapabilityPassages 只处理自带可检索文本的来源(当前 research.news), 结构化数据能力返回空,不把 Folio 自己生成的 summary 当 passage - assembleReport 接线,写入可选的 ResearchReport.evidencePassages --- packages/core/src/evidence-passage.ts | 66 ++++ packages/core/src/index.ts | 1 + packages/core/src/research.ts | 13 + .../src/research/evidence-passage.test.ts | 281 ++++++++++++++++++ .../shared/src/research/evidence-passage.ts | 209 +++++++++++++ packages/shared/src/research/index.ts | 8 + packages/shared/src/research/runner.test.ts | 122 +++++++- packages/shared/src/research/runner.ts | 17 ++ 8 files changed, 716 insertions(+), 1 deletion(-) create mode 100644 packages/core/src/evidence-passage.ts create mode 100644 packages/shared/src/research/evidence-passage.test.ts create mode 100644 packages/shared/src/research/evidence-passage.ts diff --git a/packages/core/src/evidence-passage.ts b/packages/core/src/evidence-passage.ts new file mode 100644 index 00000000..3172ecb8 --- /dev/null +++ b/packages/core/src/evidence-passage.ts @@ -0,0 +1,66 @@ +/** + * Passage-level evidence location — where a piece of evidence actually sits + * inside the source it was retrieved from. + * + * Issue #13 asks for evidence that points at a concrete passage/span instead of + * a bare URL or a document-level reference. A `SourcePassage` keeps the minimum + * needed to restore that position later: source/document identity, the verbatim + * excerpt, and location metadata describing where the excerpt was found in the + * retrieved text. + * + * Two deliberate constraints: + * + * - The excerpt is verbatim source text. It is never synthesized, summarized or + * re-worded by Folio, and a verifier's explanation can never become one. + * - The location is metadata about a past retrieval, not a promise about the + * present. Re-deriving it against a freshly retrieved document may legitimately + * fail (the source changed); callers must treat that as "unresolvable", never + * as a reason to guess a nearby offset. + */ + +/** + * Where a passage sits inside the source text it was located in. + * + * Offsets are 0-based character offsets into the exact text that was searched, + * so `sourceText.slice(start, end)` is the excerpt itself. `line` / `column` are + * 1-based and are what a reader-facing jump needs in order to scroll a document. + */ +export interface PassageLocation { + /** 0-based offset of the first character of the excerpt. */ + start: number; + /** Exclusive end offset. `sourceText.slice(start, end)` equals the excerpt. */ + end: number; + /** 1-based line number of `start`. */ + line: number; + /** 1-based column of `start` within its line. */ + column: number; +} + +/** + * One locatable span of a source document, together with the evidence-producing + * run it came from. + * + * A passage is deliberately not a claim: it records "this text exists at this + * position in this source". Which claims rest on it is decided elsewhere, which + * is what keeps one source able to support several claims. + */ +export interface SourcePassage { + /** + * Stable id inside the report. Derived from the run, the capability, the + * source document identity and the span — never from array position — so it + * survives report assembly, persistence, reload and export unchanged. + */ + id: string; + /** `CapabilityRunRecord.id` of the run whose result this source came from. */ + runId: string; + /** Capability that retrieved the source, e.g. `research.news`. */ + capabilityId: string; + /** Provider document identity within the run, e.g. `NewsItem.id`, when known. */ + documentId?: string; + /** Canonical public URL of the source document, when it has one. */ + canonicalUrl?: string; + /** Verbatim excerpt taken from the source. Never synthesized. */ + excerpt: string; + /** Where `excerpt` sits inside the retrieved source text. */ + location: PassageLocation; +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 4086eb60..c7579ef6 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -575,6 +575,7 @@ export interface Skill { export * from './answer-blocks.ts'; export * from './citations.ts'; export * from './capability.ts'; +export * from './evidence-passage.ts'; export * from './research.ts'; export * from './thesis.ts'; export * from './alert-rules.ts'; diff --git a/packages/core/src/research.ts b/packages/core/src/research.ts index b9e54f26..6bf997d3 100644 --- a/packages/core/src/research.ts +++ b/packages/core/src/research.ts @@ -1,4 +1,5 @@ import type { CapabilityProvenance, CapabilityRunStatus } from './capability.ts'; +import type { SourcePassage } from './evidence-passage.ts'; import type { SupportedLocale } from './locale.ts'; import type { StrategyId } from './strategy.ts'; @@ -88,6 +89,18 @@ export interface ResearchReport { * failed or were unavailable — the report still stands, gaps are explicit. */ runStatus: ResearchRunStatus; + /** + * Locatable source passages behind this report's evidence (issue #13): the + * verbatim excerpt each text-bearing source contributed, plus the position it + * occupies in that source, so a claim → source jump can land on the passage + * instead of the source's front page. + * + * Optional, and deliberately additive to `sections[].evidence` rather than a + * replacement: reports persisted before passage location existed omit it, and + * a source that carries no retrievable text simply contributes nothing. An + * absent entry means "no passage recorded", never "no evidence". + */ + evidencePassages?: SourcePassage[]; } /** Lightweight progress record for the Research UI. */ diff --git a/packages/shared/src/research/evidence-passage.test.ts b/packages/shared/src/research/evidence-passage.test.ts new file mode 100644 index 00000000..39b5d418 --- /dev/null +++ b/packages/shared/src/research/evidence-passage.test.ts @@ -0,0 +1,281 @@ +import { describe, expect, it } from 'bun:test'; +import type { CapabilityResult, NewsItem, SourcePassage } from '@finagent/core'; +import { + buildCapabilityPassages, + locatePassage, + passageIdOf, + passagesForEvidence, + relocatePassage, +} from './evidence-passage.ts'; + +function provenance() { + return { provider: 'longbridge', fetchedAt: 1_700_000_000_000, stale: false }; +} + +function newsItem(overrides: Partial = {}): NewsItem { + return { + id: 'n1', + title: 'Acme beats guidance', + summary: 'Acme reported revenue above guidance.', + url: 'https://example.com/acme-q3', + timestamp: 1_700_000_000, + symbols: ['ACME.US'], + ...overrides, + }; +} + +function newsResult(items: NewsItem[]): CapabilityResult { + return { data: items, provenance: provenance(), summary: 'ACME.US News' }; +} + +const NEWS_SOURCE_TEXT = 'Acme beats guidance\n\nAcme reported revenue above guidance.'; + +describe('locatePassage', () => { + it('returns exact offsets that slice back to the excerpt', () => { + const source = 'Revenue rose 12%. Guidance was unchanged for the year.'; + const excerpt = 'Guidance was unchanged for the year.'; + + const location = locatePassage(source, excerpt); + + expect(location).toBeDefined(); + expect(source.slice(location!.start, location!.end)).toBe(excerpt); + }); + + it('reports 1-based line and column for a multi-line source', () => { + const source = 'First line.\nSecond line with the passage here.\nThird line.'; + + const location = locatePassage(source, 'the passage here'); + + expect(location).toEqual({ start: 29, end: 45, line: 2, column: 18 }); + expect(source.slice(location!.start, location!.end)).toBe('the passage here'); + }); + + it('tolerates re-wrapped whitespace while keeping offsets in the original text', () => { + const source = 'Alpha\n beta gamma\ndelta'; + + const location = locatePassage(source, 'beta gamma'); + + expect(location).toEqual({ start: 9, end: 21, line: 2, column: 4 }); + expect(source.slice(location!.start, location!.end)).toBe('beta gamma'); + }); + + it('treats the excerpt literally instead of as a pattern', () => { + const source = 'EPS (diluted) rose 12% to $1.23.'; + + const location = locatePassage(source, 'EPS (diluted)'); + + expect(location).toEqual({ start: 0, end: 13, line: 1, column: 1 }); + }); + + it('selects a specific occurrence and refuses an out-of-range one', () => { + const source = 'risk risk risk'; + + expect(locatePassage(source, 'risk', { occurrence: 2 })).toEqual({ + start: 5, + end: 9, + line: 1, + column: 6, + }); + expect(locatePassage(source, 'risk', { occurrence: 4 })).toBeUndefined(); + expect(locatePassage(source, 'risk', { occurrence: 0 })).toBeUndefined(); + }); + + it('fails closed instead of matching a paraphrase', () => { + const source = 'Revenue rose 12% year over year.'; + + expect(locatePassage(source, 'Revenue fell 12% year over year.')).toBeUndefined(); + expect(locatePassage(source, 'Revenue grew 12% year over year.')).toBeUndefined(); + }); + + it('fails closed on empty input rather than returning offset zero', () => { + expect(locatePassage('some source text', '')).toBeUndefined(); + expect(locatePassage('some source text', ' \n ')).toBeUndefined(); + expect(locatePassage('', 'anything')).toBeUndefined(); + }); + + it('refuses an excerpt too long to be a passage', () => { + const long = Array.from({ length: 401 }, (_, index) => `w${index}`).join(' '); + + expect(locatePassage(long, long)).toBeUndefined(); + }); +}); + +describe('relocatePassage', () => { + it('re-derives the same location from the same text after a JSON round-trip', () => { + const passages = buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: newsResult([newsItem()]), + }); + const reloaded = JSON.parse(JSON.stringify(passages)) as SourcePassage[]; + + expect(reloaded[0].location).toEqual(passages[0].location); + expect(relocatePassage(reloaded[0], NEWS_SOURCE_TEXT)).toEqual(passages[0].location); + }); + + it('reports the passage as unresolvable when the source changed', () => { + const passage = { excerpt: 'Acme reported revenue above guidance.' }; + + expect(relocatePassage(passage, 'Acme beats guidance\n\nA rewritten article.')).toBeUndefined(); + }); + + it('never falls back to a stored offset that no longer matches', () => { + const source = 'Acme misses guidance\n\nGuidance was withdrawn.'; + + expect(locatePassage(source, 'Acme reported revenue above guidance.')).toBeUndefined(); + }); +}); + +describe('buildCapabilityPassages', () => { + it('records one locatable passage per news item', () => { + const passages = buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: newsResult([newsItem()]), + }); + + expect(passages).toHaveLength(1); + expect(passages[0]).toEqual({ + id: 'passage:run-1:research.news:n1:21-58', + runId: 'run-1', + capabilityId: 'research.news', + documentId: 'n1', + canonicalUrl: 'https://example.com/acme-q3', + excerpt: 'Acme reported revenue above guidance.', + location: { start: 21, end: 58, line: 3, column: 1 }, + }); + expect(NEWS_SOURCE_TEXT.slice(passages[0].location.start, passages[0].location.end)) + .toBe(passages[0].excerpt); + }); + + it('gives one piece of evidence several passages (many-to-many)', () => { + const passages = buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: newsResult([ + newsItem(), + newsItem({ + id: 'n2', + title: 'Acme raises outlook', + summary: 'The outlook was raised for the full year.', + url: 'https://example.com/acme-outlook', + }), + ]), + }); + + expect(passages).toHaveLength(2); + expect(passages.map((passage) => passage.documentId)).toEqual(['n1', 'n2']); + expect(passages[1].location).toEqual({ start: 21, end: 62, line: 3, column: 1 }); + expect(passagesForEvidence(passages, { runId: 'run-1', capabilityId: 'research.news' })) + .toHaveLength(2); + }); + + it('derives ids from source identity, not array position', () => { + const first = newsItem(); + const second = newsItem({ id: 'n2', title: 'Acme raises outlook', summary: 'Outlook raised.' }); + + const forward = buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: newsResult([first, second]), + }); + const reversed = buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: newsResult([second, first]), + }); + + expect(reversed.map((passage) => passage.id)).toEqual([forward[1].id, forward[0].id]); + }); + + it('falls back to the headline when a news item has no summary', () => { + const passages = buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: newsResult([newsItem({ summary: '' })]), + }); + + expect(passages).toHaveLength(1); + expect(passages[0].excerpt).toBe('Acme beats guidance'); + expect(passages[0].location).toEqual({ start: 0, end: 19, line: 1, column: 1 }); + }); + + it('skips items that carry no retrievable text', () => { + const passages = buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: newsResult([newsItem({ title: '', summary: ' ' })]), + }); + + expect(passages).toEqual([]); + }); + + it('refuses to locate a generated capability summary', () => { + const passages = buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'market.quote', + result: { + data: { symbol: 'ACME.US', change: 3 }, + provenance: provenance(), + summary: 'ACME.US price momentum reads positive.', + }, + }); + + expect(passages).toEqual([]); + }); + + it('returns no passages when news data is malformed', () => { + expect(buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: { data: [{ id: 'n1' }], provenance: provenance() }, + })).toEqual([]); + + expect(buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: { data: 'not an array', provenance: provenance() }, + })).toEqual([]); + }); +}); + +describe('passageIdOf', () => { + it('is stable and uses the provider document identity when present', () => { + const location = { start: 1, end: 5, line: 1, column: 2 }; + + expect(passageIdOf({ runId: 'r', capabilityId: 'c', documentId: 'd', position: 7, location })) + .toBe('passage:r:c:d:1-5'); + expect(passageIdOf({ runId: 'r', capabilityId: 'c', documentId: 'd', position: 7, location })) + .toBe(passageIdOf({ runId: 'r', capabilityId: 'c', documentId: 'd', position: 0, location })); + }); + + it('falls back to position only when the source has no document identity', () => { + const location = { start: 1, end: 5, line: 1, column: 2 }; + + expect(passageIdOf({ runId: 'r', capabilityId: 'c', position: 3, location })) + .toBe('passage:r:c:#3:1-5'); + }); +}); + +describe('passagesForEvidence', () => { + it('resolves only the passages produced by that evidence', () => { + const runOne = buildCapabilityPassages({ + runId: 'run-1', + capabilityId: 'research.news', + result: newsResult([newsItem()]), + }); + const runTwo = buildCapabilityPassages({ + runId: 'run-2', + capabilityId: 'research.news', + result: newsResult([newsItem({ id: 'n9' })]), + }); + const passages = [...runOne, ...runTwo]; + + expect(passagesForEvidence(passages, { runId: 'run-2', capabilityId: 'research.news' })) + .toEqual(runTwo); + expect(passagesForEvidence(passages, { runId: 'run-1', capabilityId: 'market.quote' })) + .toEqual([]); + expect(passagesForEvidence(passages, { runId: 'missing', capabilityId: 'research.news' })) + .toEqual([]); + }); +}); diff --git a/packages/shared/src/research/evidence-passage.ts b/packages/shared/src/research/evidence-passage.ts new file mode 100644 index 00000000..ca62ffd9 --- /dev/null +++ b/packages/shared/src/research/evidence-passage.ts @@ -0,0 +1,209 @@ +import type { + CapabilityResult, + NewsItem, + PassageLocation, + SourcePassage, +} from '@finagent/core'; + +/** + * Passage-level evidence location for Deep Research reports (issue #13, step 2). + * + * The report already records *which run* produced a piece of evidence. What it + * could not record was *where inside the source* the supporting text actually + * is — so a reader jumping from a claim landed on a source homepage rather than + * the passage. This module derives that position at assembly time and can + * re-derive it after a reload, which is what keeps a claim → passage jump valid + * across persistence. + * + * Everything here is a pure function over data the report already carries, so a + * rebuilt report yields byte-identical passages: no clock, no filesystem, no + * second source of truth. + */ + +/** How many whitespace-separated words an excerpt may span before we give up. */ +const MAX_EXCERPT_WORDS = 400; + +/** + * Locate `excerpt` inside `sourceText` and return its exact position. + * + * Matching is whitespace-tolerant in one direction only: runs of whitespace in + * the excerpt may match runs of whitespace in the source, so an excerpt lifted + * from a re-wrapped document still resolves. Every other character must match + * exactly — the excerpt is verbatim source text, and a "close enough" match is + * precisely the fabricated citation this feature exists to prevent. + * + * Returns `undefined` when the excerpt is empty, too long to be a passage, or + * simply absent. Callers must treat `undefined` as "unresolvable" and surface + * that, never substitute a nearby offset. + * + * @param options.occurrence 1-based index of the match to return. Defaults to 1; + * out-of-range values resolve to `undefined` rather than clamping. + */ +export function locatePassage( + sourceText: string, + excerpt: string, + options?: { occurrence?: number }, +): PassageLocation | undefined { + if (typeof sourceText !== 'string' || sourceText.length === 0) return undefined; + if (typeof excerpt !== 'string') return undefined; + + const trimmed = excerpt.trim(); + if (trimmed.length === 0) return undefined; + + const words = trimmed.split(/\s+/); + if (words.length > MAX_EXCERPT_WORDS) return undefined; + + const occurrence = options?.occurrence ?? 1; + if (!Number.isInteger(occurrence) || occurrence < 1) return undefined; + + // `\s+` between escaped words keeps offsets in the ORIGINAL source text, so + // `sourceText.slice(start, end)` stays a faithful excerpt even when the + // matched run of whitespace is longer than the one in the excerpt. + const pattern = new RegExp(words.map(escapeRegExp).join('\\s+'), 'gu'); + let match: RegExpExecArray | null; + let seen = 0; + while ((match = pattern.exec(sourceText)) !== null) { + seen += 1; + if (seen === occurrence) { + const start = match.index; + const end = start + match[0].length; + return { start, end, ...lineColumnAt(sourceText, start) }; + } + } + return undefined; +} + +/** + * Re-derive a persisted passage's location against the source text available + * now. Returns `undefined` when the excerpt no longer appears, which is the + * honest answer when a source has changed or been taken down. + * + * The passage's own `location` is never used as a fallback: stale offsets that + * happen to still be in range would highlight unrelated text. + */ +export function relocatePassage( + passage: Pick, + sourceText: string, + options?: { occurrence?: number }, +): PassageLocation | undefined { + return locatePassage(sourceText, passage.excerpt, options); +} + +export interface BuildPassagesInput { + /** `CapabilityRunRecord.id` of the run that produced `result`. */ + runId: string; + /** Capability id, e.g. `research.news`. */ + capabilityId: string; + result: CapabilityResult; +} + +/** + * Derive locatable passages from one capability result. + * + * Only sources that carry their own retrievable text can be located, so this + * returns `[]` for structured-data capabilities rather than inventing a span + * out of a generated summary — a `CapabilityResult.summary` is Folio's prose, + * not the source's, and locating it would manufacture provenance. + */ +export function buildCapabilityPassages(input: BuildPassagesInput): SourcePassage[] { + if (input.capabilityId !== 'research.news') return []; + if (!isNewsItems(input.result.data)) return []; + + const passages: SourcePassage[] = []; + input.result.data.forEach((item, index) => { + const sourceText = newsSourceText(item); + if (sourceText.length === 0) return; + + const excerpt = newsExcerpt(item); + const location = locatePassage(sourceText, excerpt); + if (location === undefined) return; + + passages.push({ + id: passageIdOf({ + runId: input.runId, + capabilityId: input.capabilityId, + documentId: item.id, + position: index, + location, + }), + runId: input.runId, + capabilityId: input.capabilityId, + ...(item.id ? { documentId: item.id } : {}), + ...(item.url ? { canonicalUrl: item.url } : {}), + excerpt, + location, + }); + }); + return passages; +} + +/** Passages a piece of evidence resolves to — the evidence → passage direction. */ +export function passagesForEvidence( + passages: readonly SourcePassage[], + evidence: { runId: string; capabilityId: string }, +): SourcePassage[] { + return passages.filter( + (passage) => + passage.runId === evidence.runId && passage.capabilityId === evidence.capabilityId, + ); +} + +/** + * Deterministic passage id. `position` is only a tiebreaker for sources with no + * provider document identity (and for duplicate ids), so reordering two news + * items that both have ids never renames an existing passage. + */ +export function passageIdOf(input: { + runId: string; + capabilityId: string; + documentId?: string; + position: number; + location: PassageLocation; +}): string { + const document = input.documentId && input.documentId.length > 0 + ? input.documentId + : `#${input.position}`; + return `passage:${input.runId}:${input.capabilityId}:${document}:${input.location.start}-${input.location.end}`; +} + +/** The exact text a news item was retrieved as. Used only to locate offsets. */ +function newsSourceText(item: NewsItem): string { + const title = typeof item.title === 'string' ? item.title.trim() : ''; + const summary = typeof item.summary === 'string' ? item.summary.trim() : ''; + if (title.length === 0) return summary; + return summary.length === 0 ? title : `${title}\n\n${summary}`; +} + +/** + * The excerpt a news item contributes. The summary is the substantive retrieved + * text, so it is preferred; a summary-less item falls back to its headline. + */ +function newsExcerpt(item: NewsItem): string { + const summary = typeof item.summary === 'string' ? item.summary.trim() : ''; + if (summary.length > 0) return summary; + return typeof item.title === 'string' ? item.title.trim() : ''; +} + +function isNewsItems(data: unknown): data is NewsItem[] { + return Array.isArray(data) && data.every( + (item) => item !== null && typeof item === 'object' + && typeof (item as NewsItem).title === 'string' + && typeof (item as NewsItem).summary === 'string', + ); +} + +function lineColumnAt(sourceText: string, offset: number): { line: number; column: number } { + let line = 1; + let lineStart = 0; + for (let index = 0; index < offset; index++) { + if (sourceText.charCodeAt(index) === 10) { + line += 1; + lineStart = index + 1; + } + } + return { line, column: offset - lineStart + 1 }; +} + +function escapeRegExp(value: string): string { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/packages/shared/src/research/index.ts b/packages/shared/src/research/index.ts index 2b071548..4a37a815 100644 --- a/packages/shared/src/research/index.ts +++ b/packages/shared/src/research/index.ts @@ -7,6 +7,14 @@ export { type PlannedCapability, } from './planner.ts'; export { ResearchRunner, type ResearchRunnerOptions, type ResearchRunRequest, type ResearchRunResult } from './runner.ts'; +export { + buildCapabilityPassages, + locatePassage, + passageIdOf, + passagesForEvidence, + relocatePassage, + type BuildPassagesInput, +} from './evidence-passage.ts'; export { LocalResearchSynthesizer } from './synthesizer-local.ts'; export { createAgentSynthesizer, parseSynthesisJson, type ResearchAgentRunner } from './agent-synth.ts'; export { ResearchReportRepository, type ReportSummary } from './repository.ts'; diff --git a/packages/shared/src/research/runner.test.ts b/packages/shared/src/research/runner.test.ts index 97ad64f5..9e3e6072 100644 --- a/packages/shared/src/research/runner.test.ts +++ b/packages/shared/src/research/runner.test.ts @@ -1,10 +1,57 @@ import { describe, expect, it } from 'bun:test'; -import type { ResearchRunSummary } from '@finagent/core'; +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { Type } from '@sinclair/typebox'; +import type { FinanceCapability, NewsItem, ResearchRunSummary } from '@finagent/core'; import { createCapabilityRegistry } from '../capabilities/index.ts'; +import { JsonFileStore } from '../storage/json-file-store.ts'; import { LocalResearchSynthesizer } from './synthesizer-local.ts'; +import { ResearchReportRepository } from './repository.ts'; import { ResearchRunner } from './runner.ts'; import { fakeCap } from './test-helpers.ts'; import { RESEARCH_CAPABILITY_PLAN } from './planner.ts'; +import { passagesForEvidence, relocatePassage } from './evidence-passage.ts'; + +const NEWS_ITEMS: NewsItem[] = [ + { + id: 'n1', + title: 'Acme beats guidance', + summary: 'Acme reported revenue above guidance.', + url: 'https://example.com/acme-q3', + timestamp: 1_700_000_000, + symbols: ['ACME.US'], + }, + { + id: 'n2', + title: 'Acme raises outlook', + summary: 'The outlook was raised for the full year.', + url: 'https://example.com/acme-outlook', + timestamp: 1_700_000_100, + symbols: ['ACME.US'], + }, +]; + +/** A `research.news` capability that returns real news items, unlike `fakeCap`. */ +function newsCapability(items: NewsItem[]): FinanceCapability { + return { + id: 'research.news', + name: 'News', + description: 'fake research.news returning retrievable text', + category: 'research', + riskLevel: 'read', + auth: 'public', + toolName: 'get_news', + inputSchema: Type.Object({ symbol: Type.String() }), + async execute() { + return { + data: items, + provenance: { provider: 'test', fetchedAt: 1_700_000_000_000, stale: false }, + summary: 'ACME.US News', + }; + }, + }; +} function makeRunner(capabilities: Array<[string, Parameters[1]?]>) { const registry = createCapabilityRegistry( @@ -132,4 +179,77 @@ describe('ResearchRunner', () => { expect(result.report).toBeUndefined(); expect(statuses[statuses.length - 1]).toBe('cancelled'); }); + + it('records locatable source passages for text-bearing evidence', async () => { + const registry = createCapabilityRegistry([newsCapability(NEWS_ITEMS)]); + const runner = new ResearchRunner({ + registry, + synthesizer: new LocalResearchSynthesizer(), + now: () => 1_700_000_000_000, + }); + + const result = await runner.run({ symbol: 'ACME.US', runId: 'run-passages' }); + const report = result.report!; + const passages = report.evidencePassages ?? []; + + expect(passages).toHaveLength(NEWS_ITEMS.length); + + // Passages hang off the same evidence the report already exposes, so the + // claim → evidence → passage jump needs no extra side table. + const newsEvidence = report.sections.find((section) => section.key === 'research.news')! + .evidence[0]; + expect(passagesForEvidence(passages, newsEvidence).map((passage) => passage.documentId)) + .toEqual(['n1', 'n2']); + expect(passages[0].runId).toBe(newsEvidence.runId); + expect(passages[0].capabilityId).toBe(newsEvidence.capabilityId); + + // The location survives persistence and still resolves to the same span. + const reloaded = JSON.parse(JSON.stringify(report)) as typeof report; + expect(reloaded.evidencePassages![0].location).toEqual(passages[0].location); + expect(relocatePassage( + reloaded.evidencePassages![0], + 'Acme beats guidance\n\nAcme reported revenue above guidance.', + )).toEqual(passages[0].location); + }); + + it('omits passages entirely when no source carries retrievable text', async () => { + const runner = makeRunner([['market.quote', 'success']]); + const result = await runner.run({ symbol: 'NVDA.US', runId: 'run-no-passages' }); + + // Structured data has no passage to point at; the field stays absent rather + // than being filled with a span over Folio's own generated summary. + expect(result.report!.evidencePassages).toBeUndefined(); + }); + + it('keeps a passage jump valid after the report is persisted and reloaded', async () => { + const dir = await mkdtemp(join(tmpdir(), 'finagent-research-passages-')); + try { + const registry = createCapabilityRegistry([newsCapability(NEWS_ITEMS)]); + const runner = new ResearchRunner({ + registry, + synthesizer: new LocalResearchSynthesizer(), + now: () => 1_700_000_000_000, + }); + const result = await runner.run({ symbol: 'ACME.US', runId: 'run-reload' }); + + await new ResearchReportRepository(new JsonFileStore(dir)).saveReport(result.report!); + + // A second store instance stands in for a fresh process reading from disk. + const reloaded = await new ResearchReportRepository(new JsonFileStore(dir)) + .getReport('report-run-reload'); + expect(reloaded).toBeDefined(); + + const evidence = reloaded!.sections.find((section) => section.key === 'research.news')! + .evidence[0]; + const passages = passagesForEvidence(reloaded!.evidencePassages ?? [], evidence); + + expect(passages).toHaveLength(NEWS_ITEMS.length); + expect(relocatePassage( + passages[0], + 'Acme beats guidance\n\nAcme reported revenue above guidance.', + )).toEqual(passages[0].location); + } finally { + await rm(dir, { recursive: true, force: true }); + } + }); }); diff --git a/packages/shared/src/research/runner.ts b/packages/shared/src/research/runner.ts index 655bddc3..b3c2f11d 100644 --- a/packages/shared/src/research/runner.ts +++ b/packages/shared/src/research/runner.ts @@ -24,6 +24,7 @@ import { planForStrategy, type PlannedCapability, } from './planner.ts'; +import { buildCapabilityPassages } from './evidence-passage.ts'; const CONCURRENCY = 4; const TIMEOUT_MS = 20000; @@ -365,6 +366,21 @@ function assembleReport(args: { .map((outcome) => outcome.result?.provenance?.instrumentId ?? readInstrumentId(outcome.result?.data)) .find((id): id is string => typeof id === 'string' && id.length > 0); + // Passage-level provenance (issue #13): a text-bearing source records the + // verbatim span it contributed together with its position in that source, so + // the report carries enough to re-open the exact passage after a reload. + // Derived from the run outcomes rather than from the synthesized prose, so + // the location can never be invented by the model. + const evidencePassages = outcomes.flatMap((outcome) => + outcome.record.status === 'success' && outcome.result + ? buildCapabilityPassages({ + runId: outcome.record.id, + capabilityId: outcome.record.capabilityId, + result: outcome.result, + }) + : [] + ); + return { id: `report-${runId}`, symbol, @@ -386,5 +402,6 @@ function assembleReport(args: { risks: synthesis.risks, capabilityRuns, runStatus: computeRunStatus(plan, successIds), + ...(evidencePassages.length > 0 ? { evidencePassages } : {}), }; } From 963d0ffb82bb51753eb9c4f1f487a13e4b9ba9b5 Mon Sep 17 00:00:00 2001 From: nanhanq1 <157583847+nanhanq1@users.noreply.github.com> Date: Mon, 28 Sep 2026 22:34:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?test(research):=20=E8=A1=A5=E7=9C=9F?= =?UTF-8?q?=E5=AE=9E=E6=A8=A1=E5=9E=8B=20+=20=E5=AE=9E=E9=99=85=20retrieva?= =?UTF-8?q?l=20=E7=9A=84=20passage=20=E5=AE=9A=E4=BD=8D=E9=AA=8C=E6=94=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit issue #13 的验收项 7/8 要求真实 Deep Research E2E,只跑 fixture 不算完成。 本 PR 的切片是 passage 级证据定位,因此补一个只走生产路径的验收: - apps/electron/e2e/evidence-passage-live-acceptance.ts 真实模型(HTTP 流式)+ 真实 AgentKernel + 生产 createAgentSynthesizer + 生产 research.news 能力(仅注入 getNews fetcher)+ 生产 ResearchService。 断言四组:真实 retrieval 产出 evidencePassages;每条 passage 的 offset 能 relocate 回同一位置且 slice 等于 excerpt;每条 EvidenceRef 能经 passagesForEvidence 解析到 passage;换全新 repository 实例重载后仍成立。 没有凭据/没有可达检索源时直接失败,不留 fixture 退路。 - docs/acceptance/evidence-passage/ 真实运行产物:deepseek-v4-pro + 49 条实时新闻 → 49 条 passage 全部可重定位。 - docs/evidence-passage-test-report.md 验收报告。明确写出本切片**不覆盖** claim 身份、verifier 输出、双向 claim↔evidence 映射与报告 UI 跳转(分别依赖 #67 / #90 接线 / 未认领), 并如实记录注入 fetcher 时 provenance.provider 仍为 'longbridge' 的既有行为。 Related to #13 --- .../e2e/evidence-passage-live-acceptance.ts | 365 ++++++++ .../evidence-passage/final-report.json | 857 ++++++++++++++++++ .../acceptance/evidence-passage/finished.json | 37 + .../evidence-passage/model-usage.json | 16 + .../evidence-passage/retrieved-news.json | 492 ++++++++++ .../evidence-passage/verification.json | 19 + docs/evidence-passage-test-report.md | 115 +++ 7 files changed, 1901 insertions(+) create mode 100644 apps/electron/e2e/evidence-passage-live-acceptance.ts create mode 100644 docs/acceptance/evidence-passage/final-report.json create mode 100644 docs/acceptance/evidence-passage/finished.json create mode 100644 docs/acceptance/evidence-passage/model-usage.json create mode 100644 docs/acceptance/evidence-passage/retrieved-news.json create mode 100644 docs/acceptance/evidence-passage/verification.json create mode 100644 docs/evidence-passage-test-report.md diff --git a/apps/electron/e2e/evidence-passage-live-acceptance.ts b/apps/electron/e2e/evidence-passage-live-acceptance.ts new file mode 100644 index 00000000..5f201e51 --- /dev/null +++ b/apps/electron/e2e/evidence-passage-live-acceptance.ts @@ -0,0 +1,365 @@ +// Live, opt-in Deep Research E2E for passage-level evidence location (#13). +// +// Runs the PRODUCTION path end to end: a real model drives the real AgentKernel +// through the real `createAgentSynthesizer`, the real `research.news` capability +// performs real network retrieval, and the real `ResearchService`/`ResearchRunner` +// assembles the report. Nothing is fixture-backed: no LocalResearchSynthesizer, +// no canned capability, no pre-seeded run. +// +// What it proves — the slice this PR owns: +// 1. real retrieval produces `ResearchReport.evidencePassages`; +// 2. every recorded offset relocates to the exact same position in the real +// source text, and slicing that range returns the recorded excerpt, so a +// claim → source jump lands on the passage rather than the front page; +// 3. every `EvidenceRef` resolves to at least one passage (evidence → passage); +// 4. the passages survive persistence: a fresh repository instance reads the +// same report and every passage still relocates. +// +// It does NOT prove the full #13 claim→verifier chain. Claim identity and the +// bidirectional claim↔evidence mapping land in separate PRs, and the verifier +// (#90) is a standalone library not yet wired into the report path. Those are +// out of scope here and are reported as such. +// +// Retrieval: the production `research.news` capability is used as-is; only its +// `getNews` fetcher is injected. With `FINAGENT_EVIDENCE_NEWS_FEED` set it reads +// a live public RSS feed over HTTPS; without it, the production Longbridge +// fetcher is used (which needs the Longbridge CLI installed and authenticated). +// +// Env: +// DEEPSEEK_API_KEY real model credential (required, no fallback) +// FINAGENT_EVIDENCE_NEWS_FEED live RSS feed URL used as the retrieval source +// FINAGENT_EVIDENCE_OUTPUT artifact dir (default apps/electron/e2e/artifacts/evidence-passage) +// FINAGENT_EVIDENCE_MODEL model id (default deepseek-chat) +// FINAGENT_EVIDENCE_SYMBOL symbol to research (default NVDA.US) +// FINAGENT_EVIDENCE_MODEL_URL chat-completions endpoint (default https://api.deepseek.com/chat/completions; +// override for a gateway/proxy, mirroring FINAGENT_JUDGE_BASE_URL) +import assert from 'node:assert/strict'; +import { mkdir, writeFile } from 'node:fs/promises'; +import { resolve, join } from 'node:path'; +import { randomUUID } from 'node:crypto'; +import type { + AgentEvent, + AgentRunInput, + AgentRuntime, + NewsItem, +} from '@finagent/core'; +import { AgentKernel } from '../../../packages/shared/src/kernel/agent-kernel'; +import { ResearchService } from '../../../packages/shared/src/research/service'; +import { ResearchReportRepository } from '../../../packages/shared/src/research/repository'; +import { JsonFileStore } from '../../../packages/shared/src/storage/json-file-store'; +import { createCapabilityRegistry } from '../../../packages/shared/src/capabilities'; +import { createResearchNewsCapability } from '../../../packages/shared/src/capabilities/manifests/research-news'; +import { defaultCapabilityFetchers } from '../../../packages/shared/src/capabilities/fetchers'; +import { createAgentSynthesizer, parseSynthesisJson } from '../../../packages/shared/src/research/agent-synth'; +import { passagesForEvidence, relocatePassage } from '../../../packages/shared/src/research/evidence-passage'; + +const output = resolve(process.env.FINAGENT_EVIDENCE_OUTPUT ?? 'apps/electron/e2e/artifacts/evidence-passage'); +const model = process.env.FINAGENT_EVIDENCE_MODEL ?? 'deepseek-chat'; +const symbol = process.env.FINAGENT_EVIDENCE_SYMBOL ?? 'NVDA.US'; +const modelUrl = process.env.FINAGENT_EVIDENCE_MODEL_URL ?? 'https://api.deepseek.com/chat/completions'; +const newsFeed = process.env.FINAGENT_EVIDENCE_NEWS_FEED; + +assert.ok(process.env.DEEPSEEK_API_KEY, 'Set DEEPSEEK_API_KEY. No fixture fallback — this acceptance must run a real model.'); +await mkdir(output, { recursive: true }); +const save = (name: string, value: unknown) => + writeFile(join(output, name), JSON.stringify(value, null, 2)); + +async function until(read: () => Promise, timeout = 300000): Promise { + const end = Date.now() + timeout; + while (Date.now() < end) { + const result = await read(); + if (result !== undefined) return result; + await Bun.sleep(50); + } + throw new Error('Live acceptance timed out'); +} + +/** The exact text a news item was retrieved as — mirrors the production rule. */ +function sourceTextOf(item: NewsItem): string { + const title = typeof item.title === 'string' ? item.title.trim() : ''; + const summary = typeof item.summary === 'string' ? item.summary.trim() : ''; + if (title.length === 0) return summary; + return summary.length === 0 ? title : `${title}\n\n${summary}`; +} + +// ── Real retrieval: a live public RSS feed over HTTPS ────────────────────── + +function decodeXmlText(value: string): string { + const unwrapped = value.replace(//g, '$1'); + const stripped = unwrapped.replace(/<[^>]*>/g, ''); + return stripped + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/�?39;/g, "'") + .replace(/ /g, ' ') + .replace(/&#(\d+);/g, (_, code: string) => String.fromCodePoint(Number(code))) + .replace(/&#x([0-9a-f]+);/gi, (_, code: string) => String.fromCodePoint(Number.parseInt(code, 16))) + .replace(/&/g, '&') + .trim(); +} + +function tagOf(block: string, tag: string): string { + const match = new RegExp(`<${tag}(?:\\s[^>]*)?>([\\s\\S]*?)`, 'i').exec(block); + return match ? decodeXmlText(match[1]) : ''; +} + +/** Minimal RSS 2.0 reader — enough for a real headline feed, no dependency. */ +function parseRssItems(xml: string): Array<{ title: string; summary: string; url: string; published: number }> { + const blocks = xml.match(/]*)?>[\s\S]*?<\/item>/gi) ?? []; + return blocks + .map((block) => { + const title = tagOf(block, 'title'); + const summary = tagOf(block, 'description'); + const link = tagOf(block, 'link'); + const guid = tagOf(block, 'guid'); + const published = Date.parse(tagOf(block, 'pubDate')); + return { + title, + summary, + url: link.length > 0 ? link : guid, + published: Number.isFinite(published) ? Math.floor(published / 1000) : 0, + }; + }) + .filter((item) => item.title.length > 0 && item.url.length > 0); +} + +async function fetchFeedNews(feedUrl: string, forSymbol: string): Promise { + const response = await fetch(feedUrl, { signal: AbortSignal.timeout(30000) }); + if (!response.ok) throw new Error(`News feed returned HTTP ${response.status}`); + const xml = await response.text(); + await writeFile(join(output, 'news-feed.xml'), xml); + const items: NewsItem[] = parseRssItems(xml).map((item, index) => ({ + id: `feed-${index}-${item.url}`, + title: item.title, + summary: item.summary, + url: item.url, + timestamp: item.published, + symbols: [forSymbol], + })); + assert.ok(items.length > 0, 'The live news feed returned no usable items'); + return items; +} + +// ── Real model runtime: HTTP streaming, no local adapter ─────────────────── +const abort = new AbortController(); +let sequence = 0; +const event = (input: AgentRunInput, type: string, payload: unknown = {}): AgentEvent => + ({ + id: randomUUID(), + sessionId: input.sessionId, + runId: input.runId, + sequence: ++sequence, + timestamp: Date.now(), + type, + payload, + }) as AgentEvent; + +const runtime: AgentRuntime = { + async getTools() { + return { ok: true, data: [] }; + }, + async ensureSession(s) { + return { sessionId: s.id, status: 'active' }; + }, + async cancel() { + abort.abort(); + }, + async dispose() { + abort.abort(); + }, + async *run(input) { + const response = await fetch(modelUrl, { + method: 'POST', + headers: { + Authorization: 'Bearer ' + process.env.DEEPSEEK_API_KEY, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + model, + stream: true, + stream_options: { include_usage: true }, + response_format: { type: 'json_object' }, + max_tokens: 6000, + messages: [{ role: 'user', content: input.content }], + }), + signal: AbortSignal.any([abort.signal, AbortSignal.timeout(180000)]), + }); + if (!response.ok || !response.body) throw new Error('Model endpoint returned HTTP ' + response.status); + yield event(input, 'message_started'); + let answer = ''; + let buffer = ''; + const decoder = new TextDecoder(); + const reader = response.body.getReader(); + for (;;) { + const { done, value } = await reader.read(); + if (done) break; + buffer += decoder.decode(value, { stream: true }); + let newline: number; + while ((newline = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, newline).trim(); + buffer = buffer.slice(newline + 1); + if (!line.startsWith('data: ') || line === 'data: [DONE]') continue; + const part = JSON.parse(line.slice(6)); + if (part.usage) await save('model-usage.json', { model: part.model, usage: part.usage }); + const delta = part.choices?.[0]?.delta?.content; + if (!delta) continue; + answer += delta; + yield event(input, 'message_delta', { delta, answer }); + } + } + yield event(input, 'message_completed', { answer }); + yield event(input, 'run_completed', { answer, toolCalls: [] }); + }, +}; + +const kernel = new AgentKernel({ + storageDir: join(output, 'kernel'), + piSessionDir: join(output, 'sessions'), + runtime, +}); + +// ── The PRODUCTION news capability, with only its fetcher injected ───────── +const news = createResearchNewsCapability( + newsFeed + ? { ...defaultCapabilityFetchers, getNews: (requested: string) => fetchFeedNews(newsFeed, requested) } + : defaultCapabilityFetchers, +); + +// ── Production synthesizer: the real agent runner, not LocalResearchSynth ── +const synthesizer = createAgentSynthesizer(async (input) => { + const session = await kernel.sessions.createSession('Evidence passage live acceptance'); + const prompt = [ + 'Analyze only these saved facts for ' + symbol + '. Return JSON with summary, stance (bullish/bearish/neutral), confidence (0..1),', + 'sections (key/title/verdict/summary), bullCase, bearCase, catalysts, risks (string arrays).', + 'Every planned key must have exactly one section. Missing data means verdict unavailable. Do not invent facts.', + 'Section verdict must be positive, negative, neutral or unavailable. State data gaps. Respond in English.', + 'Planned keys: ' + input.plannedCapabilities.join(', '), + 'Outcomes: ' + JSON.stringify(input.runs), + 'Saved facts: ' + input.dataBundle, + ].join('\n'); + let unsubscribe = () => {}; + const result = new Promise((resolveResult, reject) => { + unsubscribe = kernel.runs.subscribe((e) => { + if (e.sessionId !== session.id) return; + if (e.type === 'run_completed') resolveResult((e.payload as { answer?: string }).answer ?? ''); + if (e.type === 'run_failed') reject(new Error(String((e.payload as { error?: { message?: string } }).error?.message))); + }); + }); + void result.catch(() => {}); + try { + await kernel.runs.startRun(session.id, prompt); + return parseSynthesisJson(await result); + } finally { + unsubscribe(); + } +}); + +const repository = new ResearchReportRepository(new JsonFileStore(join(output, 'store'))); +const service = new ResearchService({ + registry: createCapabilityRegistry([news]), + synthesizer, + repository, + getIdentity: async () => ({ provider: 'deepseek', model, config: 'evidence-passage-live-v1' }), +}); + +const started = await service.start(symbol); +console.log('Started live research ' + started.id + '; waiting for real retrieval, model streaming and report assembly.'); + +const finished = await until(async () => { + const current = await service.getRun(started.id); + return current && ['completed', 'partial', 'failed', 'cancelled', 'interrupted'].includes(current.status) + ? current + : undefined; +}); +while (kernel.runs.isRunning()) await Bun.sleep(50); +await save('finished.json', finished); + +assert.ok(['completed', 'partial'].includes(finished.status), 'Research run did not settle successfully: ' + finished.status); +assert.ok(finished.reportId, 'A settled run must publish a report id'); + +const report = await repository.getReport(finished.reportId); +assert.ok(report, 'The published report must be readable'); +await save('final-report.json', report); + +// The exact payload the capability returned, read from the durable checkpoint +// rather than re-derived — so the verification uses the real ingested data. +const checkpoint = await repository.getCheckpoint(started.id); +assert.ok(checkpoint, 'The durable checkpoint must be readable'); +const newsOutcome = checkpoint.outcomes.find((outcome) => outcome.record.capabilityId === 'research.news'); +assert.ok(newsOutcome, 'The checkpoint must record the research.news outcome'); +const rawNews: unknown = newsOutcome.result?.data; +assert.ok(Array.isArray(rawNews) && rawNews.length > 0, 'The capability must have returned retrieved news items'); +const items = rawNews as NewsItem[]; +await save('retrieved-news.json', items); + +// ── 1. Real retrieval produced passages ─────────────────────────────────── +const passages = report.evidencePassages ?? []; +assert.ok(passages.length > 0, 'Real news retrieval must produce locatable passages'); + +// ── 2. Every offset relocates to the exact same position ────────────────── +for (const passage of passages) { + assert.ok(passage.excerpt.trim().length > 0, 'A passage must carry a non-empty excerpt'); + assert.ok( + passage.documentId !== undefined || passage.canonicalUrl !== undefined, + 'A passage must identify its source document', + ); + const item = items.find((candidate) => candidate.id === passage.documentId); + assert.ok(item, 'A passage documentId must resolve to a retrieved news item'); + const sourceText = sourceTextOf(item); + const relocated = relocatePassage(passage, sourceText); + assert.deepEqual(relocated, passage.location, 'Persisted offsets must relocate to the same position'); + assert.equal( + sourceText.slice(passage.location.start, passage.location.end), + passage.excerpt, + 'The recorded offset range must slice out exactly the recorded excerpt', + ); + assert.ok(passage.location.line >= 1 && passage.location.column >= 1, 'line/column must be 1-based'); +} + +// ── 3. evidence → passage direction is usable ───────────────────────────── +const evidenceRefs = report.sections.flatMap((section) => section.evidence); +assert.ok(evidenceRefs.length > 0, 'The report must carry evidence references'); +for (const evidence of evidenceRefs) { + const linked = passagesForEvidence(passages, { runId: evidence.runId, capabilityId: evidence.capabilityId }); + assert.ok(linked.length > 0, 'Each evidence reference must resolve to at least one passage'); +} + +// ── 4. Passages survive persistence (fresh repository instance) ─────────── +const reloadedRepository = new ResearchReportRepository(new JsonFileStore(join(output, 'store'))); +const reloaded = await reloadedRepository.getReport(report.id); +assert.ok(reloaded, 'A fresh repository must read back the report'); +assert.deepEqual(reloaded.evidencePassages, passages, 'Reloaded passages must be byte-identical'); +for (const passage of reloaded.evidencePassages ?? []) { + const item = items.find((candidate) => candidate.id === passage.documentId); + assert.ok(item, 'A reloaded passage must still resolve to its source'); + assert.deepEqual( + relocatePassage(passage, sourceTextOf(item)), + passage.location, + 'A reloaded passage must still relocate to the same position', + ); +} + +const verification = { + mode: 'live AgentKernel + ResearchService production path; real model over HTTP; production research.news capability', + model, + symbol, + retrieval: newsFeed ? { kind: 'live public RSS feed', url: newsFeed } : { kind: 'Longbridge CLI (production fetcher)' }, + runId: started.id, + reportId: report.id, + runStatus: finished.status, + retrievedNewsCount: items.length, + evidenceRefCount: evidenceRefs.length, + passageCount: passages.length, + allOffsetsRelocate: true, + allEvidenceMappedToPassage: true, + survivesReload: true, + scope: + 'Passage-level evidence location only. Claim identity, bidirectional claim↔evidence mapping and verifier output are separate PRs and are not exercised here.', +}; +await save('verification.json', verification); +console.log(JSON.stringify(verification)); + +await kernel.dispose(); +process.exit(0); diff --git a/docs/acceptance/evidence-passage/final-report.json b/docs/acceptance/evidence-passage/final-report.json new file mode 100644 index 00000000..99455122 --- /dev/null +++ b/docs/acceptance/evidence-passage/final-report.json @@ -0,0 +1,857 @@ +{ + "id": "report-research-064770ff-2e25-42dd-bc18-e93d72d4ff3e", + "symbol": "NVDA.US", + "generatedAt": 1790605911421, + "locale": "en-US", + "summary": "Only research. news data is available for NVDA. US. The available news includes a positive announcement of a $150B buyback increase, but most other articles are general market/AI/tariff news not directly specific to NVDA. All other planned data sections are unavailable, preventing a full assessment.", + "stance": "neutral", + "confidence": 0.2, + "sections": [ + { + "key": "company.profile", + "title": "Company Profile", + "verdict": "unavailable", + "summary": "No data available.", + "evidence": [] + }, + { + "key": "market.quote", + "title": "Market Quote", + "verdict": "unavailable", + "summary": "No data available.", + "evidence": [] + }, + { + "key": "market.kline", + "title": "Market Kline", + "verdict": "unavailable", + "summary": "No data available.", + "evidence": [] + }, + { + "key": "company.valuation", + "title": "Company Valuation", + "verdict": "unavailable", + "summary": "No data available.", + "evidence": [] + }, + { + "key": "company.financials", + "title": "Company Financials", + "verdict": "unavailable", + "summary": "No data available.", + "evidence": [] + }, + { + "key": "company.earnings", + "title": "Company Earnings", + "verdict": "unavailable", + "summary": "No data available.", + "evidence": [] + }, + { + "key": "company.ratings", + "title": "Company Ratings", + "verdict": "unavailable", + "summary": "No data available.", + "evidence": [] + }, + { + "key": "research.news", + "title": "Research News", + "verdict": "positive", + "summary": "Nvidia announced a $150 billion increase to its stock buyback program, bringing total remaining authorized amount to $235 billion (largest ever). Other news articles cover AI safety risks, US-China tariffs, oil prices, and unrelated topics, but are not directly NVDA-specific. The buyback news is clearly positive for shareholders.", + "evidence": [ + { + "capabilityId": "research.news", + "runId": "run-research.news-1790605880681-1", + "claim": "Nvidia announced a $150 billion increase to its stock buyback program, bringing total remaining authorized amount to $235 billion (largest ever). Other news articles cover AI safety risks, US-China tariffs, oil prices, and unrelated topics, but are not directly NVDA-specific. The buyback news is clearly positive for shareholders.", + "fetchedAt": 1790605881605, + "summary": "NVDA.US News\n-----------------\n- Nvidia Adds $150 Billion to Massive Stock Buyback, the Largest Ever (2026/9/28 22:28:37)\n- Could A.I. Safety Risks Derail the Sector’s I.P.O. Prospects? (2026/9/28 19:38:12)\n- China and the U.S. Pledge to Cut Tariffs on $60 Billion in Goods (2026/9/28 19:28:27)\n- Oil Prices Climb on Continued Impasse in U.S.-Iran Talks (2026/9/28 21:49:28)\n- How Scientists Can Shape Public Opinion Over A.I. Risks (2026/9/28 17:05:13)\n- Juniper Lattes? Pine Sodas? Food Is Taking Us Back to the Forest. (2026/9/28 22:03:49)\n- Trump’s Tariffs Push Canadian Companies to Look Past American Links (2026/9/28 20:03:35)\n- Under Trump, Commodity Futures Trading Commission Scales Back Enforcement, Worrying Farmers (2026/9/28 17:02:22)\n- Here’s What Fans Paid for Harry Styles Tickets at M.S.G. (2026/9/28 19:21:05)\n- Smart Headlights Finally Trickle Down to Cars in the U.S. (2026/9/28 17:00:41)" + } + ] + }, + { + "key": "market.capitalFlow", + "title": "Market Capital Flow", + "verdict": "unavailable", + "summary": "No data available.", + "evidence": [] + }, + { + "key": "portfolio.positions", + "title": "Portfolio Positions", + "verdict": "unavailable", + "summary": "No data available.", + "evidence": [] + } + ], + "bullCase": [ + "Massive $150B increase to stock buyback, bringing total authorized to $235B, signals strong cash generation and shareholder returns." + ], + "bearCase": [ + "AI safety risks could derail sector IPO prospects and potentially affect sentiment for AI-related stocks like NVDA." + ], + "catalysts": [ + "Execution of the expanded buyback program", + "Potential easing of US-China tariff tensions", + "Further developments in AI sector and related regulation" + ], + "risks": [ + "AI safety regulatory concerns", + "Trade tensions and tariffs between US and China", + "Unrelated macroeconomic factors (oil prices, CFTC enforcement) may increase market volatility" + ], + "capabilityRuns": [ + { + "runId": "missing:company.profile", + "capabilityId": "company.profile", + "status": "unavailable", + "error": "Capability not registered" + }, + { + "runId": "missing:market.quote", + "capabilityId": "market.quote", + "status": "unavailable", + "error": "Capability not registered" + }, + { + "runId": "missing:market.kline", + "capabilityId": "market.kline", + "status": "unavailable", + "error": "Capability not registered" + }, + { + "runId": "missing:company.valuation", + "capabilityId": "company.valuation", + "status": "unavailable", + "error": "Capability not registered" + }, + { + "runId": "missing:company.financials", + "capabilityId": "company.financials", + "status": "unavailable", + "error": "Capability not registered" + }, + { + "runId": "missing:company.earnings", + "capabilityId": "company.earnings", + "status": "unavailable", + "error": "Capability not registered" + }, + { + "runId": "missing:company.ratings", + "capabilityId": "company.ratings", + "status": "unavailable", + "error": "Capability not registered" + }, + { + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "status": "success", + "fetchedAt": 1790605881605, + "marketTime": 1790605717000 + }, + { + "runId": "missing:market.capitalFlow", + "capabilityId": "market.capitalFlow", + "status": "unavailable", + "error": "Capability not registered" + }, + { + "runId": "missing:portfolio.positions", + "capabilityId": "portfolio.positions", + "status": "unavailable", + "error": "Capability not registered" + } + ], + "runStatus": "partial", + "evidencePassages": [ + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-0-https://www.nytimes.com/2026/09/28/business/nvidia-stock-buyback.html:69-226", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-0-https://www.nytimes.com/2026/09/28/business/nvidia-stock-buyback.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/business/nvidia-stock-buyback.html", + "excerpt": "The increase comes four months after the chip giant added $80 billion to its buyback program, bringing the total remaining authorized amount to $235 billion.", + "location": { + "start": 69, + "end": 226, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-1-https://www.nytimes.com/2026/09/28/business/dealbook/ai-safety-risks-ipo.html:63-198", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-1-https://www.nytimes.com/2026/09/28/business/dealbook/ai-safety-risks-ipo.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/business/dealbook/ai-safety-risks-ipo.html", + "excerpt": "Big liability questions lie ahead for the artificial intelligence labs Anthropic and OpenAI as they push ahead with plans to go public.", + "location": { + "start": 63, + "end": 198, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-2-https://www.nytimes.com/2026/09/28/business/china-us-summit-tariffs.html:66-229", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-2-https://www.nytimes.com/2026/09/28/business/china-us-summit-tariffs.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/business/china-us-summit-tariffs.html", + "excerpt": "The two countries unveiled separate lists covering thousands of products as they seek to ease trade tensions and stabilize relations ahead of further negotiations.", + "location": { + "start": 66, + "end": 229, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-3-https://www.nytimes.com/2026/09/28/business/oil-stocks-gas-prices.html:58-217", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-3-https://www.nytimes.com/2026/09/28/business/oil-stocks-gas-prices.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/business/oil-stocks-gas-prices.html", + "excerpt": "President Trump rejected Iran’s proposal over the weekend, calling it “unacceptable,” but Iran said it was still waiting for “definitive news” from Washington.", + "location": { + "start": 58, + "end": 217, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-4-https://www.nytimes.com/2026/09/28/business/ai-scientists-protests.html:57-131", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-4-https://www.nytimes.com/2026/09/28/business/ai-scientists-protests.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/business/ai-scientists-protests.html", + "excerpt": "Artificial intelligence workers may have leverage to make the world safer.", + "location": { + "start": 57, + "end": 131, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-5-https://www.nytimes.com/2026/09/28/dining/forest-flavors-alpine-foraging.html:67-199", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-5-https://www.nytimes.com/2026/09/28/dining/forest-flavors-alpine-foraging.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/dining/forest-flavors-alpine-foraging.html", + "excerpt": "Drinks and snacks are being tweaked to evoke woods and meadows, on the theory that frazzled consumers want to reconnect with nature.", + "location": { + "start": 67, + "end": 199, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-6-https://www.nytimes.com/2026/09/28/business/trump-tariffs-canada-diversify.html:69-209", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-6-https://www.nytimes.com/2026/09/28/business/trump-tariffs-canada-diversify.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/business/trump-tariffs-canada-diversify.html", + "excerpt": "Shaken by tariffs and threats from the Trump administration, Canadian businesses are seeking markets and suppliers beyond the United States.", + "location": { + "start": 69, + "end": 209, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-7-https://www.nytimes.com/2026/09/28/us/politics/cotton-trump-commodity-markets-cftc.html:93-297", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-7-https://www.nytimes.com/2026/09/28/us/politics/cotton-trump-commodity-markets-cftc.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/us/politics/cotton-trump-commodity-markets-cftc.html", + "excerpt": "The fate of two cotton brokers highlights how the Commodity Futures Trading Commission has scaled back its regulatory actions not just for crypto cases but also for the kinds of cases it has long policed.", + "location": { + "start": 93, + "end": 297, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-8-https://www.nytimes.com/2026/09/28/arts/music/harry-styles-msg-ticket-prices-affordability.html:58-206", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-8-https://www.nytimes.com/2026/09/28/arts/music/harry-styles-msg-ticket-prices-affordability.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/arts/music/harry-styles-msg-ticket-prices-affordability.html", + "excerpt": "What is a concert really worth? We asked 34 fans about the time, money and effort they spent getting tickets for the star’s New York City residency.", + "location": { + "start": 58, + "end": 206, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-9-https://www.nytimes.com/2026/09/28/business/smart-headlights-adaptive.html:59-206", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-9-https://www.nytimes.com/2026/09/28/business/smart-headlights-adaptive.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/business/smart-headlights-adaptive.html", + "excerpt": "Federal data shows that half of all traffic fatalities happen after dark, but only one-quarter of driving happens at night. So any help is welcome.", + "location": { + "start": 59, + "end": 206, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-10-https://www.nytimes.com/2026/09/27/us/dee-brock-dead.html:68-179", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-10-https://www.nytimes.com/2026/09/27/us/dee-brock-dead.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/27/us/dee-brock-dead.html", + "excerpt": "As the first director of the troupe, she helped transform it from a high school squad into a global phenomenon.", + "location": { + "start": 68, + "end": 179, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-11-https://www.nytimes.com/2026/09/27/obituaries/morris-ballen-dead.html:67-208", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-11-https://www.nytimes.com/2026/09/27/obituaries/morris-ballen-dead.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/27/obituaries/morris-ballen-dead.html", + "excerpt": "Countless independent acts in every genre — punk, hip-hop, church choir — relied on him to create small runs of high-quality records and CDs.", + "location": { + "start": 67, + "end": 208, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-12-https://www.nytimes.com/2026/09/27/business/workplace-.html:50-107", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-12-https://www.nytimes.com/2026/09/27/business/workplace-.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/27/business/workplace-.html", + "excerpt": "Plus: What to do when your boss is nit-picking your work.", + "location": { + "start": 50, + "end": 107, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-13-https://www.nytimes.com/2026/09/27/technology/ai-government-regulation.html:69-232", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-13-https://www.nytimes.com/2026/09/27/technology/ai-government-regulation.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/27/technology/ai-government-regulation.html", + "excerpt": "The gap between technology and policymaking has gotten wider than ever with artificial intelligence, leaving a global policy vacuum as A.I. models rapidly advance.", + "location": { + "start": 69, + "end": 232, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-14-https://www.nytimes.com/2026/09/27/business/members-of-technical-staff-artificial-intelligence.html:69-216", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-14-https://www.nytimes.com/2026/09/27/business/members-of-technical-staff-artificial-intelligence.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/27/business/members-of-technical-staff-artificial-intelligence.html", + "excerpt": "Use of the generic title has risen by 50 percent in the last year among employees at Anthropic, OpenAI and other A.I. companies, an analysis found.", + "location": { + "start": 69, + "end": 216, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-15-https://www.nytimes.com/2026/09/27/business/heres-your-new-house-it-comes-with-friends.html:47-233", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-15-https://www.nytimes.com/2026/09/27/business/heres-your-new-house-it-comes-with-friends.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/27/business/heres-your-new-house-it-comes-with-friends.html", + "excerpt": "Fresh pies. Book groups. Puzzle challenges. Developers of master-planned communities see fear of social isolation and desire for friends as a business opportunity. Welcome to Amity Lane.", + "location": { + "start": 47, + "end": 233, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-16-https://www.nytimes.com/2026/09/27/business/women-baseball-league.html:55-215", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-16-https://www.nytimes.com/2026/09/27/business/women-baseball-league.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/27/business/women-baseball-league.html", + "excerpt": "As a group of players from an all-women’s baseball league in the 1940s and ’50s gathered at a recent reunion, the future of women’s baseball looks bright again.", + "location": { + "start": 55, + "end": 215, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-17-https://www.nytimes.com/2026/09/26/business/media/trump-white-house-tv-pool-cnn.html:72-244", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-17-https://www.nytimes.com/2026/09/26/business/media/trump-white-house-tv-pool-cnn.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/26/business/media/trump-white-house-tv-pool-cnn.html", + "excerpt": "The White House TV pool went dark this past week amid a standoff between CNN and the president. Network leaders, however, believe an indefinite suspension may be untenable.", + "location": { + "start": 72, + "end": 244, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-18-https://www.nytimes.com/2026/09/26/business/dealbook/ai-law-discount-billable-hour.html:77-203", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-18-https://www.nytimes.com/2026/09/26/business/dealbook/ai-law-discount-billable-hour.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/26/business/dealbook/ai-law-discount-billable-hour.html", + "excerpt": "There’s growing pressure to move beyond the billable hour. But big firms aren’t ready to blow up their trusted business model.", + "location": { + "start": 77, + "end": 203, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-19-https://www.nytimes.com/2026/09/26/business/media/trump-white-house-press-ban.html:62-210", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-19-https://www.nytimes.com/2026/09/26/business/media/trump-white-house-press-ban.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/26/business/media/trump-white-house-press-ban.html", + "excerpt": "An abrupt decree, a network boycott, a legal intervention and an uneasy détente: This was no typical battle between a president and his press corps.", + "location": { + "start": 62, + "end": 210, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-20-https://www.nytimes.com/2026/09/26/business/trump-economy-midterm-elections.html:67-227", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-20-https://www.nytimes.com/2026/09/26/business/trump-economy-midterm-elections.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/26/business/trump-economy-midterm-elections.html", + "excerpt": "As the president extolled the strength of the economy on the global stage, Americans continued to feel the sting of rising prices stemming from the war in Iran.", + "location": { + "start": 67, + "end": 227, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-21-https://www.nytimes.com/2026/09/26/business/ball-screws-military-production.html:74-207", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-21-https://www.nytimes.com/2026/09/26/business/ball-screws-military-production.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/26/business/ball-screws-military-production.html", + "excerpt": "As the Trump administration scrambles to increase the production of munitions, these special screws are expensive and hard to source.", + "location": { + "start": 74, + "end": 207, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-22-https://www.nytimes.com/2026/09/25/business/media/white-house-cnn-air-force-one-ban.html:64-258", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-22-https://www.nytimes.com/2026/09/25/business/media/white-house-cnn-air-force-one-ban.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/media/white-house-cnn-air-force-one-ban.html", + "excerpt": "CNN won’t be able to join President Trump on a Saturday trip to Tennessee. He had banned CNN and two other news outlets from the White House, but a court order on Thursday restored their access.", + "location": { + "start": 64, + "end": 258, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-23-https://www.nytimes.com/2026/09/25/us/politics/ice-raids-kansas-texas-oklahoma-beef-prices.html:56-225", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-23-https://www.nytimes.com/2026/09/25/us/politics/ice-raids-kansas-texas-oklahoma-beef-prices.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/us/politics/ice-raids-kansas-texas-oklahoma-beef-prices.html", + "excerpt": "Cattle industry groups in Texas, Oklahoma and Kansas and two Republican senators said the Trump administration’s immigration enforcement actions were hurting operations.", + "location": { + "start": 56, + "end": 225, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-24-https://www.nytimes.com/2026/09/25/world/middleeast/un-companies-westbank-settlements.html:82-216", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-24-https://www.nytimes.com/2026/09/25/world/middleeast/un-companies-westbank-settlements.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/world/middleeast/un-companies-westbank-settlements.html", + "excerpt": "Israel condemned the database of firms tied to activity in the occupied West Bank as a “political tool” to advance “a smear campaign.”", + "location": { + "start": 82, + "end": 216, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-25-https://www.nytimes.com/2026/09/25/business/star-meat-delivery-recall-usda.html:70-182", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-25-https://www.nytimes.com/2026/09/25/business/star-meat-delivery-recall-usda.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/star-meat-delivery-recall-usda.html", + "excerpt": "Star Meat Delivery did not complete the requirements necessary to process food, the Agriculture Department said.", + "location": { + "start": 70, + "end": 182, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-26-https://www.nytimes.com/2026/09/25/business/sunkist-fruit-gems-candy-discontinued.html:77-179", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-26-https://www.nytimes.com/2026/09/25/business/sunkist-fruit-gems-candy-discontinued.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/sunkist-fruit-gems-candy-discontinued.html", + "excerpt": "The jelly candies were a staple at bar and bat mitzvahs and other Jewish celebrations for generations.", + "location": { + "start": 77, + "end": 179, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-27-https://www.nytimes.com/2026/09/25/business/fed-interest-rates-midterms.html:84-199", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-27-https://www.nytimes.com/2026/09/25/business/fed-interest-rates-midterms.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/fed-interest-rates-midterms.html", + "excerpt": "Financial markets are placing nearly 70 percent odds on the Federal Reserve raising interest rates in late October.", + "location": { + "start": 84, + "end": 199, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-28-https://www.nytimes.com/2026/09/25/business/media/trump-white-house-tv-pool-ban.html:56-164", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-28-https://www.nytimes.com/2026/09/25/business/media/trump-white-house-tv-pool-ban.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/media/trump-white-house-tv-pool-ban.html", + "excerpt": "Crews from CBS and CNN covered two events on Friday with President Trump and the Chinese leader, Xi Jinping.", + "location": { + "start": 56, + "end": 164, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-29-https://www.nytimes.com/2026/09/25/your-money/car-insurance-child-costs.html:77-211", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-29-https://www.nytimes.com/2026/09/25/your-money/car-insurance-child-costs.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/your-money/car-insurance-child-costs.html", + "excerpt": "Unlike health insurance, a parent’s auto policy has no age limit on when a child has to be removed. But it’s good to know the options.", + "location": { + "start": 77, + "end": 211, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-30-https://www.nytimes.com/2026/09/25/business/china-trump-xi-truce.html:48-189", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-30-https://www.nytimes.com/2026/09/25/business/china-trump-xi-truce.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/china-trump-xi-truce.html", + "excerpt": "Easing tensions with Washington gives Beijing room to tackle economic troubles at home while reducing its vulnerability to American pressure.", + "location": { + "start": 48, + "end": 189, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-31-https://www.nytimes.com/2026/09/25/business/dealbook/trump-xi-tech-ceos-state-dinner.html:31-173", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-31-https://www.nytimes.com/2026/09/25/business/dealbook/trump-xi-tech-ceos-state-dinner.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/dealbook/trump-xi-tech-ceos-state-dinner.html", + "excerpt": "The state visit by Xi Jinping, China’s top leader, so far has been long on pomp but short on substance on issues like artificial intelligence.", + "location": { + "start": 31, + "end": 173, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-32-https://www.nytimes.com/2026/09/25/business/trump-xi-jinping-chinese-executives.html:83-247", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-32-https://www.nytimes.com/2026/09/25/business/trump-xi-jinping-chinese-executives.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/trump-xi-jinping-chinese-executives.html", + "excerpt": "American chief executives turned out in force to see Xi Jinping, while their Chinese counterparts were absent, a sign of how drastically business ties have changed.", + "location": { + "start": 83, + "end": 247, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-33-https://www.nytimes.com/2026/09/25/business/energy-environment/trump-diesel-export-ban.html:51-225", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-33-https://www.nytimes.com/2026/09/25/business/energy-environment/trump-diesel-export-ban.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/energy-environment/trump-diesel-export-ban.html", + "excerpt": "President Trump has said he is considering restricting overseas sales of the fuel to bring down domestic prices, but experts say the policy will have many unintended effects.", + "location": { + "start": 51, + "end": 225, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-34-https://www.nytimes.com/2026/09/25/business/interest-rates-economy-markets-inflation-mortgages.html:63-213", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-34-https://www.nytimes.com/2026/09/25/business/interest-rates-economy-markets-inflation-mortgages.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/interest-rates-economy-markets-inflation-mortgages.html", + "excerpt": "History shows that the markets and the economy flourished when rates were even higher than today’s. But, our columnist notes, those times didn’t last.", + "location": { + "start": 63, + "end": 213, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-35-https://www.nytimes.com/2026/09/25/travel/climate-change-summer-travel-disruptions.html:48-197", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-35-https://www.nytimes.com/2026/09/25/travel/climate-change-summer-travel-disruptions.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/travel/climate-change-summer-travel-disruptions.html", + "excerpt": "Once dismissed as occasional inconveniences, climate-related disruptions are now an inescapable fixture of summer travel. How will travelers respond?", + "location": { + "start": 48, + "end": 197, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-36-https://www.nytimes.com/2026/09/25/business/power-grid-jonas-birgersson.html:64-210", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-36-https://www.nytimes.com/2026/09/25/business/power-grid-jonas-birgersson.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/power-grid-jonas-birgersson.html", + "excerpt": "Jonas Birgersson, a Swedish entrepreneur, wants to use decentralized networks modeled on the internet to transform how electricity is distributed.", + "location": { + "start": 64, + "end": 210, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-37-https://www.nytimes.com/2026/09/25/business/media/nyc-tiktok-instagram-shows.html:77-158", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-37-https://www.nytimes.com/2026/09/25/business/media/nyc-tiktok-instagram-shows.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/business/media/nyc-tiktok-instagram-shows.html", + "excerpt": "The streets of New York are a backdrop for countless TikTok and Instagram videos.", + "location": { + "start": 77, + "end": 158, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-38-https://www.nytimes.com/2026/09/25/style/mark-zuckerberg-fashion-glasses.html:40-105", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-38-https://www.nytimes.com/2026/09/25/style/mark-zuckerberg-fashion-glasses.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/25/style/mark-zuckerberg-fashion-glasses.html", + "excerpt": "Meta’s C.E.O. talks for the first time about his style evolution.", + "location": { + "start": 40, + "end": 105, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-39-https://www.nytimes.com/2026/09/24/business/economy/tech-executives-state-dinner-xi-trump.html:54-204", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-39-https://www.nytimes.com/2026/09/24/business/economy/tech-executives-state-dinner-xi-trump.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/24/business/economy/tech-executives-state-dinner-xi-trump.html", + "excerpt": "Prominent executives attended a dinner at the White House Thursday night, as the countries wrangled over tariffs, trade and potential A.I. regulation.", + "location": { + "start": 54, + "end": 204, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-40-https://www.nytimes.com/2026/09/24/business/massachusetts-draftkings-ai-gambling.html:64-212", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-40-https://www.nytimes.com/2026/09/24/business/massachusetts-draftkings-ai-gambling.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/24/business/massachusetts-draftkings-ai-gambling.html", + "excerpt": "In response to a New York Times report on DraftKings, the state’s gaming commission said it would evaluate how each company deployed the technology.", + "location": { + "start": 64, + "end": 212, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-41-https://www.nytimes.com/2026/09/24/business/media/trump-white-house-press-ban-tv-pool-conservative-news.html:78-175", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-41-https://www.nytimes.com/2026/09/24/business/media/trump-white-house-press-ban-tv-pool-conservative-news.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/24/business/media/trump-white-house-press-ban-tv-pool-conservative-news.html", + "excerpt": "Real America’s Voice and LindellTV have teamed up to provide a feed of daily presidential events.", + "location": { + "start": 78, + "end": 175, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-42-https://www.nytimes.com/2026/09/24/business/trump-xi-china-exports.html:63-174", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-42-https://www.nytimes.com/2026/09/24/business/trump-xi-china-exports.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/24/business/trump-xi-china-exports.html", + "excerpt": "The Trump administration, like its predecessors, has struggled to deal with China’s excess industrial capacity.", + "location": { + "start": 63, + "end": 174, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-43-https://www.nytimes.com/2026/09/24/business/media/letterboxd-sale-sony-a24-nyt.html:76-185", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-43-https://www.nytimes.com/2026/09/24/business/media/letterboxd-sale-sony-a24-nyt.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/24/business/media/letterboxd-sale-sony-a24-nyt.html", + "excerpt": "A24, Sony and The New York Times Company all registered their interest in acquiring the cinephile’s paradise.", + "location": { + "start": 76, + "end": 185, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-44-https://www.nytimes.com/2026/09/24/business/starbucks-store-closures.html:41-133", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-44-https://www.nytimes.com/2026/09/24/business/starbucks-store-closures.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/24/business/starbucks-store-closures.html", + "excerpt": "The company said the affected cafes did not meet financial or customer service expectations.", + "location": { + "start": 41, + "end": 133, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-45-https://www.nytimes.com/2026/09/24/business/meat-recall-usda-star-ad-foods.html:74-200", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-45-https://www.nytimes.com/2026/09/24/business/meat-recall-usda-star-ad-foods.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/24/business/meat-recall-usda-star-ad-foods.html", + "excerpt": "The raw pork, beef and goat products from Star Meat Delivery Inc. of North Carolina were distributed across the United States.", + "location": { + "start": 74, + "end": 200, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-46-https://www.nytimes.com/2026/09/24/business/mortgage-rates-7-percent.html:73-199", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-46-https://www.nytimes.com/2026/09/24/business/mortgage-rates-7-percent.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/24/business/mortgage-rates-7-percent.html", + "excerpt": "The average rate on a 30-year mortgage in the United States jumped to 7.03 percent, putting pressure on housing affordability.", + "location": { + "start": 73, + "end": 199, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-47-https://www.nytimes.com/live/2026/09/24/business/trump-cnn-politico-msnow-white-house/the-court-ordered-trump-to-restore-access-for-cnn-ms-now-and-politico-last-night:84-109", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-47-https://www.nytimes.com/live/2026/09/24/business/trump-cnn-politico-msnow-white-house/the-court-ordered-trump-to-restore-access-for-cnn-ms-now-and-politico-last-night", + "canonicalUrl": "https://www.nytimes.com/live/2026/09/24/business/trump-cnn-politico-msnow-white-house/the-court-ordered-trump-to-restore-access-for-cnn-ms-now-and-politico-last-night", + "excerpt": "Around 1:15 a.m. Eastern,", + "location": { + "start": 84, + "end": 109, + "line": 3, + "column": 1 + } + }, + { + "id": "passage:run-research.news-1790605880681-1:research.news:feed-48-https://www.nytimes.com/2026/09/24/movies/film-tax-credit-congress-bill.html:62-223", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-48-https://www.nytimes.com/2026/09/24/movies/film-tax-credit-congress-bill.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/24/movies/film-tax-credit-congress-bill.html", + "excerpt": "In response to an exodus of movie and TV production, a bill is being introduced in Congress that would use tax credits to subsidize filming in the United States.", + "location": { + "start": 62, + "end": 223, + "line": 3, + "column": 1 + } + } + ] +} \ No newline at end of file diff --git a/docs/acceptance/evidence-passage/finished.json b/docs/acceptance/evidence-passage/finished.json new file mode 100644 index 00000000..5a9c52d8 --- /dev/null +++ b/docs/acceptance/evidence-passage/finished.json @@ -0,0 +1,37 @@ +{ + "id": "research-064770ff-2e25-42dd-bc18-e93d72d4ff3e", + "symbol": "NVDA.US", + "startedAt": 1790605880470, + "plannedCapabilities": [ + "company.profile", + "market.quote", + "market.kline", + "company.valuation", + "company.financials", + "company.earnings", + "company.ratings", + "research.news", + "market.capitalFlow", + "portfolio.positions" + ], + "completedCapabilities": [ + "research.news" + ], + "failedCapabilities": [ + "company.profile", + "market.quote", + "market.kline", + "company.valuation", + "company.financials", + "company.earnings", + "company.ratings", + "market.capitalFlow", + "portfolio.positions" + ], + "status": "partial", + "locale": "en-US", + "recoveryCount": 0, + "reportId": "report-research-064770ff-2e25-42dd-bc18-e93d72d4ff3e", + "finishedAt": 1790605911615, + "recoverable": false +} \ No newline at end of file diff --git a/docs/acceptance/evidence-passage/model-usage.json b/docs/acceptance/evidence-passage/model-usage.json new file mode 100644 index 00000000..fb405dd0 --- /dev/null +++ b/docs/acceptance/evidence-passage/model-usage.json @@ -0,0 +1,16 @@ +{ + "model": "deepseek-v4-pro", + "usage": { + "prompt_tokens": 2068, + "completion_tokens": 2635, + "total_tokens": 4703, + "prompt_tokens_details": { + "cached_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 1926 + }, + "prompt_cache_hit_tokens": 0, + "prompt_cache_miss_tokens": 2068 + } +} \ No newline at end of file diff --git a/docs/acceptance/evidence-passage/retrieved-news.json b/docs/acceptance/evidence-passage/retrieved-news.json new file mode 100644 index 00000000..29af057a --- /dev/null +++ b/docs/acceptance/evidence-passage/retrieved-news.json @@ -0,0 +1,492 @@ +[ + { + "id": "feed-0-https://www.nytimes.com/2026/09/28/business/nvidia-stock-buyback.html", + "title": "Nvidia Adds $150 Billion to Massive Stock Buyback, the Largest Ever", + "summary": "The increase comes four months after the chip giant added $80 billion to its buyback program, bringing the total remaining authorized amount to $235 billion.", + "url": "https://www.nytimes.com/2026/09/28/business/nvidia-stock-buyback.html", + "timestamp": 1790605717, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-1-https://www.nytimes.com/2026/09/28/business/dealbook/ai-safety-risks-ipo.html", + "title": "Could A.I. Safety Risks Derail the Sector’s I.P.O. Prospects?", + "summary": "Big liability questions lie ahead for the artificial intelligence labs Anthropic and OpenAI as they push ahead with plans to go public.", + "url": "https://www.nytimes.com/2026/09/28/business/dealbook/ai-safety-risks-ipo.html", + "timestamp": 1790595492, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-2-https://www.nytimes.com/2026/09/28/business/china-us-summit-tariffs.html", + "title": "China and the U.S. Pledge to Cut Tariffs on $60 Billion in Goods", + "summary": "The two countries unveiled separate lists covering thousands of products as they seek to ease trade tensions and stabilize relations ahead of further negotiations.", + "url": "https://www.nytimes.com/2026/09/28/business/china-us-summit-tariffs.html", + "timestamp": 1790594907, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-3-https://www.nytimes.com/2026/09/28/business/oil-stocks-gas-prices.html", + "title": "Oil Prices Climb on Continued Impasse in U.S.-Iran Talks", + "summary": "President Trump rejected Iran’s proposal over the weekend, calling it “unacceptable,” but Iran said it was still waiting for “definitive news” from Washington.", + "url": "https://www.nytimes.com/2026/09/28/business/oil-stocks-gas-prices.html", + "timestamp": 1790603368, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-4-https://www.nytimes.com/2026/09/28/business/ai-scientists-protests.html", + "title": "How Scientists Can Shape Public Opinion Over A.I. Risks", + "summary": "Artificial intelligence workers may have leverage to make the world safer.", + "url": "https://www.nytimes.com/2026/09/28/business/ai-scientists-protests.html", + "timestamp": 1790586313, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-5-https://www.nytimes.com/2026/09/28/dining/forest-flavors-alpine-foraging.html", + "title": "Juniper Lattes? Pine Sodas? Food Is Taking Us Back to the Forest.", + "summary": "Drinks and snacks are being tweaked to evoke woods and meadows, on the theory that frazzled consumers want to reconnect with nature.", + "url": "https://www.nytimes.com/2026/09/28/dining/forest-flavors-alpine-foraging.html", + "timestamp": 1790604229, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-6-https://www.nytimes.com/2026/09/28/business/trump-tariffs-canada-diversify.html", + "title": "Trump’s Tariffs Push Canadian Companies to Look Past American Links", + "summary": "Shaken by tariffs and threats from the Trump administration, Canadian businesses are seeking markets and suppliers beyond the United States.", + "url": "https://www.nytimes.com/2026/09/28/business/trump-tariffs-canada-diversify.html", + "timestamp": 1790597015, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-7-https://www.nytimes.com/2026/09/28/us/politics/cotton-trump-commodity-markets-cftc.html", + "title": "Under Trump, Commodity Futures Trading Commission Scales Back Enforcement, Worrying Farmers", + "summary": "The fate of two cotton brokers highlights how the Commodity Futures Trading Commission has scaled back its regulatory actions not just for crypto cases but also for the kinds of cases it has long policed.", + "url": "https://www.nytimes.com/2026/09/28/us/politics/cotton-trump-commodity-markets-cftc.html", + "timestamp": 1790586142, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-8-https://www.nytimes.com/2026/09/28/arts/music/harry-styles-msg-ticket-prices-affordability.html", + "title": "Here’s What Fans Paid for Harry Styles Tickets at M.S.G.", + "summary": "What is a concert really worth? We asked 34 fans about the time, money and effort they spent getting tickets for the star’s New York City residency.", + "url": "https://www.nytimes.com/2026/09/28/arts/music/harry-styles-msg-ticket-prices-affordability.html", + "timestamp": 1790594465, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-9-https://www.nytimes.com/2026/09/28/business/smart-headlights-adaptive.html", + "title": "Smart Headlights Finally Trickle Down to Cars in the U.S.", + "summary": "Federal data shows that half of all traffic fatalities happen after dark, but only one-quarter of driving happens at night. So any help is welcome.", + "url": "https://www.nytimes.com/2026/09/28/business/smart-headlights-adaptive.html", + "timestamp": 1790586041, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-10-https://www.nytimes.com/2026/09/27/us/dee-brock-dead.html", + "title": "Dee Brock, Who Created the Dallas Cowboys Cheerleaders, Dies at 96", + "summary": "As the first director of the troupe, she helped transform it from a high school squad into a global phenomenon.", + "url": "https://www.nytimes.com/2026/09/27/us/dee-brock-dead.html", + "timestamp": 1790530878, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-11-https://www.nytimes.com/2026/09/27/obituaries/morris-ballen-dead.html", + "title": "Morris Ballen, Who Helped Spread Indie Artists’ Music, Dies at 88", + "summary": "Countless independent acts in every genre — punk, hip-hop, church choir — relied on him to create small runs of high-quality records and CDs.", + "url": "https://www.nytimes.com/2026/09/27/obituaries/morris-ballen-dead.html", + "timestamp": 1790529798, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-12-https://www.nytimes.com/2026/09/27/business/workplace-.html", + "title": "Why Won’t My Co-Workers Acknowledge My Birthday?", + "summary": "Plus: What to do when your boss is nit-picking your work.", + "url": "https://www.nytimes.com/2026/09/27/business/workplace-.html", + "timestamp": 1790499704, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-13-https://www.nytimes.com/2026/09/27/technology/ai-government-regulation.html", + "title": "As A.I. Accelerates, Governments Are Increasingly Being Left Behind", + "summary": "The gap between technology and policymaking has gotten wider than ever with artificial intelligence, leaving a global policy vacuum as A.I. models rapidly advance.", + "url": "https://www.nytimes.com/2026/09/27/technology/ai-government-regulation.html", + "timestamp": 1790508168, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-14-https://www.nytimes.com/2026/09/27/business/members-of-technical-staff-artificial-intelligence.html", + "title": "Job Titles Are Out. Now We’re All ‘Members of the Technical Staff.’", + "summary": "Use of the generic title has risen by 50 percent in the last year among employees at Anthropic, OpenAI and other A.I. companies, an analysis found.", + "url": "https://www.nytimes.com/2026/09/27/business/members-of-technical-staff-artificial-intelligence.html", + "timestamp": 1790499660, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-15-https://www.nytimes.com/2026/09/27/business/heres-your-new-house-it-comes-with-friends.html", + "title": "Here’s Your New House. It Comes With Friends.", + "summary": "Fresh pies. Book groups. Puzzle challenges. Developers of master-planned communities see fear of social isolation and desire for friends as a business opportunity. Welcome to Amity Lane.", + "url": "https://www.nytimes.com/2026/09/27/business/heres-your-new-house-it-comes-with-friends.html", + "timestamp": 1790499634, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-16-https://www.nytimes.com/2026/09/27/business/women-baseball-league.html", + "title": "Women Sluggers Are No Longer in a League of Their Own", + "summary": "As a group of players from an all-women’s baseball league in the 1940s and ’50s gathered at a recent reunion, the future of women’s baseball looks bright again.", + "url": "https://www.nytimes.com/2026/09/27/business/women-baseball-league.html", + "timestamp": 1790521580, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-17-https://www.nytimes.com/2026/09/26/business/media/trump-white-house-tv-pool-cnn.html", + "title": "TV Networks to Resume Trump Coverage, Even as White House Excludes CNN", + "summary": "The White House TV pool went dark this past week amid a standoff between CNN and the president. Network leaders, however, believe an indefinite suspension may be untenable.", + "url": "https://www.nytimes.com/2026/09/26/business/media/trump-white-house-tv-pool-cnn.html", + "timestamp": 1790540615, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-18-https://www.nytimes.com/2026/09/26/business/dealbook/ai-law-discount-billable-hour.html", + "title": "As A.I. Makes Law Firms More Efficient, Clients Ask: ‘Where’s My Discount?’", + "summary": "There’s growing pressure to move beyond the billable hour. But big firms aren’t ready to blow up their trusted business model.", + "url": "https://www.nytimes.com/2026/09/26/business/dealbook/ai-law-discount-billable-hour.html", + "timestamp": 1790458554, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-19-https://www.nytimes.com/2026/09/26/business/media/trump-white-house-press-ban.html", + "title": "A 7-Day Standoff That Said So Much About Trump and the Media", + "summary": "An abrupt decree, a network boycott, a legal intervention and an uneasy détente: This was no typical battle between a president and his press corps.", + "url": "https://www.nytimes.com/2026/09/26/business/media/trump-white-house-press-ban.html", + "timestamp": 1790523688, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-20-https://www.nytimes.com/2026/09/26/business/trump-economy-midterm-elections.html", + "title": "High Gas and Rising Mortgage Rates Trouble Trump as Midterms Near", + "summary": "As the president extolled the strength of the economy on the global stage, Americans continued to feel the sting of rising prices stemming from the war in Iran.", + "url": "https://www.nytimes.com/2026/09/26/business/trump-economy-midterm-elections.html", + "timestamp": 1790413326, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-21-https://www.nytimes.com/2026/09/26/business/ball-screws-military-production.html", + "title": "This Screw Is Needed for Missiles and Planes. Buying One Can Take Years.", + "summary": "As the Trump administration scrambles to increase the production of munitions, these special screws are expensive and hard to source.", + "url": "https://www.nytimes.com/2026/09/26/business/ball-screws-military-production.html", + "timestamp": 1790428766, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-22-https://www.nytimes.com/2026/09/25/business/media/white-house-cnn-air-force-one-ban.html", + "title": "White House Blocks CNN From Covering Trump’s Trip to Tennessee", + "summary": "CNN won’t be able to join President Trump on a Saturday trip to Tennessee. He had banned CNN and two other news outlets from the White House, but a court order on Thursday restored their access.", + "url": "https://www.nytimes.com/2026/09/25/business/media/white-house-cnn-air-force-one-ban.html", + "timestamp": 1790432206, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-23-https://www.nytimes.com/2026/09/25/us/politics/ice-raids-kansas-texas-oklahoma-beef-prices.html", + "title": "ICE Raids in Beef Country Raise Fears of Higher Prices", + "summary": "Cattle industry groups in Texas, Oklahoma and Kansas and two Republican senators said the Trump administration’s immigration enforcement actions were hurting operations.", + "url": "https://www.nytimes.com/2026/09/25/us/politics/ice-raids-kansas-texas-oklahoma-beef-prices.html", + "timestamp": 1790377304, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-24-https://www.nytimes.com/2026/09/25/world/middleeast/un-companies-westbank-settlements.html", + "title": "U.N. Releases New List of Companies It Says Do Business With Israeli Settlements", + "summary": "Israel condemned the database of firms tied to activity in the occupied West Bank as a “political tool” to advance “a smear campaign.”", + "url": "https://www.nytimes.com/2026/09/25/world/middleeast/un-companies-westbank-settlements.html", + "timestamp": 1790366136, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-25-https://www.nytimes.com/2026/09/25/business/star-meat-delivery-recall-usda.html", + "title": "Distributor of Recalled Meat Worked for Years Without Proper Permits", + "summary": "Star Meat Delivery did not complete the requirements necessary to process food, the Agriculture Department said.", + "url": "https://www.nytimes.com/2026/09/25/business/star-meat-delivery-recall-usda.html", + "timestamp": 1790364681, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-26-https://www.nytimes.com/2026/09/25/business/sunkist-fruit-gems-candy-discontinued.html", + "title": "Sunkist Fruit Gems, Staple Candy of Bar Mitzvah Tradition, Are Discontinued", + "summary": "The jelly candies were a staple at bar and bat mitzvahs and other Jewish celebrations for generations.", + "url": "https://www.nytimes.com/2026/09/25/business/sunkist-fruit-gems-candy-discontinued.html", + "timestamp": 1790463978, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-27-https://www.nytimes.com/2026/09/25/business/fed-interest-rates-midterms.html", + "title": "Fed Faces Prospect of Another Interest Rate Increase Just Before Midterm Elections", + "summary": "Financial markets are placing nearly 70 percent odds on the Federal Reserve raising interest rates in late October.", + "url": "https://www.nytimes.com/2026/09/25/business/fed-interest-rates-midterms.html", + "timestamp": 1790350696, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-28-https://www.nytimes.com/2026/09/25/business/media/trump-white-house-tv-pool-ban.html", + "title": "CNN and CBS Resume White House Coverage After Standoff", + "summary": "Crews from CBS and CNN covered two events on Friday with President Trump and the Chinese leader, Xi Jinping.", + "url": "https://www.nytimes.com/2026/09/25/business/media/trump-white-house-tv-pool-ban.html", + "timestamp": 1790353767, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-29-https://www.nytimes.com/2026/09/25/your-money/car-insurance-child-costs.html", + "title": "Adding a Child to Your Car Insurance? Here Are Ways to Hold Down the Costs.", + "summary": "Unlike health insurance, a parent’s auto policy has no age limit on when a child has to be removed. But it’s good to know the options.", + "url": "https://www.nytimes.com/2026/09/25/your-money/car-insurance-child-costs.html", + "timestamp": 1790342766, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-30-https://www.nytimes.com/2026/09/25/business/china-trump-xi-truce.html", + "title": "China’s Truce With Trump Buys It Valuable Time", + "summary": "Easing tensions with Washington gives Beijing room to tackle economic troubles at home while reducing its vulnerability to American pressure.", + "url": "https://www.nytimes.com/2026/09/25/business/china-trump-xi-truce.html", + "timestamp": 1790354301, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-31-https://www.nytimes.com/2026/09/25/business/dealbook/trump-xi-tech-ceos-state-dinner.html", + "title": "Trump, Xi and the Tech Moguls", + "summary": "The state visit by Xi Jinping, China’s top leader, so far has been long on pomp but short on substance on issues like artificial intelligence.", + "url": "https://www.nytimes.com/2026/09/25/business/dealbook/trump-xi-tech-ceos-state-dinner.html", + "timestamp": 1790335513, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-32-https://www.nytimes.com/2026/09/25/business/trump-xi-jinping-chinese-executives.html", + "title": "Xi Jinping Didn’t Bring a CEO Entourage. What That Says About China-US Relations.", + "summary": "American chief executives turned out in force to see Xi Jinping, while their Chinese counterparts were absent, a sign of how drastically business ties have changed.", + "url": "https://www.nytimes.com/2026/09/25/business/trump-xi-jinping-chinese-executives.html", + "timestamp": 1790360230, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-33-https://www.nytimes.com/2026/09/25/business/energy-environment/trump-diesel-export-ban.html", + "title": "Why a U.S. Diesel Export Ban May Not Lower Prices", + "summary": "President Trump has said he is considering restricting overseas sales of the fuel to bring down domestic prices, but experts say the policy will have many unintended effects.", + "url": "https://www.nytimes.com/2026/09/25/business/energy-environment/trump-diesel-export-ban.html", + "timestamp": 1790364123, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-34-https://www.nytimes.com/2026/09/25/business/interest-rates-economy-markets-inflation-mortgages.html", + "title": "Rising Interest Rates Are Causing Alarm. Here’s What to Know.", + "summary": "History shows that the markets and the economy flourished when rates were even higher than today’s. But, our columnist notes, those times didn’t last.", + "url": "https://www.nytimes.com/2026/09/25/business/interest-rates-economy-markets-inflation-mortgages.html", + "timestamp": 1790328934, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-35-https://www.nytimes.com/2026/09/25/travel/climate-change-summer-travel-disruptions.html", + "title": "How Climate Change Is Disrupting Summer Travel", + "summary": "Once dismissed as occasional inconveniences, climate-related disruptions are now an inescapable fixture of summer travel. How will travelers respond?", + "url": "https://www.nytimes.com/2026/09/25/travel/climate-change-summer-travel-disruptions.html", + "timestamp": 1790326915, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-36-https://www.nytimes.com/2026/09/25/business/power-grid-jonas-birgersson.html", + "title": "The World Needs Better Power Grids. Why Smaller May Be Better.", + "summary": "Jonas Birgersson, a Swedish entrepreneur, wants to use decentralized networks modeled on the internet to transform how electricity is distributed.", + "url": "https://www.nytimes.com/2026/09/25/business/power-grid-jonas-birgersson.html", + "timestamp": 1790353099, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-37-https://www.nytimes.com/2026/09/25/business/media/nyc-tiktok-instagram-shows.html", + "title": "TikTok and Instagram Creators Have Turned NYC Into an Unofficial Soundstage", + "summary": "The streets of New York are a backdrop for countless TikTok and Instagram videos.", + "url": "https://www.nytimes.com/2026/09/25/business/media/nyc-tiktok-instagram-shows.html", + "timestamp": 1790326822, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-38-https://www.nytimes.com/2026/09/25/style/mark-zuckerberg-fashion-glasses.html", + "title": "Mark Zuckerberg Is Trying Something On", + "summary": "Meta’s C.E.O. talks for the first time about his style evolution.", + "url": "https://www.nytimes.com/2026/09/25/style/mark-zuckerberg-fashion-glasses.html", + "timestamp": 1790366562, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-39-https://www.nytimes.com/2026/09/24/business/economy/tech-executives-state-dinner-xi-trump.html", + "title": "Tech Titans Mingle With Trump and Xi at State Dinner", + "summary": "Prominent executives attended a dinner at the White House Thursday night, as the countries wrangled over tariffs, trade and potential A.I. regulation.", + "url": "https://www.nytimes.com/2026/09/24/business/economy/tech-executives-state-dinner-xi-trump.html", + "timestamp": 1790296223, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-40-https://www.nytimes.com/2026/09/24/business/massachusetts-draftkings-ai-gambling.html", + "title": "Massachusetts Is Investigating Gambling Companies’ Use of A.I.", + "summary": "In response to a New York Times report on DraftKings, the state’s gaming commission said it would evaluate how each company deployed the technology.", + "url": "https://www.nytimes.com/2026/09/24/business/massachusetts-draftkings-ai-gambling.html", + "timestamp": 1790283028, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-41-https://www.nytimes.com/2026/09/24/business/media/trump-white-house-press-ban-tv-pool-conservative-news.html", + "title": "Conservative Outlets Offer Trump Feed While White House TV Pool Is Suspended", + "summary": "Real America’s Voice and LindellTV have teamed up to provide a feed of daily presidential events.", + "url": "https://www.nytimes.com/2026/09/24/business/media/trump-white-house-press-ban-tv-pool-conservative-news.html", + "timestamp": 1790276578, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-42-https://www.nytimes.com/2026/09/24/business/trump-xi-china-exports.html", + "title": "Trump Struggles to Find Leverage With Xi Over Chinese Exports", + "summary": "The Trump administration, like its predecessors, has struggled to deal with China’s excess industrial capacity.", + "url": "https://www.nytimes.com/2026/09/24/business/trump-xi-china-exports.html", + "timestamp": 1790271258, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-43-https://www.nytimes.com/2026/09/24/business/media/letterboxd-sale-sony-a24-nyt.html", + "title": "Letterboxd Sees Acquisition Interest From A24, Sony and the New York Times", + "summary": "A24, Sony and The New York Times Company all registered their interest in acquiring the cinephile’s paradise.", + "url": "https://www.nytimes.com/2026/09/24/business/media/letterboxd-sale-sony-a24-nyt.html", + "timestamp": 1790272154, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-44-https://www.nytimes.com/2026/09/24/business/starbucks-store-closures.html", + "title": "Starbucks to Close 250 Stores in the US", + "summary": "The company said the affected cafes did not meet financial or customer service expectations.", + "url": "https://www.nytimes.com/2026/09/24/business/starbucks-store-closures.html", + "timestamp": 1790272480, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-45-https://www.nytimes.com/2026/09/24/business/meat-recall-usda-star-ad-foods.html", + "title": "More Than 167,000 Pounds of Meat With False Inspection Mark Are Recalled", + "summary": "The raw pork, beef and goat products from Star Meat Delivery Inc. of North Carolina were distributed across the United States.", + "url": "https://www.nytimes.com/2026/09/24/business/meat-recall-usda-star-ad-foods.html", + "timestamp": 1790266504, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-46-https://www.nytimes.com/2026/09/24/business/mortgage-rates-7-percent.html", + "title": "Mortgage Rates Hit 7% as Iran War Fallout Crushes a Weak Housing Market", + "summary": "The average rate on a 30-year mortgage in the United States jumped to 7.03 percent, putting pressure on housing affordability.", + "url": "https://www.nytimes.com/2026/09/24/business/mortgage-rates-7-percent.html", + "timestamp": 1790290333, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-47-https://www.nytimes.com/live/2026/09/24/business/trump-cnn-politico-msnow-white-house/the-court-ordered-trump-to-restore-access-for-cnn-ms-now-and-politico-last-night", + "title": "The court ordered Trump to restore access for CNN, MS Now and Politico last night.", + "summary": "Around 1:15 a.m. Eastern,", + "url": "https://www.nytimes.com/live/2026/09/24/business/trump-cnn-politico-msnow-white-house/the-court-ordered-trump-to-restore-access-for-cnn-ms-now-and-politico-last-night", + "timestamp": 1790264529, + "symbols": [ + "NVDA.US" + ] + }, + { + "id": "feed-48-https://www.nytimes.com/2026/09/24/movies/film-tax-credit-congress-bill.html", + "title": "A Weary Hollywood Celebrates the Prospect of Federal Funding", + "summary": "In response to an exodus of movie and TV production, a bill is being introduced in Congress that would use tax credits to subsidize filming in the United States.", + "url": "https://www.nytimes.com/2026/09/24/movies/film-tax-credit-congress-bill.html", + "timestamp": 1790274642, + "symbols": [ + "NVDA.US" + ] + } +] \ No newline at end of file diff --git a/docs/acceptance/evidence-passage/verification.json b/docs/acceptance/evidence-passage/verification.json new file mode 100644 index 00000000..1a476ee1 --- /dev/null +++ b/docs/acceptance/evidence-passage/verification.json @@ -0,0 +1,19 @@ +{ + "mode": "live AgentKernel + ResearchService production path; real model over HTTP; production research.news capability", + "model": "deepseek-v4-pro", + "symbol": "NVDA.US", + "retrieval": { + "kind": "live public RSS feed", + "url": "https://rss.nytimes.com/services/xml/rss/nyt/Business.xml" + }, + "runId": "research-064770ff-2e25-42dd-bc18-e93d72d4ff3e", + "reportId": "report-research-064770ff-2e25-42dd-bc18-e93d72d4ff3e", + "runStatus": "partial", + "retrievedNewsCount": 49, + "evidenceRefCount": 1, + "passageCount": 49, + "allOffsetsRelocate": true, + "allEvidenceMappedToPassage": true, + "survivesReload": true, + "scope": "Passage-level evidence location only. Claim identity, bidirectional claim↔evidence mapping and verifier output are separate PRs and are not exercised here." +} \ No newline at end of file diff --git a/docs/evidence-passage-test-report.md b/docs/evidence-passage-test-report.md new file mode 100644 index 00000000..d6d24cac --- /dev/null +++ b/docs/evidence-passage-test-report.md @@ -0,0 +1,115 @@ +# Evidence passage location acceptance report + +Issue #13 asks that report evidence resolve to the **actual source passage** rather than a +document-level citation, and that a claim → source jump still land on that passage after the report +is reloaded. This report covers the **passage-location slice** of that work (PR #251). Claim +identity, the verifier verdicts and the bidirectional claim↔evidence mapping are separate PRs and +are explicitly out of scope below. + +## Live Deep Research E2E — PASS + +Date: 2026-09-28. Environment: Windows 10.0.26200.9550, Bun 1.4.2, Node 22.22.2. +Commit under test: `bbcb4de` — `feat(research): 记录证据的原始 passage 位置,重载后仍可定位`. + +Actual command, from the repository root, after loading the model credential into the process +environment: + +```sh +FINAGENT_EVIDENCE_NEWS_FEED='https://rss.nytimes.com/services/xml/rss/nyt/Business.xml' \ +FINAGENT_EVIDENCE_MODEL='deepseek-v4-pro' \ +FINAGENT_EVIDENCE_SYMBOL='NVDA.US' \ +DEEPSEEK_API_KEY=… \ +bun apps/electron/e2e/evidence-passage-live-acceptance.ts +``` + +No fixture sits anywhere on this path — the acceptance fails closed without a model credential and +without a reachable retrieval source: + +| Stage | Real component that ran | +| --- | --- | +| Retrieval | The **production** `research.news` capability (`createResearchNewsCapability`), with only its `getNews` fetcher injected to read a live HTTPS feed | +| Model | `deepseek-v4-pro` over the real streaming chat-completions API | +| Synthesis | The production `createAgentSynthesizer` driving the real `AgentKernel` | +| Assembly | The production `ResearchService` → `ResearchRunner` → `assembleReport` | +| Persistence | The real `ResearchReportRepository` over `JsonFileStore` | + +| Evidence | Observed result | +| --- | --- | +| Research run | `research-064770ff-2e25-42dd-bc18-e93d72d4ff3e` | +| Published report | `report-research-064770ff-2e25-42dd-bc18-e93d72d4ff3e` | +| Run status | `partial` — news succeeded; the nine capabilities absent from the injected registry are explicitly `unavailable` | +| Retrieved articles | 49 live articles (real HTTP response body retained as `news-feed.xml` during the run) | +| Recorded passages | 49 — one per retrieved article | +| Offsets relocate | every passage re-locates to the **same** position, and `sourceText.slice(start, end)` equals the recorded excerpt | +| evidence → passage | every `EvidenceRef` resolves to at least one passage | +| Survives reload | a **fresh** repository instance reads byte-identical passages and every one still relocates | +| Model usage | 2068 prompt + 2635 completion tokens (1926 of them reasoning) | + +A representative recorded passage (offsets are 0-based character offsets into +`"\n\n<summary>"`, hence `line: 3, column: 1`): + +```json +{ + "id": "passage:run-research.news-1790605880681-1:research.news:feed-0-https://www.nytimes.com/2026/09/28/business/nvidia-stock-buyback.html:69-226", + "runId": "run-research.news-1790605880681-1", + "capabilityId": "research.news", + "documentId": "feed-0-https://www.nytimes.com/2026/09/28/business/nvidia-stock-buyback.html", + "canonicalUrl": "https://www.nytimes.com/2026/09/28/business/nvidia-stock-buyback.html", + "excerpt": "The increase comes four months after the chip giant added $80 billion to its buyback program, bringing the total remaining authorized amount to $235 billion.", + "location": { "start": 69, "end": 226, "line": 3, "column": 1 } +} +``` + +Committed artifacts: + +- [Verification record](acceptance/evidence-passage/verification.json) +- [Final model-generated report](acceptance/evidence-passage/final-report.json) +- [Retrieved news payload](acceptance/evidence-passage/retrieved-news.json) +- [Run summary](acceptance/evidence-passage/finished.json) +- [Provider-reported model usage](acceptance/evidence-passage/model-usage.json) + +The report and news payload are test output, not independently verified financial analysis. + +## Automated checks + +| Executed command | Result | +| --- | --- | +| `bun test --isolate packages/shared/src/research packages/core --timeout 20000` | 185 pass / 0 fail; 540 assertions across 21 files | +| `tsc --noEmit` on `apps/electron/e2e/evidence-passage-live-acceptance.ts` | exit 0 | +| `tsc --noEmit` in the core, shared, i18n, ui and electron workspaces | all exit 0 | + +`packages/shared` as a whole still reports 22 environment-only failures (21 Longbridge provider +tests needing an external CLI, 1 Langfuse test needing an external endpoint). An A/B run with the +production code reverted produces the identical 22, so they are unrelated to this change; the PR +body carries that baseline. + +## Scope and limits + +Proven by this acceptance: + +- real network retrieval produces locatable passages; +- every recorded offset resolves back to the same position in the real source text, and the + recorded excerpt is exactly the text at that range; +- the evidence → passage direction is usable; +- all of the above survives persistence and a fresh repository reload. + +**Not** proven here, because the code does not exist yet: + +- **claim identity** and the bidirectional `claim → evidence_refs` / `evidence_ref → claim_ids` + mapping — PR #67, still open; +- **verifier output** — `claim-verifier.ts` (#90) merged as a standalone library with no callers; + nothing wires it into the report path; +- **report UI bidirectional jump** — unclaimed; +- **Longbridge as the retrieval source** — see below. + +Retrieval note: the acceptance environment had no Longbridge CLI and no Longbridge credentials, so +the capability's `getNews` fetcher was injected with a live public RSS feed. The production +Longbridge fetcher remains the capability's default and is exercised end to end by +`apps/electron/e2e/research-recovery-kernel.ts` (PR #80). Because `createResearchNewsCapability` +stamps `provenance.provider` as the literal `'longbridge'`, the checkpoint for this run records +`provider: "longbridge"` even though the fetcher was an RSS feed — a pre-existing quirk of the +manifest, not something this PR changes, but worth knowing before treating that field as +authoritative. + +The acceptance harness is opt-in and is not part of any CI gate; it needs a real model credential +and a reachable retrieval source to run.