Sync story source audit visibility
This commit is contained in:
parent
8c728615af
commit
33304877dc
218
src/db.ts
218
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user