Skip to content

zettelkasten.synapse.epistemics

zettelkasten.synapse.epistemics

Deterministic epistemic-status engine for the synapse reasoning layer.

This is the ESSENCE layer of the capability: the deterministic adjudicator that makes epistemic honesty structurally enforceable rather than a matter of trust. It has three responsibilities, in increasing scope:

  1. Per-node status derivation (:func:derive_node_status) — read a substrate :class:~zettelkasten.synapse.substrate.Node's machine-checkable tethers (a git path@sha pin, a dataset content_hash, a citation handle, a decision-tree predicate) and stamp a :class:StatusVerdict: a verified tier (grounded / measured) or inferred, plus the ORTHOGONAL stale / unresolved taint flags. Tether verification is injected as a :class:TetherResolver so the engine stays pure (all IO lives behind the resolver's callbacks; the same node + resolver always yields the same status).

  2. Status-propagation calculus (:func:propagate) — given a synthesis step's inference form and the verdicts of the substrate nodes / premises it rests on, compose FOUR rules TOGETHER (never either/or) into the conclusion's verdict: weakest-link tier cap ∧ stale-taint ∧ unresolved-taint ∧ form-downgrade. The composition order is fixed and documented on :func:propagate.

  3. The honesty invariant (:func:rederive_statuses / :func:check_honesty) — given a cited reasoning-DAG, RE-DERIVE every conclusion's status from its premises, form and tethers, then REJECT any agent-asserted status that is an UPGRADE over the re-derived status. This is the load-bearing point: an agent can never claim more certainty than the deterministic derivation grants, so fabrication is structurally impossible. An assertion EQUAL to or LOWER than the derived status is always accepted.

Design invariants:

  • Pure and deterministic. No LLM calls, no randomness, no wall-clock reads. Every source of non-determinism (git state, dataset bytes, citation resolution, decision-tree walk) is injected via :class:TetherResolver, so the core is a pure function of (substrate, resolver, DAG).
  • Reuse the wire vocabulary. Status values are the :class:~zettelkasten.synapse.memdsl.schema.EpistemicStatus enum (kept in lockstep with :data:zettelkasten.graph.VALID_EPISTEMIC_STATUS); inference forms are the :class:~zettelkasten.synapse.memdsl.schema.InferenceForm enum; the contested outcome and the contradiction/qualification trigger reuse the existing :data:zettelkasten.graph.VALID_RELATIONS. No parallel vocabulary is invented (see the module NOTES for the contested modelling decision).
  • Orthogonal taint. stale and unresolved are flags, not tiers: a claim can be simultaneously grounded-tier and stale. A single wire :class:EpistemicStatus is derived on demand via :meth:StatusVerdict.wire, which floors any tainted or contested verdict to inferred (taint never inflates the presented status).

StatusVerdict dataclass

A node's or conclusion's epistemic status: a tier plus orthogonal taint.

tier is the verified KIND (grounded / measured) or inferred. stale / unresolved are ORTHOGONAL taint flags — a verdict can be both grounded-tier AND stale. extrapolation marks a conclusion reached by analogy/extrapolation; contested marks one resting on contradicting or qualifying evidence. The single wire :class:EpistemicStatus a renderer shows is derived on demand via :meth:wire, which floors any tainted or contested verdict to inferred (taint never inflates the presented status).

outdated is a DISCLOSURE, not a taint: a faithfully-cited-but-superseded git pin (committed drift — the pinned bytes are still a retrievable immutable git object and the node's own body is fully verifiable, only HEAD has since moved on) reads grounded-tier AND outdated. It is deliberately NOT part of :attr:tainted and :meth:wire does NOT floor on it, so an outdated verified verdict stays verified and reaches the grounded tier — it only obliges a downstream "grounded as of the pinned sha; the current committed file (HEAD) has since changed" disclosure (the shared render._DETAIL_OUTDATED wording), never an abstention.

Source code in zettelkasten/synapse/epistemics.py
@dataclass(frozen=True)
class StatusVerdict:
    """A node's or conclusion's epistemic status: a tier plus orthogonal taint.

    ``tier`` is the verified KIND (``grounded`` / ``measured``) or ``inferred``.
    ``stale`` / ``unresolved`` are ORTHOGONAL taint flags — a verdict can be both
    ``grounded``-tier AND ``stale``. ``extrapolation`` marks a conclusion reached
    by analogy/extrapolation; ``contested`` marks one resting on contradicting or
    qualifying evidence. The single wire :class:`EpistemicStatus` a renderer shows
    is derived on demand via :meth:`wire`, which floors any tainted or contested
    verdict to ``inferred`` (taint never inflates the presented status).

    ``outdated`` is a DISCLOSURE, not a taint: a faithfully-cited-but-superseded
    git pin (committed drift — the pinned bytes are still a retrievable immutable
    git object and the node's own body is fully verifiable, only HEAD has since
    moved on) reads ``grounded``-tier AND ``outdated``. It is deliberately NOT part
    of :attr:`tainted` and :meth:`wire` does NOT floor on it, so an ``outdated``
    verified verdict stays verified and reaches the grounded tier — it only obliges
    a downstream "grounded as of the pinned sha; the current committed file (HEAD)
    has since changed" disclosure (the shared ``render._DETAIL_OUTDATED`` wording),
    never an abstention.
    """

    tier: EpistemicStatus = EpistemicStatus.INFERRED
    stale: bool = False
    unresolved: bool = False
    extrapolation: bool = False
    contested: bool = False
    outdated: bool = False

    @property
    def tainted(self) -> bool:
        """Whether any taint flag (``stale`` / ``unresolved`` / ``contested``) is set.

        ``outdated`` is deliberately EXCLUDED — it is a non-blocking disclosure of
        committed drift, not a taint, so a purely-``outdated`` verified verdict is
        never tainted and reaches the grounded tier.
        """
        return self.stale or self.unresolved or self.contested

    def wire(self) -> EpistemicStatus:
        """Collapse to the single :class:`EpistemicStatus` a renderer emits.

        The mapping is deliberately conservative — taint NEVER inflates:

        * a ``contested`` verdict → ``inferred`` (contested evidence is not verified);
        * a ``stale`` or ``unresolved`` verdict → ``inferred`` (a claim on drifted
          or dangling ground cannot honestly be presented as verified);
        * otherwise the verdict's own ``tier``.

        ``extrapolation`` is a marker, not a taint: it annotates an
        already-``inferred`` conclusion and never changes the wire value on its own.
        ``outdated`` is likewise a disclosure, not a taint: a committed-drift verdict
        keeps its own ``tier`` (an ``outdated`` grounded/measured verdict wires to
        ``grounded``/``measured``), so this method never reads it.
        """
        if self.contested or self.stale or self.unresolved:
            return EpistemicStatus.INFERRED
        return self.tier

    @property
    def rank(self) -> int:
        """The lattice rank of this verdict's WIRE status (post-taint floor)."""
        return status_rank(self.wire())

tainted property

tainted: bool

Whether any taint flag (stale / unresolved / contested) is set.

outdated is deliberately EXCLUDED — it is a non-blocking disclosure of committed drift, not a taint, so a purely-outdated verified verdict is never tainted and reaches the grounded tier.

rank property

rank: int

The lattice rank of this verdict's WIRE status (post-taint floor).

wire

wire() -> EpistemicStatus

Collapse to the single :class:EpistemicStatus a renderer emits.

The mapping is deliberately conservative — taint NEVER inflates:

  • a contested verdict → inferred (contested evidence is not verified);
  • a stale or unresolved verdict → inferred (a claim on drifted or dangling ground cannot honestly be presented as verified);
  • otherwise the verdict's own tier.

extrapolation is a marker, not a taint: it annotates an already-inferred conclusion and never changes the wire value on its own. outdated is likewise a disclosure, not a taint: a committed-drift verdict keeps its own tier (an outdated grounded/measured verdict wires to grounded/measured), so this method never reads it.

Source code in zettelkasten/synapse/epistemics.py
def wire(self) -> EpistemicStatus:
    """Collapse to the single :class:`EpistemicStatus` a renderer emits.

    The mapping is deliberately conservative — taint NEVER inflates:

    * a ``contested`` verdict → ``inferred`` (contested evidence is not verified);
    * a ``stale`` or ``unresolved`` verdict → ``inferred`` (a claim on drifted
      or dangling ground cannot honestly be presented as verified);
    * otherwise the verdict's own ``tier``.

    ``extrapolation`` is a marker, not a taint: it annotates an
    already-``inferred`` conclusion and never changes the wire value on its own.
    ``outdated`` is likewise a disclosure, not a taint: a committed-drift verdict
    keeps its own ``tier`` (an ``outdated`` grounded/measured verdict wires to
    ``grounded``/``measured``), so this method never reads it.
    """
    if self.contested or self.stale or self.unresolved:
        return EpistemicStatus.INFERRED
    return self.tier

TetherResolver dataclass

Injected, deterministic verification of a node's grounding tethers.

Each callback answers one yes/no/unknown question about a single tether, so the engine is a pure function of (node, resolver) — all IO (git state, dataset bytes, the citation store, a decision-tree walk) lives behind these callbacks. Every callback returns:

  • True — the tether verifies (fresh git pin, matching hash, resolving citation, an applies/true predicate);
  • False — a definitive failure (a DRIFTED git pin or mismatched hash → stale; a DANGLING citation → unresolved; an EXCLUDED/false predicate → simply not a valid ground here, raising no taint);
  • None — cannot verify (unknown). Treated CONSERVATIVELY: the tether's aspired kind is kept but the corresponding taint is raised, so an unverifiable ground can never be presented as clean.

The default callbacks all return None (maximally conservative: nothing is trusted until a caller injects real verification), so a bare TetherResolver taints every tethered node. Callers inject real checks; tests inject fakes.

Source code in zettelkasten/synapse/epistemics.py
@dataclass(frozen=True)
class TetherResolver:
    """Injected, deterministic verification of a node's grounding tethers.

    Each callback answers one yes/no/unknown question about a single tether, so
    the engine is a pure function of ``(node, resolver)`` — all IO (git state,
    dataset bytes, the citation store, a decision-tree walk) lives behind these
    callbacks. Every callback returns:

    * ``True`` — the tether verifies (fresh git pin, matching hash, resolving
      citation, an ``applies``/true predicate);
    * ``False`` — a definitive failure (a DRIFTED git pin or mismatched hash →
      ``stale``; a DANGLING citation → ``unresolved``; an EXCLUDED/false
      predicate → simply not a valid ground here, raising no taint);
    * ``None`` — cannot verify (unknown). Treated CONSERVATIVELY: the tether's
      aspired kind is kept but the corresponding taint is raised, so an
      unverifiable ground can never be presented as clean.

    The default callbacks all return ``None`` (maximally conservative: nothing is
    trusted until a caller injects real verification), so a bare ``TetherResolver``
    taints every tethered node. Callers inject real checks; tests inject fakes.
    """

    git_fresh: Callable[[str], bool | None] = lambda pin: None
    hash_fresh: Callable[[str], bool | None] = lambda content_hash: None
    cite_resolves: Callable[[str], bool | None] = lambda handle: None
    predicate: Callable[[Node, str], bool | None] = lambda node, payload: None
    # Committed-drift disclosure for a git pin: ``True`` IFF the pinned bytes are a
    # retrievable immutable blob but HEAD has since moved on (``pinned != head``,
    # both regular-file blobs) — a faithfully-cited-but-superseded pin that reads
    # ``grounded``-tier + ``outdated`` rather than ``stale``. ``None`` means "not a
    # committed-drift signal" (fresh, on-disk rewrite, unresolvable, or unknown), so
    # the git branch falls through to ``stale``. Default is the conservative ``None``.
    git_committed_drift: Callable[[str], bool | None] = lambda pin: None

PropagationRule

Bases: str, Enum

The propagation rule a synthesis step follows, keyed off its inference form.

An internal RULE dispatch (not a wire vocabulary): it maps the reused :class:~zettelkasten.synapse.memdsl.schema.InferenceForm members onto the six rule categories the spec defines, so the same form always propagates the same way.

Source code in zettelkasten/synapse/epistemics.py
class PropagationRule(str, Enum):
    """The propagation rule a synthesis step follows, keyed off its inference form.

    An internal RULE dispatch (not a wire vocabulary): it maps the reused
    :class:`~zettelkasten.synapse.memdsl.schema.InferenceForm` members onto the
    six rule categories the spec defines, so the same form always propagates the
    same way.
    """

    RESTATE = "restate"        # preserve the cited node's status
    RECOMPUTE = "recompute"    # measured iff all inputs measured AND derivation given
    GENERALIZE = "generalize"  # downgrade to inferred (a defeasible leap)
    ANALOGIZE = "analogize"    # inferred + an extrapolation marker
    DEDUCE = "deduce"          # weakest-link: capped by the weakest premise

HonestyViolation dataclass

One rejected agent assertion: a status claimed HIGHER than re-derived.

claim_id names the offending claim; asserted is the status the agent claimed; derived is the engine's re-derived WIRE status. A violation exists only when asserted strictly outranks derived in the lattice.

Source code in zettelkasten/synapse/epistemics.py
@dataclass(frozen=True)
class HonestyViolation:
    """One rejected agent assertion: a status claimed HIGHER than re-derived.

    ``claim_id`` names the offending claim; ``asserted`` is the status the agent
    claimed; ``derived`` is the engine's re-derived WIRE status. A violation
    exists only when ``asserted`` strictly outranks ``derived`` in the lattice.
    """

    claim_id: str
    asserted: EpistemicStatus
    derived: EpistemicStatus

HonestyReport dataclass

The verdict of an honesty check over a cited reasoning-DAG.

ok is True iff no assertion is an upgrade over its re-derived status. derived is the full {claim_id: StatusVerdict} re-derivation (so a caller can render the honest status); violations lists every rejected upgrade (empty when ok).

Source code in zettelkasten/synapse/epistemics.py
@dataclass(frozen=True)
class HonestyReport:
    """The verdict of an honesty check over a cited reasoning-DAG.

    ``ok`` is ``True`` iff no assertion is an upgrade over its re-derived status.
    ``derived`` is the full ``{claim_id: StatusVerdict}`` re-derivation (so a
    caller can render the honest status); ``violations`` lists every rejected
    upgrade (empty when ``ok``).
    """

    ok: bool
    derived: dict[str, StatusVerdict]
    violations: list[HonestyViolation] = field(default_factory=list)

status_rank

status_rank(status: EpistemicStatus) -> int

Return a status's lattice rank (inferred = 0, grounded/measured = 1).

grounded and measured share rank 1: they are distinct verified KINDS, neither strictly above the other, so asserting one when the other was derived is never an honesty upgrade.

Source code in zettelkasten/synapse/epistemics.py
def status_rank(status: EpistemicStatus) -> int:
    """Return a status's lattice rank (``inferred`` = 0, ``grounded``/``measured`` = 1).

    ``grounded`` and ``measured`` share rank 1: they are distinct verified KINDS,
    neither strictly above the other, so asserting one when the other was derived
    is never an honesty upgrade.
    """
    return _STATUS_RANK.get(status, 0)

derive_node_status

derive_node_status(node: Node, resolver: TetherResolver | None = None) -> StatusVerdict

Derive a substrate node's :class:StatusVerdict from its tethers.

Each tether contributes independently and the result is their JOIN:

  • git pin (file:/git:) → aspires to grounded. Valid only as a non-empty path@sha pin: fresh → clean grounded; a COMMITTED-DRIFT pin (git_committed_driftTrue: the pinned bytes are still a retrievable immutable blob but HEAD has moved on) → grounded-tier + outdated (a disclosure, NOT stale — it still reaches the grounded tier); any other non-fresh outcome (an on-disk rewrite, a non-blob pin, or an unverifiable None) → grounded-tier + stale. An UNPINNED payload (no @sha) is grounded-tier + stale STRUCTURALLY — there is no committed sha to check drift against — regardless of what the resolver says.
  • dataset hash (hash:) → aspires to measured. Matching → measured; mismatched/unverifiable → measured-tier + stale.
  • citation (cite:) → aspires to grounded. Resolves → grounded; dangling/unverifiable → grounded-tier + unresolved.
  • decision-tree predicate (predicate:) → aspires to grounded. applies (True) → grounded; excluded (False) → contributes NO ground and no taint (the clause definitively does not apply here); unresolved (None) → grounded-tier + unresolved.

The node's tier is the strongest aspired kind seen (measured beats grounded beats inferred); the taint flags are the OR over every tether. A node with no tethers is a plain inferred node with no taint. The aspired-kind-plus-taint representation keeps tier and taint orthogonal so the propagation calculus can compose them independently; :meth:`wirefloors a tainted verdict toinferred`` when a single status is needed.

Source code in zettelkasten/synapse/epistemics.py
def derive_node_status(node: Node, resolver: TetherResolver | None = None) -> StatusVerdict:
    """Derive a substrate node's :class:`StatusVerdict` from its ``tethers``.

    Each tether contributes independently and the result is their JOIN:

    * **git pin** (``file:``/``git:``) → aspires to ``grounded``. Valid only as a
      non-empty ``path@sha`` pin: fresh → clean grounded; a COMMITTED-DRIFT pin
      (``git_committed_drift`` → ``True``: the pinned bytes are still a retrievable
      immutable blob but HEAD has moved on) → grounded-tier + ``outdated`` (a
      disclosure, NOT stale — it still reaches the grounded tier); any other
      non-fresh outcome (an on-disk rewrite, a non-blob pin, or an unverifiable
      ``None``) → grounded-tier + ``stale``. An UNPINNED payload (no ``@sha``) is
      grounded-tier + ``stale`` STRUCTURALLY — there is no committed sha to check
      drift against — regardless of what the resolver says.
    * **dataset hash** (``hash:``) → aspires to ``measured``. Matching → measured;
      mismatched/unverifiable → measured-tier + ``stale``.
    * **citation** (``cite:``) → aspires to ``grounded``. Resolves → grounded;
      dangling/unverifiable → grounded-tier + ``unresolved``.
    * **decision-tree predicate** (``predicate:``) → aspires to ``grounded``.
      ``applies`` (True) → grounded; ``excluded`` (False) → contributes NO ground
      and no taint (the clause definitively does not apply here); ``unresolved``
      (None) → grounded-tier + ``unresolved``.

    The node's tier is the strongest aspired kind seen (``measured`` beats
    ``grounded`` beats ``inferred``); the taint flags are the OR over every
    tether. A node with no tethers is a plain ``inferred`` node with no taint.
    The aspired-kind-plus-taint representation keeps tier and taint orthogonal so
    the propagation calculus can compose them independently; :meth:`wire`` floors
    a tainted verdict to ``inferred`` when a single status is needed.
    """
    resolver = resolver or TetherResolver()
    kinds: set[EpistemicStatus] = set()
    stale = False
    unresolved = False
    outdated = False

    for tether in node.tethers or []:
        git = _tether_payload(tether, GIT_TETHER_PREFIXES)
        if git is not None:
            kinds.add(EpistemicStatus.GROUNDED)
            # A git tether verifies ONLY when it carries a non-empty ``path@sha``
            # pin. An unpinned/invalid payload is tainted ``stale`` STRUCTURALLY
            # here — before the resolver runs — so a resolver returning ``True``
            # can never make an unpinned path read clean grounded (fabrication
            # stays structurally impossible, never delegated to trust). A valid pin
            # then resolves in tiers: a FRESH pin (``git_fresh`` → ``True``) reads
            # clean grounded; a COMMITTED-DRIFT pin (``git_committed_drift`` →
            # ``True``) reads grounded-tier + ``outdated`` — a disclosure, NOT a
            # taint, since the pinned bytes are still a retrievable immutable blob
            # and the node's own body is verifiable; any other non-fresh outcome
            # (on-disk rewrite, non-blob, unverifiable ``None``) stays ``stale`` so
            # it can never read false-grounded.
            if not _is_git_pin(git):
                stale = True
            elif resolver.git_fresh(git) is True:
                pass
            elif resolver.git_committed_drift(git) is True:
                outdated = True
            else:
                stale = True
            continue
        content_hash = _tether_payload(tether, HASH_TETHER_PREFIX)
        if content_hash is not None:
            kinds.add(EpistemicStatus.MEASURED)
            # A payload that is not a valid content hash (:func:`_is_content_hash`
            # — an anchored hex digest) has nothing to match against, so it is
            # tainted ``stale`` STRUCTURALLY here, before ``hash_fresh`` runs and
            # mirroring the git short-circuit. A POSITIVE hex gate means a junk /
            # invisible-char payload can never read clean measured by delegating to
            # a resolver returning ``True``.
            if not _is_content_hash(content_hash) or resolver.hash_fresh(content_hash) is not True:
                stale = True
            continue
        cite = _tether_payload(tether, CITE_TETHER_PREFIX)
        if cite is not None:
            kinds.add(EpistemicStatus.GROUNDED)
            # A payload that is not a valid handle (:func:`_is_handle` — a positive
            # ASCII-graphic gate) names no resolvable target, so it is tainted
            # ``unresolved`` STRUCTURALLY here, before ``cite_resolves`` runs — a
            # malformed / invisible-char cite can never read clean grounded.
            if not _is_handle(cite) or resolver.cite_resolves(cite) is not True:
                unresolved = True
            continue
        pred = _tether_payload(tether, PREDICATE_TETHER_PREFIX)
        if pred is not None:
            # A payload that is not a valid handle (:func:`_is_handle`) names no
            # clause to evaluate, so it aspires to grounded but is tainted
            # ``unresolved`` STRUCTURALLY here — before the resolver's predicate
            # walk is consulted.
            if not _is_handle(pred):
                kinds.add(EpistemicStatus.GROUNDED)
                unresolved = True
                continue
            verdict = resolver.predicate(node, pred)
            if verdict is True:
                kinds.add(EpistemicStatus.GROUNDED)
            elif verdict is None:
                # Unresolved decision: aspires to grounded but cannot be confirmed.
                kinds.add(EpistemicStatus.GROUNDED)
                unresolved = True
            # verdict is False → excluded: not a ground here, and not a gap either.
            continue
        # An unrecognized tether prefix grounds nothing (ignored, never crashes).

    if EpistemicStatus.MEASURED in kinds:
        tier = EpistemicStatus.MEASURED
    elif EpistemicStatus.GROUNDED in kinds:
        tier = EpistemicStatus.GROUNDED
    else:
        tier = EpistemicStatus.INFERRED

    return StatusVerdict(tier=tier, stale=stale, unresolved=unresolved, outdated=outdated)

rule_for_form

rule_for_form(form: Any) -> PropagationRule

Return the :class:PropagationRule for an inference form.

Total over :class:InferenceForm; any unknown/unmapped value degrades to :attr:PropagationRule.GENERALIZE, so an unrecognized form can only ever be treated as an inferred leap — it can never grant an unearned upgrade.

Source code in zettelkasten/synapse/epistemics.py
def rule_for_form(form: Any) -> PropagationRule:
    """Return the :class:`PropagationRule` for an inference ``form``.

    Total over :class:`InferenceForm`; any unknown/unmapped value degrades to
    :attr:`PropagationRule.GENERALIZE`, so an unrecognized form can only ever be
    treated as an ``inferred`` leap — it can never grant an unearned upgrade.
    """
    try:
        form = InferenceForm(form)
    except ValueError:
        return PropagationRule.GENERALIZE
    return _FORM_RULE.get(form, PropagationRule.GENERALIZE)

propagate

propagate(form: Any, inputs: Iterable[StatusVerdict], *, derivation_available: bool = False, contested: bool = False) -> StatusVerdict

Propagate a conclusion's status from its inputs, form, and taint signals.

inputs are the verdicts of EVERY substrate node cited and every premise the step rests on. The four combination rules are applied TOGETHER (never either/or), in this fixed, deterministic order:

  1. Taint union (orthogonal). stale / unresolved / contested / extrapolation are OR-ed across all inputs (with the explicit contested signal). A conclusion resting on any stale premise is stale; on any unresolved reference is unresolved; on any contradicting/qualifying evidence is contested. The outdated DISCLOSURE is unioned SEPARATELY (kept apart from the stale union): a conclusion resting on any committed-drift premise is itself outdated, but — like the flag it carries — outdated never downgrades the tier or the wire status, so an outdated grounded conclusion stays grounded.
  2. Form base tier. The rule for form proposes a base tier: RESTATE → the strongest input kind (preserve); RECOMPUTEmeasured iff every input is measured AND derivation_available, else inferred (cannot claim measured without the derivation); DEDUCEgrounded (a conclusion grounded in its premises); GENERALIZEinferred; ANALOGIZEinferred and sets the extrapolation marker.
  3. Weakest-link cap. The base tier is capped by the weakest input: a verified base cannot survive an inferred input, so it is floored to inferred when any input is inferred (a two-rank lattice).
  4. Wire floor (on read). The resulting verdict keeps its tier and taint independently; :meth:StatusVerdict.wire later floors any tainted or contested verdict to inferred. Composing at the verdict level (not the wire level) is what lets all four rules stack instead of clobbering.

Returns the composed :class:StatusVerdict. Pure and deterministic.

Source code in zettelkasten/synapse/epistemics.py
def propagate(
    form: Any,
    inputs: Iterable[StatusVerdict],
    *,
    derivation_available: bool = False,
    contested: bool = False,
) -> StatusVerdict:
    """Propagate a conclusion's status from its inputs, form, and taint signals.

    ``inputs`` are the verdicts of EVERY substrate node cited and every premise
    the step rests on. The four combination rules are applied TOGETHER (never
    either/or), in this fixed, deterministic order:

    1. **Taint union (orthogonal).** ``stale`` / ``unresolved`` / ``contested`` /
       ``extrapolation`` are OR-ed across all inputs (with the explicit
       ``contested`` signal). A conclusion resting on any stale premise is stale;
       on any unresolved reference is unresolved; on any contradicting/qualifying
       evidence is contested. The ``outdated`` DISCLOSURE is unioned SEPARATELY
       (kept apart from the ``stale`` union): a conclusion resting on any
       committed-drift premise is itself ``outdated``, but — like the flag it
       carries — ``outdated`` never downgrades the tier or the wire status, so an
       ``outdated`` grounded conclusion stays grounded.
    2. **Form base tier.** The rule for ``form`` proposes a base tier:
       ``RESTATE`` → the strongest input kind (preserve); ``RECOMPUTE`` →
       ``measured`` iff every input is ``measured`` AND ``derivation_available``,
       else ``inferred`` (cannot claim measured without the derivation);
       ``DEDUCE`` → ``grounded`` (a conclusion grounded in its premises);
       ``GENERALIZE`` → ``inferred``; ``ANALOGIZE`` → ``inferred`` and sets the
       ``extrapolation`` marker.
    3. **Weakest-link cap.** The base tier is capped by the weakest input: a
       verified base cannot survive an ``inferred`` input, so it is floored to
       ``inferred`` when any input is inferred (a two-rank lattice).
    4. **Wire floor (on read).** The resulting verdict keeps its tier and taint
       independently; :meth:`StatusVerdict.wire` later floors any tainted or
       contested verdict to ``inferred``. Composing at the verdict level (not the
       wire level) is what lets all four rules stack instead of clobbering.

    Returns the composed :class:`StatusVerdict`. Pure and deterministic.
    """
    inputs = list(inputs)
    rule = rule_for_form(form)

    taint_stale = any(inp.stale for inp in inputs)
    taint_unresolved = any(inp.unresolved for inp in inputs)
    taint_contested = bool(contested) or any(inp.contested for inp in inputs)
    extrapolation = any(inp.extrapolation for inp in inputs)
    # ``outdated`` is unioned SEPARATELY from the stale taint: a conclusion resting
    # on a committed-drift premise inherits the disclosure, but it never enters the
    # weakest-link cap or the wire floor, so the tier is unaffected.
    disclosure_outdated = any(inp.outdated for inp in inputs)

    if rule is PropagationRule.RESTATE:
        base = _best_kind(inputs)
    elif rule is PropagationRule.RECOMPUTE:
        all_measured = bool(inputs) and all(inp.tier is EpistemicStatus.MEASURED for inp in inputs)
        base = EpistemicStatus.MEASURED if (all_measured and derivation_available) else EpistemicStatus.INFERRED
    elif rule is PropagationRule.DEDUCE:
        base = EpistemicStatus.GROUNDED
    elif rule is PropagationRule.ANALOGIZE:
        base = EpistemicStatus.INFERRED
        extrapolation = True
    else:  # GENERALIZE and any fallback
        base = EpistemicStatus.INFERRED

    # Weakest-link cap: a verified base cannot outrank the weakest input tier.
    weak = _weakest_rank(inputs)
    tier = base if status_rank(base) <= weak else EpistemicStatus.INFERRED

    return StatusVerdict(
        tier=tier,
        stale=taint_stale,
        unresolved=taint_unresolved,
        extrapolation=extrapolation,
        contested=taint_contested,
        outdated=disclosure_outdated,
    )

contested_from_relations

contested_from_relations(relations: Iterable[str]) -> bool

Whether any relation in relations is a contradiction/qualification.

Maps the substrate edge relations (:data:zettelkasten.graph.VALID_RELATIONS) onto the contested trigger: a contradicts or qualifies edge among a step's cited nodes renders its conclusion contested.

Source code in zettelkasten/synapse/epistemics.py
def contested_from_relations(relations: Iterable[str]) -> bool:
    """Whether any relation in ``relations`` is a contradiction/qualification.

    Maps the substrate edge relations (:data:`zettelkasten.graph.VALID_RELATIONS`)
    onto the contested trigger: a ``contradicts`` or ``qualifies`` edge among a
    step's cited nodes renders its conclusion contested.
    """
    return any(rel in CONTESTING_RELATIONS for rel in relations)

rederive_statuses

rederive_statuses(dag: ReasoningDAG, node_status: Mapping[str, StatusVerdict], *, edges: Sequence[Edge] = (), derivation_available: Callable[[Claim], bool] | None = None) -> dict[str, StatusVerdict]

Re-derive every claim's :class:StatusVerdict from premises, form and tethers.

Traverses dag in dependency order (:func:_topological_order), re-deriving each claim from the verdicts of the substrate nodes it cites and the premises it builds on:

  • A cited ref present in node_status contributes its per-node verdict; a ref ABSENT from node_status is an unresolved reference and contributes an inferred + unresolved verdict, so a dangling cite taints the step.
  • A premise contributes its already-re-derived verdict.
  • The step is contested when a contradicts / qualifies edge joins two of its cited nodes (see :func:_contested_cites).
  • derivation_available decides whether a recomputation step may claim measured; it defaults to "the claim carries a non-empty derivation".

Returns {claim_id: StatusVerdict}. Deterministic: same DAG + substrate → same verdicts. Tolerates an empty DAG (returns {}) and does not itself call :meth:ReasoningDAG.validate — see :func:check_honesty for the defensive validation gate.

Source code in zettelkasten/synapse/epistemics.py
def rederive_statuses(
    dag: ReasoningDAG,
    node_status: Mapping[str, StatusVerdict],
    *,
    edges: Sequence[Edge] = (),
    derivation_available: Callable[[Claim], bool] | None = None,
) -> dict[str, StatusVerdict]:
    """Re-derive every claim's :class:`StatusVerdict` from premises, form and tethers.

    Traverses ``dag`` in dependency order (:func:`_topological_order`), re-deriving
    each claim from the verdicts of the substrate nodes it cites and the premises
    it builds on:

    * A cited ref present in ``node_status`` contributes its per-node verdict; a
      ref ABSENT from ``node_status`` is an unresolved reference and contributes an
      ``inferred`` + ``unresolved`` verdict, so a dangling cite taints the step.
    * A premise contributes its already-re-derived verdict.
    * The step is contested when a ``contradicts`` / ``qualifies`` edge joins two
      of its cited nodes (see :func:`_contested_cites`).
    * ``derivation_available`` decides whether a ``recomputation`` step may claim
      ``measured``; it defaults to "the claim carries a non-empty ``derivation``".

    Returns ``{claim_id: StatusVerdict}``. Deterministic: same DAG + substrate →
    same verdicts. Tolerates an empty DAG (returns ``{}``) and does not itself
    call :meth:`ReasoningDAG.validate` — see :func:`check_honesty` for the
    defensive validation gate.
    """
    if derivation_available is None:
        def derivation_available(claim: Claim) -> bool:
            return bool(claim.derivation and claim.derivation.strip())

    by_id = {c.id: c for c in dag.claims}
    edges = list(edges)
    derived: dict[str, StatusVerdict] = {}

    for cid in _topological_order(dag):
        claim = by_id[cid]
        inputs: list[StatusVerdict] = []
        for ref in claim.cites:
            # Explicit dict-default (not ``or``): a present-but-falsy verdict must
            # never be swapped for the unresolved default on the honesty path.
            inputs.append(
                node_status.get(ref, StatusVerdict(tier=EpistemicStatus.INFERRED, unresolved=True))
            )
        for premise in claim.premises:
            # A validated DAG resolves every premise; guard defensively regardless.
            inputs.append(derived.get(premise, StatusVerdict(tier=EpistemicStatus.INFERRED, unresolved=True)))
        contested = _contested_cites(claim, edges)
        derived[cid] = propagate(
            claim.form,
            inputs,
            derivation_available=derivation_available(claim),
            contested=contested,
        )
    return derived

check_honesty

check_honesty(dag: ReasoningDAG, asserted: Mapping[str, EpistemicStatus], node_status: Mapping[str, StatusVerdict], *, edges: Sequence[Edge] = (), derivation_available: Callable[[Claim], bool] | None = None, validate: bool = True) -> HonestyReport

Enforce the honesty invariant over an agent's cited reasoning-DAG.

Re-derives every claim's status (:func:rederive_statuses) and compares it to the agent's asserted status per claim. An assertion is REJECTED only when it is an UPGRADE — strictly higher in the lattice than the re-derived WIRE status. An assertion EQUAL to (including grounded vs measured, which share a rank) or LOWER than the derived status is always accepted, so an agent may under-claim freely but can never over-claim. This is what makes fabrication structurally impossible.

asserted maps claim ids to the :class:EpistemicStatus the agent claims (only ids present are checked — an unasserted claim cannot violate anything). When validate is set (default) and the DAG is non-empty, the DAG is validated defensively first (:meth:ReasoningDAG.validate), so a malformed DAG is rejected before any status is trusted. Returns a :class:HonestyReport.

Source code in zettelkasten/synapse/epistemics.py
def check_honesty(
    dag: ReasoningDAG,
    asserted: Mapping[str, EpistemicStatus],
    node_status: Mapping[str, StatusVerdict],
    *,
    edges: Sequence[Edge] = (),
    derivation_available: Callable[[Claim], bool] | None = None,
    validate: bool = True,
) -> HonestyReport:
    """Enforce the honesty invariant over an agent's cited reasoning-DAG.

    Re-derives every claim's status (:func:`rederive_statuses`) and compares it to
    the agent's ``asserted`` status per claim. An assertion is REJECTED only when
    it is an UPGRADE — strictly higher in the lattice than the re-derived WIRE
    status. An assertion EQUAL to (including ``grounded`` vs ``measured``, which
    share a rank) or LOWER than the derived status is always accepted, so an agent
    may under-claim freely but can never over-claim. This is what makes
    fabrication structurally impossible.

    ``asserted`` maps claim ids to the :class:`EpistemicStatus` the agent claims
    (only ids present are checked — an unasserted claim cannot violate anything).
    When ``validate`` is set (default) and the DAG is non-empty, the DAG is
    validated defensively first (:meth:`ReasoningDAG.validate`), so a malformed
    DAG is rejected before any status is trusted. Returns a :class:`HonestyReport`.
    """
    if validate and dag.claims:
        dag.validate()
    derived = rederive_statuses(
        dag, node_status, edges=edges, derivation_available=derivation_available
    )
    violations: list[HonestyViolation] = []
    for claim_id, asserted_status in asserted.items():
        asserted_status = EpistemicStatus(asserted_status)
        derived_verdict = derived.get(claim_id)
        # An assertion about an unknown claim cannot be honestly grounded: treat
        # the (absent) derivation as the floor so any positive claim is an upgrade.
        derived_wire = derived_verdict.wire() if derived_verdict is not None else EpistemicStatus.INFERRED
        if status_rank(asserted_status) > status_rank(derived_wire):
            violations.append(
                HonestyViolation(
                    claim_id=claim_id,
                    asserted=asserted_status,
                    derived=derived_wire,
                )
            )
    return HonestyReport(ok=not violations, derived=derived, violations=violations)

resolver_from_walk

resolver_from_walk(walk_result: Mapping[str, Any], *, git_fresh: Callable[[str], bool | None] | None = None, hash_fresh: Callable[[str], bool | None] | None = None, cite_resolves: Callable[[str], bool | None] | None = None) -> TetherResolver

Build a :class:TetherResolver whose predicate REUSES a decision-tree walk.

Consumes the bundle :func:zettelkasten.decision_tree.walk returns — its per-node applies / excluded / unresolved classification — instead of re-evaluating any predicate here. A predicate: tether whose payload (or the node's own id) names an applies node verifies as grounded; an excluded node is a definitive non-ground; an unresolved or unknown node is an unresolved gap. The git/hash/citation callbacks are passed through unchanged (default to the conservative None), so the returned resolver plugs the decision-tree layer into :func:derive_node_status without the engine ever touching predicate evaluation itself.

Source code in zettelkasten/synapse/epistemics.py
def resolver_from_walk(
    walk_result: Mapping[str, Any],
    *,
    git_fresh: Callable[[str], bool | None] | None = None,
    hash_fresh: Callable[[str], bool | None] | None = None,
    cite_resolves: Callable[[str], bool | None] | None = None,
) -> TetherResolver:
    """Build a :class:`TetherResolver` whose predicate REUSES a decision-tree walk.

    Consumes the bundle :func:`zettelkasten.decision_tree.walk` returns — its
    per-node ``applies`` / ``excluded`` / ``unresolved`` classification — instead
    of re-evaluating any predicate here. A ``predicate:`` tether whose payload (or
    the node's own id) names an ``applies`` node verifies as ``grounded``; an
    ``excluded`` node is a definitive non-ground; an ``unresolved`` or unknown node
    is an unresolved gap. The git/hash/citation callbacks are passed through
    unchanged (default to the conservative ``None``), so the returned resolver
    plugs the decision-tree layer into :func:`derive_node_status` without the
    engine ever touching predicate evaluation itself.
    """
    from zettelkasten import decision_tree

    nodes = (walk_result or {}).get("nodes") or {}
    status_by_id: dict[str, str] = {}
    for uid, rec in nodes.items():
        bare = str(rec.get("id") or uid.split("::")[-1])
        status_by_id[bare] = str(rec.get("status") or "")

    def predicate(node: Node, payload: str) -> bool | None:
        status = status_by_id.get(payload) or status_by_id.get(node.id)
        if status == decision_tree.STATUS_APPLIES:
            return True
        if status == decision_tree.STATUS_EXCLUDED:
            return False
        return None  # unresolved, or a node the walk never reached

    base = TetherResolver()
    return TetherResolver(
        git_fresh=git_fresh or base.git_fresh,
        hash_fresh=hash_fresh or base.hash_fresh,
        cite_resolves=cite_resolves or base.cite_resolves,
        predicate=predicate,
    )

apply_node_statuses

apply_node_statuses(nodes: Iterable[Node], resolver: TetherResolver | None = None) -> dict[str, StatusVerdict]

Derive statuses for many nodes at once, keyed by :attr:Node.token.

A thin convenience for the honesty gate: produce the node_status map :func:check_honesty / :func:rederive_statuses consume from an assembled substrate neighborhood. Also stamps each node's epistemic_status placeholder IN PLACE with the derived WIRE status (the substrate leaves it None), so a downstream renderer can read it directly. The committed-drift outdated disclosure is stamped ALONGSIDE it (node.outdated): because a purely-outdated verdict wires to grounded — byte-indistinguishable from fresh at the scalar — the flag must ride with the scalar so a reloaded/consumed node still carries the disclosure and can never be presented as fresh-grounded. Returns the {token: verdict} map.

Called on the RAW seed neighborhood (see :meth:Navigator.navigate) BEFORE any downstream guard, so a bad-id node whose token/:class:Address cannot be constructed (an id carrying a char ZK's validate_id permits but the address scheme forbids) would raise :class:MemDSLParseError and sink the whole turn. Reuse the assembler's _safe_token so such a node is SKIPPED (absent from the result, a debug log) and the surviving nodes are still statused. Only the address-validity MemDSLParseError is swallowed; any other error propagates.

Source code in zettelkasten/synapse/epistemics.py
def apply_node_statuses(
    nodes: Iterable[Node], resolver: TetherResolver | None = None
) -> dict[str, StatusVerdict]:
    """Derive statuses for many nodes at once, keyed by :attr:`Node.token`.

    A thin convenience for the honesty gate: produce the ``node_status`` map
    :func:`check_honesty` / :func:`rederive_statuses` consume from an assembled
    substrate neighborhood. Also stamps each node's ``epistemic_status`` placeholder
    IN PLACE with the derived WIRE status (the substrate leaves it ``None``), so a
    downstream renderer can read it directly. The committed-drift ``outdated``
    disclosure is stamped ALONGSIDE it (``node.outdated``): because a purely-outdated
    verdict wires to ``grounded`` — byte-indistinguishable from fresh at the scalar
    — the flag must ride with the scalar so a reloaded/consumed node still carries
    the disclosure and can never be presented as fresh-grounded. Returns the
    ``{token: verdict}`` map.

    Called on the RAW seed neighborhood (see :meth:`Navigator.navigate`) BEFORE any
    downstream guard, so a bad-id node whose ``token``/:class:`Address` cannot be
    constructed (an id carrying a char ZK's ``validate_id`` permits but the address
    scheme forbids) would raise :class:`MemDSLParseError` and sink the whole turn.
    Reuse the assembler's ``_safe_token`` so such a node is SKIPPED (absent from the
    result, a debug log) and the surviving nodes are still statused. Only the
    address-validity ``MemDSLParseError`` is swallowed; any other error propagates.
    """
    resolver = resolver or TetherResolver()
    out: dict[str, StatusVerdict] = {}
    for node in nodes:
        token = _safe_token(node)
        if token is None:
            logger.debug(
                "epistemics: skipping unaddressable node in apply_node_statuses "
                "(id=%r source=%r store=%r)",
                node.id, node.source, node.store,
            )
            continue
        verdict = derive_node_status(node, resolver)
        out[token] = verdict
        node.epistemic_status = verdict.wire()
        # Stamp the committed-drift disclosure INSEPARABLY alongside the wire scalar
        # (a purely-outdated verdict wires to ``grounded``), so a consumer of the
        # node can never mistake an outdated-grounded node for a fresh one.
        node.outdated = verdict.outdated
    return out