Skip to content
🚧 Early alpha — building the foundation. See the roadmap →

Mapping storage: two forms, notes or one table, pinned per set

Created Updated

A mapping set (plain: one published crosswalk release, a list of “this concept corresponds to that concept” rows) used to become one note per row. That is right for review and Bases, and wrong for a set with tens of thousands of rows. Crosswalker now offers two storage forms: Notes (unchanged, still the default) and Table (one .mapping-table.tsv file that opens in any spreadsheet). The form is chosen once, when the import set (plain: the collection of files one import owns) is minted, and a refresh keeps it. Switching forms later will be a resumable conversion job, not an edit.

Take a synthetic mapping set from framework demo-a to framework demo-b with 40,000 rows. In Notes form that is 40,000 crosswalk edge notes, each with about 16 managed fields. Every one is a vault entry to index, sync and back up; on a shared network drive a refresh touches all 40,000. The same set in Table form is one file with 40,000 lines.

The code survey behind the decision found that the note is the unit of storage everywhere: the query database projects one file to one mappings row, the SSSOM and OLIR exporters walk mapping notes, and the stack completion screen counts notes. Ownership is whole-note per import set. So a smaller footprint needed a real second form, not a tweak.

Three options were weighed:

OptionFiles for the exampleKeeps per-row detailCost
A. Links written onto framework notes0 new, but rewrites framework notesNo, or only in the query databaseNew per-key ownership; refresh churns framework notes; reverses two logged decisions (the edge-as-note model and “a crosswalk column writes nothing into the concept note”)
B. One table file per mapping set1Yes, full fidelityLoses Obsidian-native views for that set
C. One note per source concept, rows groupedHundreds to low thousandsOnly as parallel arrays, not queryable per rowPartial Bases support

B was recommended. The owner accepted B and kept today’s form beside it.

3. The owner’s direction (2026-09-29, in substance)

Section titled “3. The owner’s direction (2026-09-29, in substance)”
  • Portability is the problem. Notes are portable inside Obsidian (Bases, backlinks, graph). A standard table is portable outside it (spreadsheets, SSSOM tools). Neither wins for every user, so ship both.
  • Locked per set. The form is fixed when the set is minted and recorded in its provenance, the same way identity schemes are fixed. A refresh keeps it.
  • No notes on demand in table form. One source of truth per set: review happens in the table (or in a later plugin view), or the set is converted to notes.
  • Lossless both ways. Every managed and user-owned field has a place in both forms.
  • Conversion only if resumable and fault tolerant. An interruption at any step must be continuable by rerunning, the source is never deleted before parity is proven, and a half-finished conversion is never silently rolled back.
  • Default stays Notes. Table is opt-in.
#DecisionWhy
1Two forms, notes and table, recorded as _crosswalker.import_set.mapping_form (Tier 1 schema 1.6.0, optional; absent means notes)Existing sets are never rewritten to gain the field
2Pinned at mint, kept on refresh. A refresh through the wrong form is refused with an action (“Convert the set instead”)Refresh is chosen, never guessed: the plugin never switches form on its own
3Default Notes. Opt-in through the Store as choice on the stack review screen and the standalone mapping import, pre-filled from the Default mapping storage settingUI first; no profile concept exists yet, so a setting is the opt-in
4The table is a Crosswalker ledger that is SSSOM-shaped, marked by a crosswalker_format header. predicate_id holds the STRM predicate; sssom_predicate holds the original SKOS predicate. The standards-pure export stays the SSSOM exporterKeeping STRM in place needs no lossy reverse mapping on read
5The file header carries the set’s whole provenance block onceA table set has no notes, so the header is the only record that the set exists
6Row identity is a hash of the assertion facts (subject, predicate, object, modifier, mapping set id) plus an occurrence ordinal ordered by assertion facts only. Never a file pathIdentity, never path. Review columns are excluded from the ordering so a review edit never renumbers a sibling
7Review columns (review_status, reviewer, notes) live in the file and are carried across refresh by row id. Rows the source dropped are removed and countedA refresh that wiped reviews would be the user-preserve regression in table form
8Discovery is by the .mapping-table.tsv suffix, never by folder. A set seen as both notes and a table, or in two tables, is refused by nameThe conversion job will lean on this refusal

5. Trade-offs, stated at the point of choice

Section titled “5. Trade-offs, stated at the point of choice”
Lost in Table formWhy
Bases views for that setBases reads only Markdown file properties
Graph view, backlinks, and the endpoint wikilinksObsidian resolves links only inside .md files
Per-mapping notes to open, tag or linkNone exist

Gained: one vault entry per set, a file any spreadsheet opens, review columns in the file. When Table is selected the review screen says: “One file that opens in a spreadsheet. These mappings will not appear in Bases views, graph view or backlinks.” The setting description says the same.

SlicePRWhat it delivered
1. Codec and form pin#21Pure table codec with a fixed column order, deterministic output, and lossless note-to-row-to-note conversion; the mapping_form type, reader and validation (unknown values refused, never coerced)
2. Read everywhere#22Provenance in the table header; occurrence-aware row ids; one shared reader used by the query database projector, the SSSOM and OLIR exporters, and import-set discovery; a bad row still counts its set and blocks pruning; schema 1.6.0 and the provenance writer stamp the pin
3. Write and choose#23Table writer with read-back verification; the importer branch covering stack, wizard and every mapping source kind; refresh with review carry-over; set-qualified file names when two sets map the same pair; Store as on the stack and the standalone import; the setting; file counts treat a table as one file
4. Resumable conversion#24Convert a mapping set between notes and a table, in either direction, from Installed stacks; a visible <set-id>.converting.json marker records the phase; the new form is written and verified row by row (identities, assertion facts, every review and user field, tags and titles) before the old form retires to the vault trash; Cancel is available until retirement begins; an interrupted conversion is picked up by a startup notice or the Finish interrupted mapping conversions command
5. Table review view#25A windowed grid for reviewing a table-form mapping set: search, filter and sort, status counts, bulk edit of status or reviewer, and verified debounced saves that reload and reapply pending edits if the file changes underneath; opens from the Review mappings button on a table set in Installed stacks, the Review a mapping table command, or the .mapping-table.tsv file directly through the Open mapping tables in Crosswalker setting

Remaining:

  • Table rows and the identity index (ruled 2026-09-30): table rows are reconciled inside their file by row_id; the identity index stays markdown-only; import-set discovery is the existence authority for both forms. A #row_id claim in the index would reach the note rename step, so guard tests pin that the two never meet.
  • Edge curies and the Tier 1 curie pattern (ruled 2026-09-30): the recorded gap was false, since both edge-curie sanitizers emit a subset of the pattern. The real failure was a source subject_id or object_id that is not a curie, which failed at write time with a schema message; the importer now refuses those rows early, by row number and value, in both forms, and nothing is written.
  • Inline crosswalk columns on a framework slot stay as notes for now.