From 33304877dc08be22aa8d9a48204e2951f5f01592 Mon Sep 17 00:00:00 2001 From: Admin Date: Tue, 29 Sep 2026 08:07:20 +0000 Subject: [PATCH] Sync story source audit visibility --- src/db.ts | 218 +++++++++++++++++++++++++++++------------------------- 1 file changed, 117 insertions(+), 101 deletions(-) diff --git a/src/db.ts b/src/db.ts index f963fa6..38af0f2 100644 --- a/src/db.ts +++ b/src/db.ts @@ -686,6 +686,104 @@ export async function listStoryThreads(args: { return result.rows; } +function buildStorySourceAudit(story: any) { + const nodes = Array.isArray(story.nodes) ? story.nodes : []; + const edges = Array.isArray(story.edges) ? story.edges : []; + + const evidenceBacked = nodes.filter((n: any) => + Boolean( + n.evidence_id && + n.snapshot_id && + n.provenance_source_id && + n.snapshot_kind === "source_document" && + n.evidence_kind === "source_document" + ) + ); + + const distinctSourceIds = Array.from(new Set( + evidenceBacked.map((n: any) => String(n.provenance_source_id)).filter(Boolean) + )); + const distinctGroups = Array.from(new Set( + evidenceBacked.map((n: any) => String(n.provenance_independence_group)).filter(Boolean) + )); + const publishers = Array.from(new Set( + evidenceBacked.map((n: any) => String(n.provenance_publisher)).filter(Boolean) + )); + const tierOneCount = evidenceBacked.filter((n: any) => + Number(n.provenance_authority_tier) === 1 + ).length; + + const evidentiaryTypes = new Set(["evidence","actual","forecast","trend","signal","metric"]); + const sourceTaggedButUnbacked = nodes + .filter((n: any) => evidentiaryTypes.has(n.node_type) && n.source_id && !n.evidence_id) + .map((n: any) => ({ id: n.id, title: n.title, node_type: n.node_type, source_id: n.source_id })); + + const claimTypes = new Set(["claim","counterclaim"]); + const evidenceNodeIds = new Set(evidenceBacked.map((n: any) => n.id)); + const supportingRelationships = new Set([ + "supports","contradicts","qualifies","forecast_for","actual_for","regional_breakdown_of" + ]); + + const unsupportedClaims = nodes + .filter((n: any) => claimTypes.has(n.node_type)) + .filter((claim: any) => !edges.some((edge: any) => + edge.to_node_id === claim.id && + evidenceNodeIds.has(edge.from_node_id) && + supportingRelationships.has(edge.relationship) + )) + .map((n: any) => ({ id: n.id, title: n.title, node_type: n.node_type })); + + const staleCutoff = Date.now() - 45 * 86_400_000; + const staleEvidence = evidenceBacked + .filter((n: any) => { + const value = n.snapshot_published_at ?? n.snapshot_retrieved_at; + const ts = value ? new Date(value).getTime() : NaN; + return Number.isFinite(ts) && ts < staleCutoff; + }) + .map((n: any) => ({ id: n.id, title: n.title, source_id: n.provenance_source_id })); + + const checks = { + at_least_two_independent_sources: distinctGroups.length >= 2, + at_least_two_evidence_backed_nodes: evidenceBacked.length >= 2, + at_least_one_tier_one_source: tierOneCount >= 1, + all_claims_have_direct_evidence: unsupportedClaims.length === 0, + no_source_tag_only_evidence_nodes: sourceTaggedButUnbacked.length === 0 + }; + + return { + robust: Object.values(checks).every(Boolean), + checks, + counts: { + evidence_backed_nodes: evidenceBacked.length, + distinct_sources: distinctSourceIds.length, + distinct_independence_groups: distinctGroups.length, + publishers: publishers.length, + tier_one_evidence_nodes: tierOneCount, + unsupported_claims: unsupportedClaims.length, + source_tag_only_nodes: sourceTaggedButUnbacked.length, + stale_evidence: staleEvidence.length + }, + sources: evidenceBacked.map((n: any) => ({ + node_id: n.id, + node_type: n.node_type, + title: n.title, + source_id: n.provenance_source_id, + publisher: n.provenance_publisher, + source_class: n.provenance_source_class, + independence_group: n.provenance_independence_group, + authority_tier: n.provenance_authority_tier, + evidence_id: n.evidence_id, + snapshot_id: n.snapshot_id, + snapshot_retrieved_at: n.snapshot_retrieved_at, + snapshot_published_at: n.snapshot_published_at, + url: n.evidence_url ?? n.signal_url + })), + unsupported_claims: unsupportedClaims, + source_tag_only_nodes: sourceTaggedButUnbacked, + stale_evidence: staleEvidence + }; +} + export async function getStoryThread(id: string) { const threadResult = await pool.query( `SELECT t.*, @@ -735,12 +833,17 @@ export async function getStoryThread(id: string) { ) ]); - return { + const story = { ...threadResult.rows[0], nodes: nodes.rows, edges: edges.rows, research_checks: checks.rows }; + + return { + ...story, + source_audit: buildStorySourceAudit(story) + }; } export async function addStoryNode( @@ -937,105 +1040,7 @@ export async function markStoryThreadStatus( export async function auditStorySources(threadId: string) { const story = await getStoryThread(threadId); if (!story) throw new Error("Story thread not found"); - - const nodes = Array.isArray(story.nodes) ? story.nodes : []; - const edges = Array.isArray(story.edges) ? story.edges : []; - - const evidenceBacked = nodes.filter((n: any) => - Boolean( - n.evidence_id && - n.snapshot_id && - n.provenance_source_id && - n.snapshot_kind === "source_document" && - n.evidence_kind === "source_document" - ) - ); - - const distinctSourceIds = Array.from(new Set( - evidenceBacked.map((n: any) => String(n.provenance_source_id)).filter(Boolean) - )); - const distinctGroups = Array.from(new Set( - evidenceBacked.map((n: any) => String(n.provenance_independence_group)).filter(Boolean) - )); - const publishers = Array.from(new Set( - evidenceBacked.map((n: any) => String(n.provenance_publisher)).filter(Boolean) - )); - const tierOneCount = evidenceBacked.filter((n: any) => - Number(n.provenance_authority_tier) === 1 - ).length; - - const evidentiaryTypes = new Set(["evidence","actual","forecast","trend","signal","metric"]); - const sourceTaggedButUnbacked = nodes - .filter((n: any) => evidentiaryTypes.has(n.node_type) && n.source_id && !n.evidence_id) - .map((n: any) => ({ id: n.id, title: n.title, node_type: n.node_type, source_id: n.source_id })); - - const claimTypes = new Set(["claim","counterclaim"]); - const evidenceNodeIds = new Set(evidenceBacked.map((n: any) => n.id)); - const supportingRelationships = new Set([ - "supports","contradicts","qualifies","forecast_for","actual_for","regional_breakdown_of" - ]); - - const unsupportedClaims = nodes - .filter((n: any) => claimTypes.has(n.node_type)) - .filter((claim: any) => !edges.some((edge: any) => - edge.to_node_id === claim.id && - evidenceNodeIds.has(edge.from_node_id) && - supportingRelationships.has(edge.relationship) - )) - .map((n: any) => ({ id: n.id, title: n.title, node_type: n.node_type })); - - const staleCutoff = Date.now() - 45 * 86_400_000; - const staleEvidence = evidenceBacked - .filter((n: any) => { - const value = n.snapshot_published_at ?? n.snapshot_retrieved_at; - const ts = value ? new Date(value).getTime() : NaN; - return Number.isFinite(ts) && ts < staleCutoff; - }) - .map((n: any) => ({ id: n.id, title: n.title, source_id: n.provenance_source_id })); - - const checks = { - at_least_two_independent_sources: distinctGroups.length >= 2, - at_least_two_evidence_backed_nodes: evidenceBacked.length >= 2, - at_least_one_tier_one_source: tierOneCount >= 1, - all_claims_have_direct_evidence: unsupportedClaims.length === 0, - no_source_tag_only_evidence_nodes: sourceTaggedButUnbacked.length === 0 - }; - - const robust = Object.values(checks).every(Boolean); - - return { - thread_id: threadId, - robust, - checks, - counts: { - evidence_backed_nodes: evidenceBacked.length, - distinct_sources: distinctSourceIds.length, - distinct_independence_groups: distinctGroups.length, - publishers: publishers.length, - tier_one_evidence_nodes: tierOneCount, - unsupported_claims: unsupportedClaims.length, - source_tag_only_nodes: sourceTaggedButUnbacked.length, - stale_evidence: staleEvidence.length - }, - sources: evidenceBacked.map((n: any) => ({ - node_id: n.id, - node_type: n.node_type, - title: n.title, - source_id: n.provenance_source_id, - publisher: n.provenance_publisher, - source_class: n.provenance_source_class, - independence_group: n.provenance_independence_group, - authority_tier: n.provenance_authority_tier, - evidence_id: n.evidence_id, - snapshot_id: n.snapshot_id, - snapshot_retrieved_at: n.snapshot_retrieved_at, - snapshot_published_at: n.snapshot_published_at, - url: n.evidence_url ?? n.signal_url - })), - unsupported_claims: unsupportedClaims, - source_tag_only_nodes: sourceTaggedButUnbacked, - stale_evidence: staleEvidence - }; + return { thread_id: threadId, ...(story as any).source_audit }; } export async function enrichEvidenceDocument(evidenceId: string) { @@ -1153,10 +1158,21 @@ export async function enrichStoryEvidenceDocuments(limit = 20) { const evidence = await enrichEvidenceDocument(row.evidence_id); enriched.push({ evidence_id: row.evidence_id, ok: true, snapshot_id: evidence?.snapshot_id ?? null }); } catch (error) { + const message = error instanceof Error ? error.message : String(error); + const errorHash = createHash("sha256").update(message).digest("hex").slice(0, 16); + await postSystemMessageOnce({ + dedupeKey: `evidence-enrich-error:${row.evidence_id}:${errorHash}`, + title: "Source document enrichment failed", + body: `Evidence ${row.evidence_id} could not be upgraded from discovery metadata to a frozen source document: ${message}`, + severity: "warning", + kind: "warning", + relatedType: "evidence", + relatedId: row.evidence_id + }); enriched.push({ evidence_id: row.evidence_id, ok: false, - error: error instanceof Error ? error.message : String(error) + error: message }); } }