Matrices¶
A matrix is the one rendered grid output of the zettelkasten — rows against typed columns, every cell grounded in the corpus. It arises two ways, and keeping them apart is the whole point of this page. One is the single-spine matrix — the flat grid a single spine definition projects, its row axis against its own columns. The other is the spine-group matrix — the Layer 2 comparison surface that appears when several spines built from the same spine-schema line up into one grid. Both render through the identical matrix UI, but they answer different questions and are produced by different machinery.
- Matrix engine:
zettelkasten/tables.py - Identity-stable reprojection:
zettelkasten/reconcile.py - Registry it reads:
zettelkasten/organizations.py

A matrix in the Workshop: rows are a spine's dimensions, columns are what each row is compared on, and every cell is a grounded synthesis routed from note membership.
Two sources, one grid¶
The single-spine matrix is the grid of a single spine
definition: its row_axis enumerates the rows, its columns are the
dimensions, and each cell is filled by deterministic routing (exact-tag or
spine-side membership) plus optional agent synthesis. Rows can be sources, notes,
tags, note types, or a group aggregation — the row axis is selectable and
symmetric with the columns (Configurable matrix row axis (pivot) + builder wizard, Generalizable Group row axis (field/link/semantic aggregation) replaces the stubbed Spine axis). A proposed
spine is live and read-only; a saved matrix is the same grid cached under
_reviews/<name>.tables.json (v2 cells carry both their members and a
summary). This is a matrix of one spine.
The spine-group matrix is the Layer 2 object. It is what the organizing principle is ultimately for: many spines, each built from the same spine-schema, folded into a single comparison (Organizing principle: semantic base -> spine -> matrix (two kinds of uniting)). Here rows are the spine apexes (the topics) and columns are the shared dimensions. It is a matrix across spines.

A spine-group matrix: three same-spine-schema spines (rows = topics) folded against their shared dimensions (columns). Each cell rolls up its member notes, shown by the stacked cards and +N pill.
How same-spine-schema spines fold together¶
The alignment key is deliberately simple. A spine's column_key_fingerprint is
just its sorted set of column keys — two spines share a spine-schema iff they
expose the same dimension-tag set, regardless of per-spine labels or graph names.
group_spines_by_spine_schema() buckets promoted spines by that fingerprint into a
spine-group and only forms a group when at least two spines share it; a
singleton spine-schema stays an individual spine and is not grouped.
spine_group_matrix() then builds the read-only grid: each row is a member spine's
apex, each cell reads that topic's dimension-node body (falling back to the joined
evidence titles when the node has no synthesized body yet) plus its rolled-up
evidence members for drill-down. The grid is ephemeral — materialized at view
time and never persisted unless the populated-matrix wizard chooses to.
This is exactly the distinction that once caused confusion with the persona spines: running the extraction six times produced six standalone apexes (six Layer-1 spines) but no Layer-2 grouping, so the UI showed six separate spines instead of one comparison. A shared spine-schema — the same column-key fingerprint across the group — is precisely what turns independent strands into a matrix (Organizing principle: semantic base -> spine -> matrix (two kinds of uniting)).
Spine-schema is not the extraction schema
The spine-schema here is the shared column-key fingerprint that groups
sibling spines — a grouping rubric. It is distinct from the
grounded-extraction schema, the extraction
rubric a run reads sources against (create_extraction_graph, the entries in
extraction-schemas.yaml). The spine-schema is derived from that schema's
dimension tags, but the two name different jobs; the extraction layer keeps
the plain word schema, and the grouping layer is always spine-schema.
The populated matrix builder¶
Building a spine-group matrix is a curate-and-fill flow, presented side by side: on
the left you curate the rows and columns; on the right a streaming populated grid
fills in as you go. The step that makes cells read like a comparison rather
than a bag of titles is per-cell paragraph synthesis. At build time,
_synthesize_dimension_paragraph() authors a short 2–4 sentence, strictly
grounded portrait of how one topic handles one dimension, over that cell's routed
member claims and their verbatim quotes. The paragraph is then persisted back
onto the backing spine dimension node and its status flipped to
synthesis_status='materialized', so it shows everywhere the node is read — the
matrix, the spine's own views, and any ephemeral comparison.
Materialization is careful about not clobbering work. A node is authored only
while it is still a spine skeleton (a placeholder body or scaffold status); a body
that a human or agent already materialized is never overwritten unless
force is set. Cells with no members are left blank — there is nothing to
ground — rather than fabricating prose.
Per-row staleness and drift¶
Because a spine-group matrix is assembled from independent member spines, its freshness is naturally per row. Each row is one spine, and a row goes stale when its member spine drifts — new in-scope notes attach to no dimension, or its membership no longer matches what the grid last recorded. Rather than a single global "this matrix is stale" badge, staleness is attributed to the specific rows whose backing spine changed, so a resync can target exactly the spine that moved. The cheap drift signal is the count of in-scope notes attached to no cell — the "N new notes unrouted · Resync" cue — surfaced without rebuilding the grid.
Reconcile: identity-stable reprojection¶
A matrix is rebuilt constantly, and every rebuild is a chance to lose identity.
If a row hub, an induced column, or a depth node were keyed on its position or
label, then renaming a column or drifting a cluster by one member would orphan
the prior durable id — and with it every spine-member edge and human
correction hanging off that node. The fix is to derive identity from
member-set overlap instead.
reconcile.py
is the pure primitive for this. reconcile_ids(prior, proposed, threshold=0.5)
computes an optimal (maximum-total-overlap) one-to-one assignment between the
prior materialized sets and the newly proposed ones, reusing a prior durable id
whenever a proposed set substantially overlaps it (Jaccard ≥ threshold) and
minting a fresh id only when nothing overlaps enough. The matching is globally
optimal rather than order-greedy, so it never orphans a prior id that a different
pairing could have preserved (Optimal matching + dashboard reconcile parity (closes the two follow-ups)). It is deterministic and pure —
no graph, no IO — and the greenfield case (no prior spine) is byte-identical
to minting every id in order.
That one primitive is reused everywhere identity must survive a rebuild: remine's
column reconcile, induced dimensions, matrix row and column re-materialization, and
spine promotion. It is the reason a corrected matrix stays corrected
through the next rebuild. (This is distinct from review.reconcile(), which
merges a deterministic projection with an author overlay — a different concern.)
Where remine fits¶
Deterministic routing only sees exact-tag matches and spine-side membership — a note that belongs to a dimension but was never tagged for it is invisible to the grid. Closing that gap is the job of remine, the agent-semantic tier that classifies notes into dimensions (and can induce the dimensions themselves), then applies an approved grid back through spine promotion. A matrix is the surface; remine is how you fill and shape it when tags alone are not enough.
Design notes¶
The decisions behind this, drawn as a slice of the memory tree.
- D Organizing principle: semantic base -> spine -> matrix (two kinds of uniting)active
The canonical conceptual model for the whole zettelkasten organization layer. - D Configurable matrix row axis (pivot) + builder wizardactive
Generalized the synthesis matrix from hard-wired "one row per source" to a selectable row axis, symmetric with columns. - D Generalizable Group row axis (field/link/semantic aggregation) replaces the stubbed Spine axisactive
The matrix row builder could not express "a row per persona" (or any aggregation into an emergent entity). - D Optimal matching + dashboard reconcile parity (closes the two follow-ups)active
Resolved both follow-ups left from the identity-stability hardening.
- R Angelo
- P zettelkastenactive
- P Phase 2: Literature Review Systemactive
- P Workshop tab: Reading Syllabus (learn) + claim-centric Review/Outline (write)active
- P Phase M3: Review authoring + generative loopactive
- D Configurable matrix row axis (pivot) + builder wizardactive
- P Organizations: project-level lenses + spines (matrix builder redesign)active
- D Generalizable Group row axis (field/link/semantic aggregation) replaces the stubbed Spine axisactive
- P Schema matrix views (ephemeral hub) + user-facing Spines renameactive
- D Organizing principle: semantic base -> spine -> matrix (two kinds of uniting)active
- P Re-mining: Schema-Aware Structure Induction over existing notes (constraint lattice)active
- P Hardening: identity-stability invariant (overlap reconcile)active
- D Optimal matching + dashboard reconcile parity (closes the two follow-ups)active
- P Hardening: identity-stability invariant (overlap reconcile)active
- P Phase M3: Review authoring + generative loopactive
- P Workshop tab: Reading Syllabus (learn) + claim-centric Review/Outline (write)active
- P Phase 2: Literature Review Systemactive
- P zettelkastenactive
Related¶
Semantically related entries from the memory graph.