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;
|
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) {
|
export async function getStoryThread(id: string) {
|
||||||
const threadResult = await pool.query(
|
const threadResult = await pool.query(
|
||||||
`SELECT t.*,
|
`SELECT t.*,
|
||||||
@ -735,12 +833,17 @@ export async function getStoryThread(id: string) {
|
|||||||
)
|
)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
const story = {
|
||||||
...threadResult.rows[0],
|
...threadResult.rows[0],
|
||||||
nodes: nodes.rows,
|
nodes: nodes.rows,
|
||||||
edges: edges.rows,
|
edges: edges.rows,
|
||||||
research_checks: checks.rows
|
research_checks: checks.rows
|
||||||
};
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...story,
|
||||||
|
source_audit: buildStorySourceAudit(story)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addStoryNode(
|
export async function addStoryNode(
|
||||||
@ -937,105 +1040,7 @@ export async function markStoryThreadStatus(
|
|||||||
export async function auditStorySources(threadId: string) {
|
export async function auditStorySources(threadId: string) {
|
||||||
const story = await getStoryThread(threadId);
|
const story = await getStoryThread(threadId);
|
||||||
if (!story) throw new Error("Story thread not found");
|
if (!story) throw new Error("Story thread not found");
|
||||||
|
return { thread_id: threadId, ...(story as any).source_audit };
|
||||||
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
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function enrichEvidenceDocument(evidenceId: string) {
|
export async function enrichEvidenceDocument(evidenceId: string) {
|
||||||
@ -1153,10 +1158,21 @@ export async function enrichStoryEvidenceDocuments(limit = 20) {
|
|||||||
const evidence = await enrichEvidenceDocument(row.evidence_id);
|
const evidence = await enrichEvidenceDocument(row.evidence_id);
|
||||||
enriched.push({ evidence_id: row.evidence_id, ok: true, snapshot_id: evidence?.snapshot_id ?? null });
|
enriched.push({ evidence_id: row.evidence_id, ok: true, snapshot_id: evidence?.snapshot_id ?? null });
|
||||||
} catch (error) {
|
} 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({
|
enriched.push({
|
||||||
evidence_id: row.evidence_id,
|
evidence_id: row.evidence_id,
|
||||||
ok: false,
|
ok: false,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user