Skip to content

Claim mining

Once an outline has themes, each theme needs claims — the grounded sentences that actually say something, each backed by evidence. The outline wizard offers four ways to populate a theme, and it is easy to conflate them. They differ on two axes: whether they reuse claims already in the corpus or draft new ones from full text, and how they gather their material.

Four claim-mining strategies: propose placements, find existing, generate claims, and mine from sources

Two strategies reuse claims that already exist (left); two synthesize new, grounded drafts from full text (right).

The four strategies

1. Propose placements — sort what you already have

propose_placements is deterministic and read-only. It takes every claim that is unplaced in the current board and homes each one into its nearest existing theme by cosine similarity to that theme's claim centroid. A claim that is not close enough to any theme stays unplaced rather than being force-fit. No model is called; nothing new is written until you accept the placements. This is the bulk "file the leftovers" move — best when the corpus already contains the claims and you just need them sorted.

Propose placements: unplaced claims auto-home into their nearest existing themes by similarity

2. Find existing — retrieve corpus claims for one theme

suggest_claims (the wizard's Find existing button) is also retrieval, but scoped to a single theme and driven by you. It ranks the corpus's existing claims by similarity to the theme and shows the top matches with their scores; you click the ones that belong. Where propose placements auto-sorts the whole unplaced pool at once, find existing surfaces candidates for one theme so you place them by hand.

Find existing: the corpus ranked by similarity to one theme, with the top matches surfaced to place by hand

3. Generate claims — synthesize new drafts from the theme's members

mine_claims_for_topic (the Generate claims button) is agentic. It reads the full text of the notes already gathered under the theme and synthesizes new grounded claim candidates for it — including counter-claims. These are drafts: each proposed claim carries its supporting members, and nothing is committed until you place it. Reach for this when the evidence is present in the theme's own members but no one has written the summarizing claim yet.

Generate claims: an agent synthesizes new grounded claim drafts from the theme's own member notes

4. Mine from sources — draft atomic claims from chosen papers

The Mine from sources flow is two steps. propose_mine_sources suggests candidate source papers worth mining for the theme (papers not necessarily already in it); then mine_atomic_claims reads the full text of the sources you select and extracts atomic, grounded claim drafts from them. Unlike generate claims, which works from material already in the theme, this pulls in new claims from sources you deliberately bring in.

Mine from sources: chosen source papers are read in full and atomic grounded claims are drafted from them

How they compare

Strategy Existing or new? Scope Agentic? You do
Propose placements Existing claims Whole unplaced pool → all themes No (vectors) Review the auto-homing
Find existing Existing claims One theme No (retrieval) Pick from ranked matches
Generate claims New drafts The theme's own members Yes Place the drafts you like
Mine from sources New drafts Selected external source papers Yes Choose sources, place drafts

The dialectical layer — contradictions, debates, and supersession

Populating themes with claims is only half the picture: a mature corpus also disagrees with itself, and those tensions are where the field is live. Three propose-only read functions in zettelkasten/claims.py surface that dialectic. Like the mining strategies above they write nothing — they emit proposals, and a confirmed edge is only ever authored through the gated claim producer (which honors ZK_PROPOSE_ONLY).

  • claim(action="discover_contradictions") finds candidate contradictions between the corpus's claims. It clusters the claim vectors for candidate recall (reusing propose_clusters), forms candidate pairs, and applies three deterministic guards before any LLM call: a subject-overlap gate (the two claims must share a concept tag, a linked concept note, or a cited work), a near-duplicate exclusion (cosine ≥ ~0.92 is a paraphrase, not a contradiction), and a similarity band (contradictions live related-but-not-identical). Survivors go to a conservative, injectable, write-free stance classifier restricted to contradicts/qualifies/responds-to/none that is told to answer none unless the two claims make opposing assertions about the same thing. A pair is proposed only when classified contradicts above a confidence floor (default 0.6, ZK_CONTRADICTION_CONFIDENCE_FLOOR) and, by default, with reverse-order (A↔B) agreement (ZK_CONTRADICTION_REQUIRE_SYMMETRY). It is precision-first: we would rather miss a contradiction than assert a false one. The subject-overlap gate is a recall-narrowing pre-filter — it only decides which pairs are cheap enough to be worth classifying; the injectable stance classifier owns the final contradicts/none verdict.
  • claim(action="debate_map") aggregates the stance edges (discovered and authored) into a dashboard-ready tension graph: nodes are claims sized by claim_strength and colored by claim_centrality; edges are the stance relations; camps are the connected components over contradicts edges (ignoring self-loop contradicts artifacts, which are not debates), each labelled contested, resolved, or thin (the debate is under-evidenced) — the same dialectical logic analyze_gaps uses. A camp is resolved only when one member is superseded by another member of the same camp: an external supersedes edge from a claim outside the debate does not resolve a genuinely contested camp.
  • claim(action="find_supersessions") proposes belief-revision supersedes edges. B supersedes A when B contradicts A and is stronger by a margin (default 0.15) and newer and their support sets overlap (a shared source or subject). Recency is judged on a single axis: if either side has a datable supporting-paper year the comparison is made on paper-years alone (a stronger-but-undated claim never supersedes an evidence-dated one on note-id), and only when neither side has a paper-year does it fall back to the note-id creation date. Self-loop contradicts artifacts are skipped. Each proposal carries the strength and recency deltas that justify it, and is never auto-written.

The opportunity layer — scanning the negative space

The gap analyzer (syllabus(action="gaps")claims.analyze_gaps) surfaces weaknesses attached to existing claims — the six default families evidential, dialectical, structural, coverage, comprehensiveness, and temporal. A second, opt-in set of six families in the leaf module zettelkasten/opportunities.py turns the same engine into an opportunity finder by scanning the negative space — the missing edges and unmatched nodes across the knowledge graph and across the memory↔zettelkasten boundary. They are all read-only / propose-only (a detector only returns a gap dict describing an opportunity and a suggested action — it never writes), and are emitted only when named in the gap_types filter, so the default scan is unchanged. To keep claims.py free of a synapse import (which would re-create a circular import), analyze_gaps does a lazy from zettelkasten import opportunities that fires only when one of these families is requested.

  • asymmetry — a practice memory-node with no aligned canon claim, or a canon claim with no aligned practice node. Read off the cross-store claim overlay: it is a two-sided set difference between the practice nodes (get_memory_source) and the claims (build_claim_index) versus the endpoints the overlay actually links. Each gap's anchor names the unmatched node and which side is missing. An absent/empty overlay yields nothing.
  • bridge — two dense clusters with (near-)zero connecting edges but high inter-cluster embedding similarity: a synthesis opportunity. Cluster-pair work is O(k²) over the small number of clusters (comparing centroids), never O(n²) over notes. The anchor carries a representative from each cluster so the frontend can draw a ghost edge.
  • crux — an unresolved central debate: a contested camp from the debate_map, ranked by strength × centrality. The action suggests resolving it via a supersedes decision.
  • orphaned_question — a question note with no incoming answering edge (no supports / responds-to backlink). The action suggests answering it.
  • void — a sparse-but-surrounded region of embedding space: a tiny cluster whose centroid hugs a dense one (an unasked question / underexplored area). Optionally named by an injected LLM, else a deterministic label.
  • transfer — a method/model note applied within one thematic cluster that structurally fits an adjacent cluster where it has not been tried.

Each family registers an entry in GAP_TYPE_WEIGHTS and _GAP_ACTIONABILITY, so its gaps carry the same bounded severity = salience × type-weight × actionability and rank alongside the claim-anchored families. Every detector is a pure function of an already-built ClaimIndex plus explicit inputs (clusters, vectors, the debate map, the overlay); the embedder/LLM is injected as an optional parameter (default None) so the scan is deterministic and testable without a model, and each detector returns [] rather than raising on a degenerate corpus (empty box, no claims, no overlay, a single cluster).

Everything stays grounded

None of these strategies fabricate free-floating text. The two retrieval strategies only ever surface claims that already exist (and were themselves grounded when written). The two agentic strategies produce drafts that carry their supporting members forward, so when a draft is committed it lands as a claim wired to its evidence — the same claim + quote contract described in Grounded extraction. Placing a claim in the wizard never bypasses that contract; it only decides where a claim lives in the outline.