zettelkasten.synapse.memdsl¶
zettelkasten.synapse.memdsl ¶
MemDSL: the versioned, bidirectional, machine-checkable synapse contract.
MemDSL is the CONTRACT (not a compression scheme) through which the deterministic synapse engine renders a heterogeneous knowledge substrate to a reasoning agent and the agent returns actions plus a cited reasoning-DAG the engine can verify. It is the first synapse workstream: substrate, epistemics, the grounding router, navigation, and the renderer all consume these types, so they are designed to be importable and stable.
The two directions of the contract:
- RENDER (engine → agent) — an :class:
~.schema.Envelopeof :class:~.schema.RenderedNodebodies, each carrying a unified :class:~.schema.Address, an engine-emitted :class:~.schema.EpistemicStatus, provenance handles, and :class:~.schema.Affordancehandles, plus first-class :class:~.schema.GapTokenabsences. - OUTPUT (agent → engine) — an :class:
~.schema.AgentOutputof :class:~.schema.Actionmoves (an OPEN registry) and a cited :class:~.schema.ReasoningDAGof :class:~.schema.Claimnodes, or a labeled :class:~.schema.Abstention.
An :class:~.schema.ActionResponse is the engine's versioned mid-loop reply to a
single action (read results in v1; shaped to report state mutations + impact
sets later). The (de)serializers live in :mod:~.parser and are deterministic
and injection-safe.
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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 | |
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
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
body_codec ¶
Return the (encode, decode) codec for body_kind, defaulting to text.
get_action_spec ¶
get_action_spec(name: str) -> ActionSpec | 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
register_action ¶
register_action(spec: ActionSpec) -> None
register_node_type ¶
register_node_type(spec: NodeTypeSpec) -> None
assign_refs ¶
assign_refs(nodes: list[RenderedNode], *, prefix: str = 'n', start: int = 1) -> list[RenderedNode]
Assign stable, deterministic envelope-local refs (n1, n2, …) in order.
Mutates each node's ref in list order and returns the list, so the same
node sequence always yields the same refs — the "stable id assignment" half
of deterministic rendering. Ordering of the nodes themselves is the caller's
responsibility (the serializer preserves it).
Source code in zettelkasten/synapse/memdsl/parser.py
parse_envelope ¶
parse_envelope(text: str) -> Envelope
Parse MemDSL render text back into an :class:Envelope.
Raises :class:MemDSLParseError on a bad/foreign header or malformed block.
Source code in zettelkasten/synapse/memdsl/parser.py
parse_output ¶
parse_output(text: str) -> AgentOutput
Parse agent OUTPUT text into an :class:AgentOutput.
The required @memdsl.out/<ver> header is what makes this injection-safe:
a render envelope or a raw untrusted node body has a different (or no) header,
so it is rejected here rather than mined for actions. Within an @answer
block, v1 carries claims/conclude and v2 additionally requires
prose plus exact cite bindings.
A PURELY SYNTACTIC, MARKER-DELIMITED pre-parse recovery
(:func:_recover_output_envelope) first unwraps a single, exactly-matched
enclosing markdown code fence (with only blank lines outside it), so a
production LLM's common near-miss still yields an answer. It trims NOTHING it
would have to classify as prose-vs-structural — only a matched fence pair's
wrapper and leading/trailing blank lines — and hands the envelope interior
VERBATIM to the strict parse below, which requires the first non-blank line to
be the header and consumes to the end or refuses. So recovery can launder
nothing: a pristine structural line OUTSIDE the recovered core (pre-header
preamble, or outside a fence) is never dropped — it stays in the interior and
is rejected here, or lies outside the fence and refuses — and everything below
fails-closed.
Exactly one terminal decision is allowed: @answer XOR @abstain, once.
A second terminal, or any action AFTER a terminal, is a
:class:MemDSLParseError — a turn ends at its single terminal decision, so a
trailing action can never silently ride along after it.
Source code in zettelkasten/synapse/memdsl/parser.py
1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 | |
parse_response ¶
parse_response(text: str) -> ActionResponse
Parse an :class:ActionResponse fragment back from MemDSL text.
Source code in zettelkasten/synapse/memdsl/parser.py
serialize_envelope ¶
serialize_envelope(env: Envelope) -> str
Serialize a render :class:Envelope to MemDSL text (deterministic).
Source code in zettelkasten/synapse/memdsl/parser.py
serialize_output ¶
serialize_output(out: AgentOutput) -> str
Serialize an :class:AgentOutput (actions + terminal decision) to MemDSL text.
Raises :class:MemDSLParseError if both a reasoning answer and an
abstain are present (they are mutually exclusive terminal decisions).
The conclude line is emitted whenever the reasoning-DAG has claims: the
conclusion is canonicalized to the last claim id at DAG construction (see
:meth:ReasoningDAG.__post_init__), so there is no empty-conclusion case for
the serializer to drop. This is what makes parse(serialize(x)) == x hold,
closing the old asymmetry where the serializer omitted conclude but the
parser back-filled it.
Source code in zettelkasten/synapse/memdsl/parser.py
1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 | |
serialize_response ¶
serialize_response(resp: ActionResponse) -> str
Serialize an :class:ActionResponse fragment to MemDSL text.
v1 emits status + read results (nodes/gaps); the reserved mut/
impact lines only appear when future write actions populate them.