zettelkasten.synapse.memdsl.schema¶
zettelkasten.synapse.memdsl.schema ¶
In-memory contract types for MemDSL — the machine-checkable synapse protocol.
MemDSL is the versioned, bidirectional CONTRACT through which the deterministic synapse engine renders a heterogeneous knowledge substrate to a reasoning agent (the RENDER direction) and the agent returns actions plus a cited reasoning-DAG that the engine can verify (the OUTPUT direction). It is a contract, not a compression scheme: its job is to make epistemic honesty structurally enforceable — the engine emits every node's epistemic status, the agent can only cite, and any hole in coverage is rendered as a first-class gap rather than silently dropped.
This module defines the domain-agnostic in-memory representation only; the
line-oriented (de)serializers live in :mod:zettelkasten.synapse.memdsl.parser.
Nothing here bakes in domain content: the same types serve a schema table, a
document passage, a temporal series, or any of the zettelkasten note types.
Design invariants pinned here:
- Unified addressing (:class:
Address) —store:source:idplus an optional locator that encodes external-dataset provenance shapes (a passage char-offset span, a temporal as-of stamp) without a schema change. Thesession/workstore prefixes are RESERVED for future session-scoped mutable working state (v1 never emits them, but the scheme does not preclude them). - Engine-owned epistemics (:class:
EpistemicStatus) — a node's status is emitted BY the engine on the render side. The OUTPUT side (:class:Claim) carries only an :class:InferenceForm; there is deliberately no field through which an agent could assertgrounded. Its values mirror :data:zettelkasten.graph.VALID_EPISTEMIC_STATUSso the vocabulary cannot drift (guarded by a round-trip test). - First-class gaps (:class:
GapToken) — stale / unresolved / missing / elided absence is a rendered token, never a silent drop, so calibrated abstention can see the hole. - Open action registry (:data:
ACTION_REGISTRY) — the v1 read actions (expand/join/ground/prune/abstain/answer) are entries in a registry that new write/apply actions can extend WITHOUT re-cutting the grammar. - Registry-driven per-type bodies (:data:
NODE_TYPE_REGISTRY) — a new node type resolves to a default text body codec and degrades gracefully instead of crashing the parser. - Versioning (:data:
FORMAT_VERSION) — every serialized artifact embeds the version, and the action-response fragment is shaped to later report state mutations + impact sets even though v1 emits only read results.
MemDSLError ¶
MemDSLParseError ¶
Bases: MemDSLError
A serialized MemDSL artifact was malformed or of an unsupported version.
Raised by the parser on a missing/wrong header, an unparseable address, an unknown or badly-ar'd action, or a malformed reasoning-DAG. Unknown NODE TYPES are NOT an error — they degrade to the default body codec — so this is reserved for genuine grammar violations.
Source code in zettelkasten/synapse/memdsl/schema.py
EpistemicStatus ¶
Bases: str, Enum
The machine-checkable epistemic status the engine stamps on a node.
Values mirror :data:zettelkasten.graph.VALID_EPISTEMIC_STATUS exactly so
the render contract and the note store share one vocabulary. This status is
engine-emitted on the RENDER side only; nothing on the OUTPUT side lets an
agent assert it. UNKNOWN/unresolved absence is represented by a
:class:GapToken, not by a status value, so it is never confused with a
positive claim of groundedness.
Source code in zettelkasten/synapse/memdsl/schema.py
InferenceForm ¶
Bases: str, Enum
The reasoning form connecting a claim's cited nodes to the claim.
Domain-agnostic taxonomy the epistemics layer later maps onto
:data:zettelkasten.graph.VALID_RELATIONS for status propagation. Carried on
the OUTPUT side (:class:Claim) so the engine can re-derive a claim's status
from its cited nodes and its form, rather than trusting an agent assertion.
Source code in zettelkasten/synapse/memdsl/schema.py
GapKind ¶
Bases: str, Enum
The kind of a first-class absence token.
Absence is always rendered, never dropped, so a coverage-gated abstention can reason about the hole:
STALE— the node's tether drifted since it was pinned.UNRESOLVED— a referenced address did not resolve to a node.MISSING— an expected hop/relation is absent from the substrate.ELIDED— a body was truncated to a lossy view; agroundre-fetches it. Truncation must never silently break a verbatim-quote check, so the elision is explicit.OUTDATED— the node's git pin is a faithfully-cited-but-SUPERSEDED commit: the pinned bytes are still a retrievable immutable git object (so the node stays GROUNDED — this is a DISCLOSURE, not an abstain-forcing taint likeSTALE), but HEAD has since moved on, so a consumer must be told "grounded as of the pinned sha; the current file may differ".
Source code in zettelkasten/synapse/memdsl/schema.py
Address
dataclass
¶
A stable, collision-free, short unified address for a substrate node.
Canonical form is store:source:id with an optional trailing {locator}
that encodes external-dataset provenance shapes without changing the scheme:
- a passage char-offset span →
{#<start>-<end>}; - a temporal as-of stamp →
{@<stamp>}(e.g. an ISO 8601 instant); - both, in that order →
{#<start>-<end>@<stamp>}.
A schema table.column provenance needs no locator: the dotted column is
the natural id (e.g. sql:salesdb:orders.total). store, source
and id must be non-empty and free of the :/{/}/,/whitespace
separators; id may contain dots. , is forbidden because address
tokens are embedded in ,-delimited lines (cites/prov/aff/
impact); a comma inside a component would silently split one token into
two on parse. The session/work stores are reserved (see
:data:RESERVED_STORES).
Source code in zettelkasten/synapse/memdsl/schema.py
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | |
to_token ¶
Serialize to the canonical store:source:id[{locator}] string.
Source code in zettelkasten/synapse/memdsl/schema.py
parse
classmethod
¶
Parse a canonical address token back into an :class:Address.
Raises :class:MemDSLParseError on anything that is not a well-formed
store:source:id[{locator}] string.
Source code in zettelkasten/synapse/memdsl/schema.py
Affordance
dataclass
¶
A navigation handle the engine advertises FROM a rendered node.
verb is one of :data:AFFORDANCE_VERBS and target is the address
token (or a node ref) the move applies to — e.g. expand the neighborhood
of this node, or join it with another. Advisory: the engine still
validates any action the agent returns.
Source code in zettelkasten/synapse/memdsl/schema.py
RenderedNode
dataclass
¶
One node as rendered to the agent.
Carries a short, stable, envelope-local ref (e.g. n1) the agent cites
instead of the full address, the unified address, the node type (from
:data:zettelkasten.graph.VALID_TYPES, or an unknown type that degrades to
the default body codec), a single-line title, a body (which may be a
lossy view — see elided), the engine-emitted status, provenance
handles, and the affordance handles available from here.
When elided is True the body is a truncated/lossy view and a
ground(ref) re-fetches the full body; a verbatim-quote check must treat an
elided body as not-yet-fetched rather than authoritative.
Source code in zettelkasten/synapse/memdsl/schema.py
GapToken
dataclass
¶
A first-class rendered absence (see :class:GapKind).
address is the node/hop the gap concerns when known (e.g. the unresolved
reference, or the elided node), else None. detail is an optional
single-line human note. Rendered explicitly so calibrated abstention can see
the hole.
Source code in zettelkasten/synapse/memdsl/schema.py
Envelope
dataclass
¶
The render-direction payload: nodes + gaps the engine shows the agent.
Determinism is the caller's responsibility for ordering (nodes are rendered
in list order with their assigned refs — see
:func:zettelkasten.synapse.memdsl.parser.assign_refs) and the serializer is
a pure function of the envelope, so identical input yields byte-identical
output. focus optionally names the ref the render is centered on.
Source code in zettelkasten/synapse/memdsl/schema.py
Mutation
dataclass
¶
A state mutation reported by the engine after a write/apply action.
v1 emits none (the navigation loop is read-only), but the fragment is shaped
to carry them so v2 write actions need no grammar change. op names the
mutation kind, address the node it touched, detail an optional note.
Source code in zettelkasten/synapse/memdsl/schema.py
ActionResponse
dataclass
¶
The engine's versioned reply to a single agent action, mid-loop.
The navigation loop is render → action → action-response → action, so this
fragment is a first-class part of the contract. v1 populates only nodes
and gaps (read results); mutations and impact (the set of
addresses a mutation touched) are reserved for future write/apply actions and
stay empty. status is a short machine token (ok by default).
Source code in zettelkasten/synapse/memdsl/schema.py
RuleClass ¶
Bases: str, Enum
The graph-rewrite class of an action, in the sense of a design grammar.
An action is a production over the substrate view, so it carries the topologic/parametric distinction from grammar-based computational design synthesis (Königseder & Shea): a rule either changes the structure (which nodes/edges exist) or the parameters (a node's content/resolution) of the graph. This is engine-side metadata only — advisory, never serialized — that lets the (deferred) navigation/VOI policy reason about which kind of move it is spending budget on, and gives the reserved v2 write actions a principled axis:
TOPOLOGIC— changes the graph's structure. Read:expand/join/prunechange which nodes/edges are in view. Write (v2):assert/retractadd/remove a node,link/unlinkadd/remove an edge.PARAMETRIC— changes a node's content/parameters without changing topology. Read:grounddeepens a node's resolution. Write (v2):reviseedits a node body in place.TERMINAL— the whole-turnanswer/abstaindecisions, which end the loop rather than rewriting the graph.
Source code in zettelkasten/synapse/memdsl/schema.py
ActionSpec
dataclass
¶
One entry in the open action registry.
kind is read for every v1 action; write is reserved for future
apply actions (the registry is the extension seam — a new action is a new
entry, not a grammar change). terminal marks the whole-turn decisions
(answer/abstain) that end the navigation loop and are expressed with a
@-marker rather than the verb(args) call form. min_args/
max_args bound the positional arity (max_args=None is unbounded).
rule_class is the optional graph-rewrite class (see :class:RuleClass):
advisory engine-side metadata that never touches the wire, so it is purely
additive — an unclassified action (None) round-trips exactly as before.
Source code in zettelkasten/synapse/memdsl/schema.py
Action
dataclass
¶
A single non-terminal navigation action the agent returned.
Generic verb(args, kwargs) shape so the registry can grow. verb is
validated against :data:ACTION_REGISTRY at parse time; args are node
refs/addresses, kwargs an open map for future parameters.
Source code in zettelkasten/synapse/memdsl/schema.py
Claim
dataclass
¶
One node of the agent's cited reasoning-DAG.
cites are the node refs (or addresses) the claim rests on; form is the
:class:InferenceForm connecting them to the claim; premises are other
claim ids this one builds on (making the reasoning a DAG, not a flat list);
derivation is an optional derivation-expression (e.g. the arithmetic a
CALCULATION claim computed). There is deliberately no epistemic-status
field: the engine RE-DERIVES the claim's status from its cited nodes and
form, so the agent cannot assert grounded.
Source code in zettelkasten/synapse/memdsl/schema.py
ProseCitation
dataclass
¶
A binding from one prose assertion to the claim that licenses it.
MemDSL OUTPUT v2 encodes this binding as an exact quote anchor. The strict
parser resolves that quote to one half-open span [start, end) in
:attr:ReasoningDAG.prose; zero or multiple matches fail closed. In-memory
callers may still construct this type directly for compatibility; validated
OUTPUT v2 prose requires exactly one claim id per binding.
The gate enforces per-assertion CITATION-ANCHORING against this binding: the
assertion's content tokens must be licensed by a SINGLE cited verified claim
(closed vocabulary per claim, never the pooled union), and any prose token
outside every citation span is an UNCITED assertion. It is purely additive —
ReasoningDAG.prose_citations defaults to empty, so a legacy v1 DAG leaves
the gate on its conservative fallback path.
Source code in zettelkasten/synapse/memdsl/schema.py
ReasoningDAG
dataclass
¶
The cited reasoning-DAG returned with an answer.
claims are the DAG nodes; conclusion names the claim id that IS the
answer (defaults to the last claim when empty). This is what makes
"don't fabricate" enforceable: every claim is traceable to cited substrate
nodes and an inference form the engine can check.
prose is the answer text licensed for public projection. OUTPUT v2
serializes it with exact quote-anchored prose_citations; OUTPUT v1 omits
both fields and remains accepted byte-for-byte. Empty defaults preserve
construction compatibility for existing v1 and direct router callers.
The empty-conclusion default is resolved to the last claim id in
:meth:__post_init__, so the in-memory form is canonical: an omitted and a
last-claim-named conclusion are the same object. That canonicalization is
what lets the serializer always emit a conclude line and still satisfy
parse(serialize(x)) == x (the round-trip-symmetry guarantee).
Source code in zettelkasten/synapse/memdsl/schema.py
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 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 | |
validate ¶
Structurally verify the reasoning-DAG (raises :class:MemDSLParseError).
Enforces the epistemic-honesty guarantees the engine relies on, so a fabricated or malformed DAG is rejected at parse rather than trusted:
- Citation floor — every non-
assumptionclaim carries at least one citation. Anassumptionis the only form allowed to cite nothing; any grounding-bearing form (deduction/restatement/calculation/…) must rest on cited substrate, so an uncited deduction can no longer be the whole answer. - Premise integrity — every
premisesreference resolves to a defined claim, claim ids are unique, and the premise edges form no cycle (the reasoning is a DAG, not a tangle). - Non-empty answer — a zero-claim
@answeris rejected: an answer with no claims has no ground, so it can never be a valid answer (the claim-less path is@abstain, which never builds a DAG). - Grounded conclusion — the conclusion names a defined claim and its transitive premise closure bottoms out in at least one cited substrate reference, so an answer can never be assembled purely from uncited assumptions.
Direct citation existence (does n3 resolve to a rendered node?) is
deliberately NOT checked here: cites reference substrate the DAG alone
cannot see, so the grounding gate verifies them against the envelope.
Source code in zettelkasten/synapse/memdsl/schema.py
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 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 | |
Abstention
dataclass
¶
A labeled, calibrated refusal to answer.
reason is a short label (see :data:ABSTAIN_REASONS for the canonical v1
set — the field stays open so the router may add labels); detail is an
optional single-line note.
Source code in zettelkasten/synapse/memdsl/schema.py
AgentOutput
dataclass
¶
The output-direction payload: the actions + terminal decision an agent returned.
actions are the non-terminal navigation moves. Exactly one terminal
decision may accompany them: reasoning (an answer with its cited DAG)
XOR abstain (a labeled refusal). Both being set is a contract violation
the parser rejects.
Source code in zettelkasten/synapse/memdsl/schema.py
NodeTypeSpec
dataclass
¶
How a node type's compact body is encoded on the wire.
body_kind selects a codec in :data:BODY_CODECS. v1 ships a single
text codec (the body is plain, injection-framed text), but the registry
is the seam for richer per-type bodies (e.g. a schema descriptor for a data
node) without re-cutting the parser. An UNKNOWN type resolves to the default
text spec via :func:node_type_spec, so a brand-new node type degrades
gracefully instead of crashing.
Source code in zettelkasten/synapse/memdsl/schema.py
register_action ¶
register_action(spec: ActionSpec) -> None
get_action_spec ¶
get_action_spec(name: str) -> ActionSpec | None
register_node_type ¶
register_node_type(spec: NodeTypeSpec) -> None
node_type_spec ¶
node_type_spec(node_type: str) -> NodeTypeSpec
Return the :class:NodeTypeSpec for node_type.
Falls back to a default text spec for any unregistered/new type, which is
what lets an unknown node type round-trip rather than crash the parser.
Source code in zettelkasten/synapse/memdsl/schema.py
body_codec ¶
Return the (encode, decode) codec for body_kind, defaulting to text.