Problem
#422 added article-level numeric confidence with decay. That's the right foundation, but it scores at too coarse a granularity: an article that mixes one well-sourced claim with one weakly-sourced claim gets a single blended number, and there's no way for the Q&A agent to weight individual claims when synthesizing answers.
The original #422 issue called for per-claim scoring; we deliberately deferred it because the harder problem isn't the math — it's concept identity. Without a way to recognize that "Redis is single-threaded" in article A is the same claim as "Redis runs on a single thread" in article B, reinforcement and contradiction can't fire at the claim level.
Prerequisites already in place (from #422)
Article.confidence_score and Article.last_reinforced_at columns
- Pure
confidence.py module with compute_confidence and apply_decay
_refresh_confidence_score() runs in Compiler.save_article()
- Decay surfaced on
ArticleResponse, GraphNode, CitationResponse
CompiledClaim Pydantic model with claim, categorical confidence, optional quote
What's missing
Three additions, in order:
1. Per-claim source attribution
CompiledClaim currently has no source_ids field — attribution is implicit via the article's ArticleSource join. Add:
class CompiledClaim(BaseModel):
claim: str
confidence: ConfidenceLevel # categorical, unchanged
quote: str | None
# New:
source_ids: list[str]
confidence_score: float # numeric, 0.0–1.0
last_reinforced_at: datetime
Compiler change: when extracting claims, ask the LLM to attach the specific source IDs each claim came from (subset of the article's sources). Persist claims to a new compiled_claim SQLModel table (currently key_claims is stored as JSON on the article).
2. Concept identity layer
Two viable approaches; recommend starting with the cheap one:
(a) Embedding-cluster IDs (recommended MVP)
- On compile, embed each claim text using the existing embedding pipeline (or add one if absent).
- Maintain a
concept_cluster table: each row is a representative embedding + canonical text + member claim IDs.
- New claims with cosine similarity > threshold (e.g. 0.85) join an existing cluster; otherwise spawn a new one.
concept_id becomes a foreign key on CompiledClaim.
(b) Canonical Concept entity (full version, defer)
3. Reinforcement on ingest
When a new source is compiled and produces a claim that joins an existing concept cluster:
- Append the new source ID to every
CompiledClaim in that cluster
- Update
last_reinforced_at = now() on those claims
- Recompute
confidence_score per claim using compute_confidence (already exists in confidence.py — works at any granularity)
- Article-level
confidence_score becomes a weighted aggregate over its claims (mean weighted by claim length or fixed equal weight).
Contradictions: when the LLM compiler emits a CONTRADICTS backlink, also tag which concept cluster the contradiction is about, so we can penalize confidence on the specific contradicted claims rather than the whole article.
Q&A integration
When qa_agent synthesizes an answer:
- Retrieve claims (not articles) by relevance
- Weight high-confidence claims more heavily in the prompt
- Surface per-claim confidence in
CitationResponse: {claim: "...", confidence: 0.82, sources: [...]}
- Optionally: explicit confidence language in answers ("supported by 4 sources" vs "based on a single 2022 article")
Out of scope (separate issues)
Testing approach
- Unit tests for embedding-cluster join logic at known thresholds
- Integration: ingest two paraphrased confirmations of the same fact, verify both attach to one cluster and confidence increases
- Integration: ingest a contradicting source, verify only the contradicted claim's confidence drops, not the whole article
- Snapshot test for Q&A
CitationResponse shape with per-claim fields
References
Complexity
Medium-high. Embedding pipeline + clustering + per-claim persistence is the bulk of the work; the math reuses #422's confidence.py verbatim. Estimate: 1–2 weeks for the MVP (approach 2a), separately from the full Concept entity (approach 2b, defer).
Problem
#422 added article-level numeric confidence with decay. That's the right foundation, but it scores at too coarse a granularity: an article that mixes one well-sourced claim with one weakly-sourced claim gets a single blended number, and there's no way for the Q&A agent to weight individual claims when synthesizing answers.
The original #422 issue called for per-claim scoring; we deliberately deferred it because the harder problem isn't the math — it's concept identity. Without a way to recognize that "Redis is single-threaded" in article A is the same claim as "Redis runs on a single thread" in article B, reinforcement and contradiction can't fire at the claim level.
Prerequisites already in place (from #422)
Article.confidence_scoreandArticle.last_reinforced_atcolumnsconfidence.pymodule withcompute_confidenceandapply_decay_refresh_confidence_score()runs inCompiler.save_article()ArticleResponse,GraphNode,CitationResponseCompiledClaimPydantic model withclaim, categoricalconfidence, optionalquoteWhat's missing
Three additions, in order:
1. Per-claim source attribution
CompiledClaimcurrently has nosource_idsfield — attribution is implicit via the article'sArticleSourcejoin. Add:Compiler change: when extracting claims, ask the LLM to attach the specific source IDs each claim came from (subset of the article's sources). Persist claims to a new
compiled_claimSQLModel table (currentlykey_claimsis stored as JSON on the article).2. Concept identity layer
Two viable approaches; recommend starting with the cheap one:
(a) Embedding-cluster IDs (recommended MVP)
concept_clustertable: each row is a representative embedding + canonical text + member claim IDs.concept_idbecomes a foreign key onCompiledClaim.(b) Canonical Concept entity (full version, defer)
ConceptSQLModel withid,canonical_name,aliases, etc.3. Reinforcement on ingest
When a new source is compiled and produces a claim that joins an existing concept cluster:
CompiledClaimin that clusterlast_reinforced_at = now()on those claimsconfidence_scoreper claim usingcompute_confidence(already exists inconfidence.py— works at any granularity)confidence_scorebecomes a weighted aggregate over its claims (mean weighted by claim length or fixed equal weight).Contradictions: when the LLM compiler emits a
CONTRADICTSbacklink, also tag which concept cluster the contradiction is about, so we can penalize confidence on the specific contradicted claims rather than the whole article.Q&A integration
When
qa_agentsynthesizes an answer:CitationResponse:{claim: "...", confidence: 0.82, sources: [...]}Out of scope (separate issues)
compute_confidence)Testing approach
CitationResponseshape with per-claim fieldsReferences
Complexity
Medium-high. Embedding pipeline + clustering + per-claim persistence is the bulk of the work; the math reuses #422's
confidence.pyverbatim. Estimate: 1–2 weeks for the MVP (approach 2a), separately from the full Concept entity (approach 2b, defer).