diff --git a/README.md b/README.md index 65ddc9c..4dfa069 100644 --- a/README.md +++ b/README.md @@ -138,9 +138,10 @@ See [`docs/public-deployment.md`](docs/public-deployment.md) for the store check V2 builds one compact `ArticleIndex`, creates an adaptive `AnalysisPlan`, and runs a global `RetrievalCoordinator` against a shared source ledger and evidence graph. Fast, balanced, and deep -modes change passage, mission, concurrency, deadline, and output budgets. Evidence is validated -once before it can serve any report section, and sections only project the ledger; the side panel -never reconciles sources itself. +modes change passage, mission, concurrency, deadline, and output budgets. Providers return +candidate sources only; a bounded adjudication step maps exact candidate IDs to article claims and +relationships, and centralized validation is required before an assertion can serve any report +section. The side panel only projects validated ledger assertions. Exa uses bounded search requests with returned text/highlights. Native ChatGPT web search is one bounded global search workflow: its URL-attributed results are `search-summary` evidence and are diff --git a/apps/extension/entrypoints/offscreen/main.ts b/apps/extension/entrypoints/offscreen/main.ts index 15ac297..812ec15 100644 --- a/apps/extension/entrypoints/offscreen/main.ts +++ b/apps/extension/entrypoints/offscreen/main.ts @@ -2,6 +2,7 @@ import { createChatGPT } from "@opencoredev/loginwithchatgpt-ai"; import type { ChatGPTTokens, ReasoningEffort } from "@opencoredev/loginwithchatgpt-core"; import { analyzeArticle, + createModelEvidenceAdjudicator, retryArticleSections, type AnalysisArtifacts, type PipelineTelemetry, @@ -199,7 +200,7 @@ async function testSearchProvider( controller.signal, ); const first = await iterator[Symbol.asyncIterator]().next(); - const sourceCount = first.done ? 0 : first.value.cards.length; + const sourceCount = first.done ? 0 : first.value.candidates.length; if (sourceCount === 0) throw new Error("ChatGPT completed the request but did not return a web source."); return { available: true, sourceCount }; @@ -236,6 +237,7 @@ async function runJob( article: command.request.article, retriever, model, + adjudicator: createModelEvidenceAdjudicator(model), modelVersion: preferences.model, reasoningEffort: preferences.reasoningEffort, mode: preferences.mode, @@ -340,7 +342,7 @@ async function runRetryJob( reasoningEffort: "medium" as const, mode: "balanced" as const, }; - const { retriever } = await createRetriever( + const { model, retriever } = await createRetriever( jobId, preferences.model, preferences.reasoningEffort, @@ -349,6 +351,7 @@ async function runRetryJob( for await (const event of retryArticleSections({ artifacts, retriever, + adjudicator: createModelEvidenceAdjudicator(model), sections: command.sections, signal: controller.signal, onTelemetry: (telemetry) => logTelemetry(jobId, telemetry), diff --git a/apps/extension/entrypoints/sidepanel/AnalysisProgress.test.ts b/apps/extension/entrypoints/sidepanel/AnalysisProgress.test.ts index 37dd0df..6af23f3 100644 --- a/apps/extension/entrypoints/sidepanel/AnalysisProgress.test.ts +++ b/apps/extension/entrypoints/sidepanel/AnalysisProgress.test.ts @@ -14,6 +14,7 @@ describe("analysis pipeline progress", () => { ...state, phase: "retrieval", research: { + candidateCount: 3, completedMissions: 2, totalMissions: 4, acceptedSources: 3, diff --git a/apps/extension/entrypoints/sidepanel/App.tsx b/apps/extension/entrypoints/sidepanel/App.tsx index 9d755c8..6d8cfdf 100644 --- a/apps/extension/entrypoints/sidepanel/App.tsx +++ b/apps/extension/entrypoints/sidepanel/App.tsx @@ -1,4 +1,5 @@ import { + memo, useCallback, useEffect, useRef, @@ -37,6 +38,7 @@ import { isAnalysisActive, } from "./report-state"; import { ReportStore } from "./report-store"; +import type { ReportSectionKey } from "./report-store"; import { extensionMode, clearAnalysisLogs, @@ -359,6 +361,136 @@ export function ProvisionalCompassWarning() { ); } +function useReportSection(store: ReportStore, section: K) { + const subscribe = useCallback( + (listener: () => void) => store.subscribeSection(section, listener), + [section, store], + ); + const getSnapshot = useCallback(() => store.getSectionSnapshot(section), [section, store]); + return useSyncExternalStore(subscribe, getSnapshot, getSnapshot); +} + +const ConnectedCompass = memo(function ConnectedCompass({ store }: { store: ReportStore }) { + const section = useReportSection(store, "compass"); + return section.data ? ( + <> + + {section.status === "error" ? : null} + + ) : ( +
+ + + Political Spectrum + {section.status === "error" ? "Unavailable" : "Finding placement…"} + +
+ ); +}); + +const ConnectedBias = memo(function ConnectedBias({ store }: { store: ReportStore }) { + const section = useReportSection(store, "bias"); + const data = section.data; + return ( +
+ {data ? ( + data.readerCopy ? ( + [finding.id, finding.displayName]))} + citations={ + new Map( + (data.citations ?? []).map((citation: ReaderCitation) => [citation.id, citation]), + ) + } + /> + ) : ( + <> +

+ +

+ {data.findings.map((finding) => ( +
+

{finding.displayName}

+
+ +
+

+ +

+
+ ))} + + ) + ) : null} +
+ ); +}); + +const ConnectedJournalistContext = memo(function ConnectedJournalistContext({ + store, +}: { + store: ReportStore; +}) { + const section = useReportSection(store, "journalistContext"); + return ( +
+ {section.data ? : null} +
+ ); +}); + +const ConnectedEvidence = memo(function ConnectedEvidence({ + store, + section: sectionKey, + title, + id, +}: { + store: ReportStore; + section: "supporting" | "contradicting"; + title: string; + id: string; +}) { + const section = useReportSection(store, sectionKey); + return ( +
+ {section.data ? : null} +
+ ); +}); + +const ConnectedAdditionalContext = memo(function ConnectedAdditionalContext({ + store, +}: { + store: ReportStore; +}) { + const section = useReportSection(store, "additionalContext"); + return ( +
+ {section.data ? : null} +
+ ); +}); + +const ConnectedSourceList = memo(function ConnectedSourceList({ store }: { store: ReportStore }) { + const section = useReportSection(store, "sourceList"); + return ( +
+ {section.data ? : null} +
+ ); +}); + function AnalysisReport({ preferences, onOpenSettings }: AnalysisReportProps) { const reportStoreRef = useRef(null); reportStoreRef.current ??= new ReportStore(); @@ -558,111 +690,28 @@ function AnalysisReport({ preferences, onOpenSettings }: AnalysisReportProps) { ) : null} - {state.compass.data ? ( - <> - - {state.compass.status === "error" ? : null} - - ) : ( -
- - - Political Spectrum - - {state.compass.status === "error" ? "Unavailable" : "Finding placement…"} - - -
- )} - -
- {state.bias.data ? ( - state.bias.data.readerCopy ? ( - [finding.id, finding.displayName]), - ) - } - citations={ - new Map( - (state.bias.data.citations ?? []).map((citation: ReaderCitation) => [ - citation.id, - citation, - ]), - ) - } - /> - ) : ( - <> -

- -

- {state.bias.data.findings.map((finding) => ( -
-

{finding.displayName}

-
- -
-

- -

-
- ))} - - ) - ) : null} -
- -
- {state.journalistContext.data ? ( - - ) : null} -
- -
+ + + - {state.supporting.data ? : null} -
- -
+ - {state.contradicting.data ? : null} -
- -
- {state.additionalContext.data ? ( - - ) : null} -
+ /> + + -
- {state.sourceList.data ? : null} -
+ {/* Legacy section markup removed in favor of connected sections. + {state.compass.status === "error" ? "Unavailable" : "Finding placement…"} + + */}