Skip to content

zettelkasten.synapse.synthesis

zettelkasten.synapse.synthesis

Claim-aligned cross-store synthesis: the WS4 layer over the synapse overlay.

The generic note-note overlay (:mod:zettelkasten.synapse.overlay, kind generic) stays as-is — a useful, register-agnostic substrate. This module adds a CLAIM-ALIGNED overlay + a grounded per-cell synthesis on top of it:

  • :func:build_claim_connections — a thin wrapper over :func:overlay.build_connections that restricts the two registers (canon = claim/finding ZK notes, preferring _cross synthesis claims; practice = decision/experiment/checkpoint memory entries), overfetches to offset the cross-register recall loss, persists to a SEPARATE file (.synapse/links/claim_links.json, kind claim), and ANNOTATES each surviving edge with claim_strength and a blended synthesis priority. The typer confidence stays the ONLY gate; claim_strength only weights the priority/synthesis order — it never drops an edge.

  • :func:synthesize_matrix / :func:synthesize_cell — a GROUNDED per-cell brief for an aligned (practice, canon-claim) pair. The brief says what the practice did, what the canon claim asserts, and the agreement/tension, CITING the memory entry id and the claim's RETRIEVED verbatim quotes and inventing nothing. Quotes are retrieved (never generated); synth is injectable (mirrors :func:zettelkasten.synapse.matrix._llm_synth) so it builds/tests with no LLM; an optional verifier checks the brief traces to the material. Only HIGH-VALUE cells (a strong claim met by a high-confidence relation, or ANY contradiction) are synthesized, to bound LLM cost.

  • :func:cross_store_contradictions — extract practice→canon contradicts edges from the claim overlay as real-world counter-evidence to feed :func:zettelkasten.claims.debate_map (so a claim's contested status can reflect practice, not just the literature).

Read-only w.r.t. .memory/ and .zettelkasten/; the only write is synapse's own .synapse/ overlay, exactly as the generic build.

priority_weight

priority_weight(claim_strength: 'float | None') -> float

Map a claim strength to a priority weight in [0.5, 1.0].

A weak (or unknown) claim keeps a positive FLOOR so its edge's blended priority is never zero — claim_strength only reorders synthesis, it must NEVER silently drop an edge. Unknown strength (claim not in the index) maps to a neutral 0.75.

Source code in zettelkasten/synapse/synthesis.py
def priority_weight(claim_strength: "float | None") -> float:
    """Map a claim strength to a priority weight in ``[0.5, 1.0]``.

    A weak (or unknown) claim keeps a positive FLOOR so its edge's blended
    priority is never zero — ``claim_strength`` only reorders synthesis, it must
    NEVER silently drop an edge. Unknown strength (claim not in the index) maps to
    a neutral ``0.75``.
    """
    if claim_strength is None:
        return 0.75
    s = max(0.0, min(1.0, claim_strength))
    return 0.5 + 0.5 * s

claim_strength_lookup

claim_strength_lookup(get_graph: GetGraph, projects: list[str] | None, graphs_dir: 'Any | None' = None) -> 'Callable[[str, str], float | None]'

A (zk_source, zk_id) -> claim_strength|None lookup over one claim context.

Builds the deterministic :mod:zettelkasten.claims context ONCE (lazily, on first lookup) over the whole scope, then resolves each canon endpoint's :func:zettelkasten.claims.claim_strength. Returns None for an endpoint that is not a scored claim (e.g. a finding with no evidence, or a note the index does not hold) — the caller treats that as neutral priority.

Source code in zettelkasten/synapse/synthesis.py
def claim_strength_lookup(
    get_graph: GetGraph, projects: list[str] | None, graphs_dir: "Any | None" = None
) -> "Callable[[str, str], float | None]":
    """A ``(zk_source, zk_id) -> claim_strength|None`` lookup over one claim context.

    Builds the deterministic :mod:`zettelkasten.claims` context ONCE (lazily, on
    first lookup) over the whole scope, then resolves each canon endpoint's
    :func:`zettelkasten.claims.claim_strength`. Returns ``None`` for an endpoint
    that is not a scored claim (e.g. a finding with no evidence, or a note the
    index does not hold) — the caller treats that as neutral priority.
    """
    cache: dict[str, Any] = {}

    def lookup(zk_source: str, zk_id: str) -> "float | None":
        if "ctx" not in cache:
            try:
                cache["ctx"] = _claims._build_context(
                    get_graph, project="", graph="", graphs_dir=graphs_dir,
                    namespace=None, localize=None,
                )
            except Exception:
                cache["ctx"] = None
        ctx = cache["ctx"]
        if ctx is None:
            return None
        index, _works, _citations, importance_map, _centrality = ctx
        rc = index.resolved.get((zk_source, zk_id))
        if rc is None:
            return None
        try:
            return _claims.claim_strength(rc, importance_map=importance_map)
        except Exception:
            return None

    return lookup

build_claim_connections

build_claim_connections(zk_get_graph: GetGraph, projects: list[str] | None = None, graphs_dir: 'Any | None' = None, typer: Any = None, per_node_k: int = CLAIM_PER_NODE_K, sim_threshold: float = 0.45, min_confidence: float = 0.55, max_pairs: int = 200, limit: int | None = None, overfetch: int = CLAIM_OVERFETCH, strength_fn: 'Callable[[str, str], float | None] | None' = None, stance_classify_fn: 'Any | None' = None) -> dict[str, Any]

Build (and persist) the CLAIM-ALIGNED overlay (kind claim).

Restricts candidates to canon claim/finding × practice decision/experiment/ checkpoint (preferring _cross synthesis claims), overfetches to offset the cross-register recall loss, types each pair with the injectable typer (LLM by default), keeps edges clearing min_confidence (the ONLY gate), and annotates each with claim_strength + a blended priority. Persists to .synapse/links/claim_links.json and returns the overlay dict.

PRECISION-FIRST CONTRADICTIONS. On the DEFAULT path (no injected typer) the LLM relation typer is wrapped by :func:_stance_gated_typer so any contradicts verdict must additionally clear the SHARED symmetric stance gate (:func:zettelkasten.stance.classify_contradiction, forward+reverse at :data:CONTRADICTION_CONFIDENCE_FLOOR) — the same precision bar WS3's intra-corpus discovery uses — before it can survive as a cross-store contradicts edge feeding the debate map. Because that gate is UNCONDITIONAL on this debate-feeding path, an injected typer (tests, the eval harness) MUST also supply a stance_classify_fn so its contradicts verdicts route through the same symmetric gate without an LLM; omitting it raises ValueError rather than shipping ungated contradictions into the debate map.

Source code in zettelkasten/synapse/synthesis.py
def build_claim_connections(
    zk_get_graph: GetGraph,
    projects: list[str] | None = None,
    graphs_dir: "Any | None" = None,
    typer: Any = None,
    per_node_k: int = CLAIM_PER_NODE_K,
    sim_threshold: float = 0.45,
    min_confidence: float = 0.55,
    max_pairs: int = 200,
    limit: int | None = None,
    overfetch: int = CLAIM_OVERFETCH,
    strength_fn: "Callable[[str, str], float | None] | None" = None,
    stance_classify_fn: "Any | None" = None,
) -> dict[str, Any]:
    """Build (and persist) the CLAIM-ALIGNED overlay (kind ``claim``).

    Restricts candidates to canon claim/finding × practice decision/experiment/
    checkpoint (preferring ``_cross`` synthesis claims), overfetches to offset the
    cross-register recall loss, types each pair with the injectable ``typer``
    (LLM by default), keeps edges clearing ``min_confidence`` (the ONLY gate), and
    annotates each with ``claim_strength`` + a blended ``priority``. Persists to
    ``.synapse/links/claim_links.json`` and returns the overlay dict.

    PRECISION-FIRST CONTRADICTIONS. On the DEFAULT path (no injected ``typer``) the
    LLM relation typer is wrapped by :func:`_stance_gated_typer` so any
    ``contradicts`` verdict must additionally clear the SHARED symmetric stance gate
    (:func:`zettelkasten.stance.classify_contradiction`, forward+reverse at
    :data:`CONTRADICTION_CONFIDENCE_FLOOR`) — the same precision bar WS3's
    intra-corpus discovery uses — before it can survive as a cross-store
    ``contradicts`` edge feeding the debate map. Because that gate is UNCONDITIONAL
    on this debate-feeding path, an injected ``typer`` (tests, the eval harness) MUST
    also supply a ``stance_classify_fn`` so its ``contradicts`` verdicts route
    through the same symmetric gate without an LLM; omitting it raises
    ``ValueError`` rather than shipping ungated contradictions into the debate map.
    """
    enrich = _make_strength_enricher(graphs_dir, strength_fn)
    # The symmetric stance gate is UNCONDITIONAL on this debate-feeding build path:
    # this overlay's ``contradicts`` edges feed :func:`cross_store_contradictions` →
    # the debate map, so ungated ``contradicts`` must NEVER be able to reach it. The
    # default LLM typer is always wrapped. An INJECTED typer may emit ``contradicts``
    # too, so it MUST come with a ``stance_classify_fn`` to route those verdicts
    # through the same gate — otherwise we refuse rather than silently ship ungated
    # contradictions. Deterministic LLM-free tests pass a stance_classify_fn
    # alongside their typer.
    if typer is None:
        typer = _stance_gated_typer(_overlay.llm_typer, stance_classify_fn)
    else:
        if stance_classify_fn is None:
            raise ValueError(
                "build_claim_connections: an injected `typer` can emit `contradicts` "
                "edges that feed the debate map, so a `stance_classify_fn` MUST also "
                "be supplied to route them through the shared symmetric stance gate. "
                "Pass a deterministic stance_classify_fn alongside the typer, or leave "
                "`typer=None` to use the default gated LLM typer."
            )
        typer = _stance_gated_typer(typer, stance_classify_fn)
    return _overlay.build_connections(
        zk_get_graph, projects=projects, graphs_dir=graphs_dir, typer=typer,
        per_node_k=per_node_k, sim_threshold=sim_threshold, min_confidence=min_confidence,
        max_pairs=max_pairs, limit=limit, kind=CLAIM_OVERLAY_KIND,
        canon_types=CANON_TYPES, practice_types=PRACTICE_TYPES, overfetch=overfetch,
        enrich_edges=enrich,
    )

is_high_value

is_high_value(relation: str, claim_strength: 'float | None', confidence: float) -> bool

Whether a cell is worth an (LLM) grounded brief.

ANY contradiction qualifies (it is candidate real-world counter-evidence); otherwise the canon claim must be strong AND the relation high-confidence.

Source code in zettelkasten/synapse/synthesis.py
def is_high_value(relation: str, claim_strength: "float | None", confidence: float) -> bool:
    """Whether a cell is worth an (LLM) grounded brief.

    ANY contradiction qualifies (it is candidate real-world counter-evidence);
    otherwise the canon claim must be strong AND the relation high-confidence.
    """
    if relation == "contradicts":
        return True
    s = claim_strength if claim_strength is not None else 0.0
    return s >= STRONG_CLAIM_FLOOR and confidence >= HIGH_CONF_FLOOR

verify_brief

verify_brief(brief: str, material: dict[str, Any]) -> bool

Deterministic TRACEABILITY check: does the brief trace to the material?

A True result is a traceability signal — every numeric token and quoted span in the brief traces to the provided material AND the memory id is cited — NOT a semantic-entailment guarantee. It answers "did the brief invent specifics absent from the material?", never "is the brief's meaning correct?". Two whole classes of error are therefore explicitly OUT OF SCOPE and pass this check (the brief is FLAGGED downstream, never rewritten):

  • lexical MEANING-INVERSION — asserting the opposite with in-vocabulary words ("momentum DECREASES returns" when the material says it increases them);
  • UNIT-VARIANT restatement — an equivalent figure in different units (a brief saying 2.5% when the material says 250bps);
  • spelled MULTIPLIER / FRACTION / ORDINAL magnitudes — the spelled-number check only covers cardinals combined with a magnitude word or a statistic unit (see (b)); it does NOT recognise multipliers ("double", "tenfold"), fractions ("a quarter", "half", "a third") or ordinals used as magnitudes ("a fourth"), so a fabricated figure phrased that way is not traced by the numeric gate.

Requirements, all conservative and LLM-free:

  • (a) the brief CITES the memory entry id verbatim;
  • (b) NUMERIC faithfulness — every numeric/statistic token in the brief (percentages, basis points, p-values, plain numbers) also appears in the material. SPELLED-out numbers are traced ONLY when they carry a magnitude word or a statistic unit ("four hundred basis points", "twenty percent"): such a span must trace to the material as its digit equivalent or the same spelled phrase. A STANDALONE bare cardinal ("one of our decisions", "the first experiment") is ordinary prose, not a figure, and is deliberately NOT required to trace — so this catches spelled fabricated magnitudes, but does not treat every number-word as a statistic. The cited memory id is handled SEPARATELY: its literal string must appear in the brief (a), but its own digits are neither required to trace nor counted as "material numbers" — a hex id like deci-a7688a22 must never lend its hash digits ({'22', '7688'}) to a fabricated brief integer;
  • (c) any explicitly quoted span in the brief is a verbatim substring of a provided quote body;
  • (d) at least _TRACE_FLOOR of the brief's content words appear in the material's vocabulary (memory summary/title + claim text + retrieved quotes + relation).
Source code in zettelkasten/synapse/synthesis.py
def verify_brief(brief: str, material: dict[str, Any]) -> bool:
    """Deterministic TRACEABILITY check: does the brief trace to the material?

    A ``True`` result is a **traceability** signal — every numeric token and quoted
    span in the brief traces to the provided material AND the memory id is cited —
    NOT a semantic-entailment guarantee. It answers "did the brief invent specifics
    absent from the material?", never "is the brief's meaning correct?". Two whole
    classes of error are therefore explicitly OUT OF SCOPE and pass this check (the
    brief is FLAGGED downstream, never rewritten):

    * lexical MEANING-INVERSION — asserting the opposite with in-vocabulary words
      ("momentum DECREASES returns" when the material says it increases them);
    * UNIT-VARIANT restatement — an equivalent figure in different units (a brief
      saying ``2.5%`` when the material says ``250bps``);
    * spelled MULTIPLIER / FRACTION / ORDINAL magnitudes — the spelled-number check
      only covers cardinals combined with a magnitude word or a statistic unit (see
      (b)); it does NOT recognise multipliers ("double", "tenfold"), fractions
      ("a quarter", "half", "a third") or ordinals used as magnitudes ("a fourth"),
      so a fabricated figure phrased that way is not traced by the numeric gate.

    Requirements, all conservative and LLM-free:

    * (a) the brief CITES the memory entry id verbatim;
    * (b) NUMERIC faithfulness — every numeric/statistic token in the brief
      (percentages, basis points, p-values, plain numbers) also appears in the
      material. SPELLED-out numbers are traced ONLY when they carry a magnitude word
      or a statistic unit ("four hundred basis points", "twenty percent"): such a
      span must trace to the material as its digit equivalent or the same spelled
      phrase. A STANDALONE bare cardinal ("one of our decisions", "the first
      experiment") is ordinary prose, not a figure, and is deliberately NOT required
      to trace — so this catches spelled fabricated *magnitudes*, but does not treat
      every number-word as a statistic.
      The cited memory id is handled SEPARATELY: its literal string must appear in
      the brief (a), but its own digits are neither required to trace nor counted as
      "material numbers" — a hex id like ``deci-a7688a22`` must never lend its hash
      digits ({'22', '7688'}) to a fabricated brief integer;
    * (c) any explicitly quoted span in the brief is a verbatim substring of a
      provided quote body;
    * (d) at least ``_TRACE_FLOOR`` of the brief's content words appear in the
      material's vocabulary (memory summary/title + claim text + retrieved quotes +
      relation).
    """
    brief = (brief or "").strip()
    if not brief:
        return False
    memory_id = str(material.get("memory_id", "")).strip()
    if memory_id and memory_id not in brief:
        return False

    quote_bodies = [(q.get("body", "") or "") for q in (material.get("quotes", []) or [])]

    # (b) Numeric/statistic faithfulness. The cited memory id is EXCLUDED from both
    # sides: it is stripped from the brief so its own digits are not required to
    # trace (citing ``deci-a7688a22`` must not introduce brief numbers 22/7688), and
    # it is kept OUT of the material corpus so a real hex-id hash can never supply a
    # "material number" that a fabricated brief integer rides on.
    brief_no_id = brief.replace(memory_id, " ") if memory_id else brief
    material_number_blob = "\n".join([
        material.get("memory_summary", "") or "",
        material.get("memory_title", "") or "",
        material.get("claim", "") or "",
        material.get("zk_title", "") or "",
        *quote_bodies,
    ])
    material_numbers = _numeric_tokens(material_number_blob)
    if any(tok not in material_numbers for tok in _numeric_tokens(brief_no_id)):
        return False
    # Spelled-out numbers: each spelled span must trace as a digit equivalent (in the
    # material's number tokens) or as the same spelled phrase (in the material text).
    material_blob_norm = re.sub(r"\s+", " ", material_number_blob).strip().lower()
    for norm_span, digit_cands in _spelled_number_spans(brief_no_id):
        if any(c in material_numbers for c in digit_cands):
            continue
        if norm_span in material_blob_norm:
            continue
        return False

    # (c) Quoted-span faithfulness: an explicitly quoted span must be a verbatim
    # substring of some provided quote body (case-insensitive).
    quotes_blob = "\n".join(quote_bodies).lower()
    for span in _QUOTED_SPAN_RE.findall(brief):
        if span.strip().lower() not in quotes_blob:
            return False

    # (d) Lexical coverage over content words.
    vocab = _content_words(material.get("memory_summary", ""))
    vocab |= _content_words(material.get("memory_title", ""))
    vocab |= _content_words(material.get("claim", ""))
    vocab |= _content_words(material.get("zk_title", ""))
    vocab.add((material.get("relation") or "").lower())
    for q in material.get("quotes", []) or []:
        vocab |= _content_words(q.get("body", ""))
    brief_words = _content_words(brief)
    if not brief_words:
        return False
    covered = sum(1 for w in brief_words if w in vocab)
    return (covered / len(brief_words)) >= _TRACE_FLOOR

synthesize_cell

synthesize_cell(material: dict[str, Any], synth: 'CellSynthesizer | None' = None, *, verify: bool = True, verify_fn: 'BriefVerifier | None' = None) -> dict[str, Any]

Produce (and optionally verify) a grounded brief for one cell's material.

Returns {brief, verified, quote_ids}. synth is injectable (default is a tool-free LLM pass that degrades to ""); verify_fn defaults to the deterministic :func:verify_brief. A brief that fails verification is kept but flagged verified=False (surfaced, never silently dropped).

Source code in zettelkasten/synapse/synthesis.py
def synthesize_cell(
    material: dict[str, Any],
    synth: "CellSynthesizer | None" = None,
    *,
    verify: bool = True,
    verify_fn: "BriefVerifier | None" = None,
) -> dict[str, Any]:
    """Produce (and optionally verify) a grounded brief for one cell's material.

    Returns ``{brief, verified, quote_ids}``. ``synth`` is injectable (default is
    a tool-free LLM pass that degrades to ``""``); ``verify_fn`` defaults to the
    deterministic :func:`verify_brief`. A brief that fails verification is kept
    but flagged ``verified=False`` (surfaced, never silently dropped).
    """
    synth = synth or _llm_cell_synth
    brief = (synth(material) or "").strip()
    verifier = verify_fn or verify_brief
    verified = bool(brief) and (verifier(brief, material) if verify else True)
    return {
        "brief": brief,
        "verified": verified,
        "quote_ids": [q.get("id") for q in (material.get("quotes", []) or [])],
    }

synthesize_matrix

synthesize_matrix(lens: dict[str, Any], material_fn: CellMaterialFn, synth: 'CellSynthesizer | None' = None, *, verify: bool = True, verify_fn: 'BriefVerifier | None' = None) -> dict[str, Any]

Add grounded per-cell briefs to a claim-overlay matrix lens (HIGH-VALUE only).

For each high-value cell (:func:is_high_value), material_fn(cell) returns the grounded material (memory summary + claim + retrieved verbatim quotes) or None to skip; :func:synthesize_cell then produces the brief. Purely additive: returns a lens copy with a cell_synthesis list. material_fn and synth are injectable so this builds/tests with no claim index or LLM.

Source code in zettelkasten/synapse/synthesis.py
def synthesize_matrix(
    lens: dict[str, Any],
    material_fn: CellMaterialFn,
    synth: "CellSynthesizer | None" = None,
    *,
    verify: bool = True,
    verify_fn: "BriefVerifier | None" = None,
) -> dict[str, Any]:
    """Add grounded per-cell briefs to a claim-overlay matrix lens (HIGH-VALUE only).

    For each high-value cell (:func:`is_high_value`), ``material_fn(cell)`` returns
    the grounded material (memory summary + claim + retrieved verbatim quotes) or
    ``None`` to skip; :func:`synthesize_cell` then produces the brief. Purely
    additive: returns a lens copy with a ``cell_synthesis`` list. ``material_fn``
    and ``synth`` are injectable so this builds/tests with no claim index or LLM.
    """
    out_cells: list[dict[str, Any]] = []
    for cell in lens.get("cells", []):
        relation = cell.get("relation") or ""
        confidence = float(cell.get("confidence", 0.0) or 0.0)
        strength = cell.get("claim_strength")
        if not is_high_value(relation, strength, confidence):
            continue
        material = material_fn(cell)
        if material is None:
            continue
        result = synthesize_cell(material, synth, verify=verify, verify_fn=verify_fn)
        out_cells.append({
            "memory_id": cell.get("memory_id"),
            "zk_id": cell.get("zk_id"),
            "zk_source": cell.get("zk_source"),
            "relation": relation,
            "confidence": cell.get("confidence"),
            "claim_strength": strength,
            "priority": cell.get("priority"),
            **result,
        })
    lens = dict(lens)
    lens["cell_synthesis"] = out_cells
    lens["synthesized_cells"] = len(out_cells)
    return lens

claim_material_provider

claim_material_provider(zk_get_graph: GetGraph, projects: list[str] | None = None, graphs_dir: 'Any | None' = None) -> CellMaterialFn

Default material_fn: assemble a cell's grounded material from the stores.

Builds the claim context + memory source ONCE (lazily) and, per cell, resolves the memory entry's summary and the canon claim's top RETRIEVED verbatim quotes (from :attr:ResolvedClaim.supporting_quotes, strongest first). Returns None when the canon endpoint is not a scored claim in scope.

Source code in zettelkasten/synapse/synthesis.py
def claim_material_provider(
    zk_get_graph: GetGraph,
    projects: list[str] | None = None,
    graphs_dir: "Any | None" = None,
) -> CellMaterialFn:
    """Default ``material_fn``: assemble a cell's grounded material from the stores.

    Builds the claim context + memory source ONCE (lazily) and, per cell, resolves
    the memory entry's summary and the canon claim's top RETRIEVED verbatim quotes
    (from :attr:`ResolvedClaim.supporting_quotes`, strongest first). Returns
    ``None`` when the canon endpoint is not a scored claim in scope.
    """
    _sources, get_graph = resolve_scope(zk_get_graph, projects=projects, graphs_dir=graphs_dir)
    cache: dict[str, Any] = {}

    def _ctx() -> Any:
        if "ctx" not in cache:
            try:
                cache["ctx"] = _claims._build_context(
                    get_graph, project="", graph="", graphs_dir=graphs_dir,
                    namespace=None, localize=None,
                )
            except Exception:
                cache["ctx"] = None
        return cache["ctx"]

    def material_fn(cell: dict[str, Any]) -> "dict[str, Any] | None":
        ctx = _ctx()
        if ctx is None:
            return None
        index, _works, _citations, importance_map, _centrality = ctx
        zk_source = cell.get("zk_source") or ""
        zk_id = cell.get("zk_id") or ""
        # Resolve the canon claim by its EXACT (zk_source, zk_id) key. The matrix
        # claim cells carry zk_source (:func:`matrix.build_matrix_lens`), so we must
        # NOT scan the index by bare id — the same note id can exist in multiple
        # in-scope graphs, and a bare-id scan would bind to an arbitrary source.
        rc = index.resolved.get((zk_source, zk_id)) if zk_source else None
        if rc is None:
            return None
        try:
            mem = get_graph(MEMORY_SOURCE)
            mnote = mem.notes.get(cell.get("memory_id"))
        except Exception:
            mnote = None
        quotes = [
            {"id": q["id"], "body": q["body"], "page": q["page"], "grounded": q["grounded"],
             "graph": q["graph"]}
            for q in rc.supporting_quotes[:MAX_QUOTES]
        ]
        return {
            "memory_id": cell.get("memory_id"),
            "memory_title": getattr(mnote, "title", "") if mnote else "",
            "memory_type": getattr(mnote, "type", "") if mnote else "",
            "memory_summary": (getattr(mnote, "body", "") if mnote else "")[:1000],
            "zk_id": zk_id,
            "zk_source": zk_source,
            "zk_title": rc.note.title or zk_id,
            "claim": (rc.note.body or "")[:1000],
            "claim_strength": cell.get("claim_strength"),
            "relation": cell.get("relation"),
            "confidence": cell.get("confidence"),
            "quotes": quotes,
        }

    return material_fn

cross_store_contradictions

cross_store_contradictions(overlay: dict[str, Any] | None = None, *, min_confidence: float = 0.55) -> list[dict[str, Any]]

Practice→canon contradicts edges from the claim overlay (counter-evidence).

Returns the claim-overlay edges typed contradicts clearing min_confidence, shaped for :func:zettelkasten.claims.debate_map's cross_store_contradictions parameter: {memory_id, zk_id, zk_source, confidence, rationale, claim_strength}. A practice node contradicting a strong canon claim is real-world counter-evidence, so feeding these lets a claim's contested status reflect practice, not just the literature.

On the default build path these contradicts edges were already vetted by the SHARED symmetric stance gate (:func:build_claim_connections wraps the LLM typer with :func:_stance_gated_typer), so they clear the same precision bar as WS3's intra-corpus discovery before they reach the debate map. This function only re-applies the min_confidence floor as a final selection cut.

Source code in zettelkasten/synapse/synthesis.py
def cross_store_contradictions(
    overlay: dict[str, Any] | None = None,
    *,
    min_confidence: float = 0.55,
) -> list[dict[str, Any]]:
    """Practice→canon ``contradicts`` edges from the claim overlay (counter-evidence).

    Returns the claim-overlay edges typed ``contradicts`` clearing ``min_confidence``,
    shaped for :func:`zettelkasten.claims.debate_map`'s ``cross_store_contradictions``
    parameter: ``{memory_id, zk_id, zk_source, confidence, rationale, claim_strength}``.
    A practice node contradicting a strong canon claim is real-world counter-evidence,
    so feeding these lets a claim's contested status reflect practice, not just the
    literature.

    On the default build path these ``contradicts`` edges were already vetted by the
    SHARED symmetric stance gate (:func:`build_claim_connections` wraps the LLM typer
    with :func:`_stance_gated_typer`), so they clear the same precision bar as WS3's
    intra-corpus discovery before they reach the debate map. This function only
    re-applies the ``min_confidence`` floor as a final selection cut.
    """
    if overlay is None:
        overlay = _overlay.load_overlay(CLAIM_OVERLAY_KIND)
    out: list[dict[str, Any]] = []
    for e in overlay.get("edges", []):
        if e.get("relation") != "contradicts":
            continue
        if float(e.get("confidence", 0.0) or 0.0) < min_confidence:
            continue
        out.append({
            "memory_id": e.get("memory_id"),
            "zk_id": e.get("zk_id"),
            "zk_source": e.get("zk_source"),
            "confidence": e.get("confidence"),
            "rationale": e.get("rationale", ""),
            "claim_strength": e.get("claim_strength"),
        })
    return out