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:
-
Per-node status derivation (:func:
derive_node_status) — read a substrate :class:~zettelkasten.synapse.substrate.Node's machine-checkabletethers(a gitpath@shapin, a datasetcontent_hash, a citation handle, a decision-tree predicate) and stamp a :class:StatusVerdict: a verified tier (grounded/measured) orinferred, plus the ORTHOGONALstale/unresolvedtaint flags. Tether verification is injected as a :class:TetherResolverso the engine stays pure (all IO lives behind the resolver's callbacks; the same node + resolver always yields the same status). -
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. -
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.EpistemicStatusenum (kept in lockstep with :data:zettelkasten.graph.VALID_EPISTEMIC_STATUS); inference forms are the :class:~zettelkasten.synapse.memdsl.schema.InferenceFormenum; thecontestedoutcome and the contradiction/qualification trigger reuse the existing :data:zettelkasten.graph.VALID_RELATIONS. No parallel vocabulary is invented (see the module NOTES for thecontestedmodelling decision). - Orthogonal taint.
staleandunresolvedare flags, not tiers: a claim can be simultaneouslygrounded-tier andstale. A single wire :class:EpistemicStatusis derived on demand via :meth:StatusVerdict.wire, which floors any tainted or contested verdict toinferred(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
tainted
property
¶
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.
wire ¶
wire() -> EpistemicStatus
Collapse to the single :class:EpistemicStatus a renderer emits.
The mapping is deliberately conservative — taint NEVER inflates:
- a
contestedverdict →inferred(contested evidence is not verified); - a
staleorunresolvedverdict →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
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, anapplies/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
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
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
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
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
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 togrounded. Valid only as a non-emptypath@shapin: 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 unverifiableNone) → grounded-tier +stale. An UNPINNED payload (no@sha) is grounded-tier +staleSTRUCTURALLY — there is no committed sha to check drift against — regardless of what the resolver says. - dataset hash (
hash:) → aspires tomeasured. Matching → measured; mismatched/unverifiable → measured-tier +stale. - citation (
cite:) → aspires togrounded. Resolves → grounded; dangling/unverifiable → grounded-tier +unresolved. - decision-tree predicate (
predicate:) → aspires togrounded.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
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 | |
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
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:
- Taint union (orthogonal).
stale/unresolved/contested/extrapolationare OR-ed across all inputs (with the explicitcontestedsignal). A conclusion resting on any stale premise is stale; on any unresolved reference is unresolved; on any contradicting/qualifying evidence is contested. TheoutdatedDISCLOSURE is unioned SEPARATELY (kept apart from thestaleunion): a conclusion resting on any committed-drift premise is itselfoutdated, but — like the flag it carries —outdatednever downgrades the tier or the wire status, so anoutdatedgrounded conclusion stays grounded. - Form base tier. The rule for
formproposes a base tier:RESTATE→ the strongest input kind (preserve);RECOMPUTE→measurediff every input ismeasuredANDderivation_available, elseinferred(cannot claim measured without the derivation);DEDUCE→grounded(a conclusion grounded in its premises);GENERALIZE→inferred;ANALOGIZE→inferredand sets theextrapolationmarker. - Weakest-link cap. The base tier is capped by the weakest input: a
verified base cannot survive an
inferredinput, so it is floored toinferredwhen any input is inferred (a two-rank lattice). - Wire floor (on read). The resulting verdict keeps its tier and taint
independently; :meth:
StatusVerdict.wirelater floors any tainted or contested verdict toinferred. 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
545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 | |
contested_from_relations ¶
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
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_statuscontributes its per-node verdict; a ref ABSENT fromnode_statusis an unresolved reference and contributes aninferred+unresolvedverdict, so a dangling cite taints the step. - A premise contributes its already-re-derived verdict.
- The step is contested when a
contradicts/qualifiesedge joins two of its cited nodes (see :func:_contested_cites). derivation_availabledecides whether arecomputationstep may claimmeasured; it defaults to "the claim carries a non-emptyderivation".
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
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
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
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.