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

Shape-first workbench architecture — synthesis (one live screen, one StructureMapping, connected-by-default output)

Created Updated

The import wizard stops being a 4-step form and becomes a mapping workbench: one live screen where you shape a source into vault primitives and watch the vault render back, plus a review screen that answers where/what/why before Generate. Underneath sits one model — StructureMapping — read and written by three progressive views. Output is connected by default: every relation the source encodes must exist as a graph-visible artifact. And tests now assert the consumer’s view of the generated vault, not each module’s own boundary.

DecisionVerdictShipped
Step wizard → one live workbench + one review screenAdopted649c721a, 9822e72e
One StructureMapping model, three views (preset → cards → matrix), view-coherence lawAdopted52796e32
Level-count-agnostic presets with quantifier semantics (every_level · leaf · tail)Adopted52796e32
Six-primitive destination palette (folder · file name · tag · heading · link · property)Adopted52796e32, b612918e
Single-structural constraint + unique-leaf eligibility gateAdopted5957c159, e10bb8e3
Connectedness mandate — vaults must be graphsAdopted (mandate); enrichment pass pending7118faba, b612918e
Consumer’s-view testing doctrine (L2–L5)Adopted26fec0ac
Global shape chips (M0 first draft)Rejected — tri-state incoherence over a per-level matrix (§7a)—
Pass 1.5 batch enrichment, materialize/own-note, folder-note relocationDeferred — designed, not yet built—

“You should be able to do all of this from one or two screens, with limitless capability.” (project owner, mockup review 5). The old step sequence survives only as zones of a single live screen:

ScreenZonesAbsorbs
1 — mapping workbenchsource rail (file + shape chips, column list with detection badges, sample rows) · mapping canvas (preset bar → shape cards → matrix) · vault preview rail (live folder tree + one rendered note + deviation banner)old Steps 1–3
2 — review + generatedestination block (where) → shape-map recap (what) → stat chips → deviation banner → provenance line (why trust); Generate is confirm + progress + result onlyold Step 4

Edit anywhere, see everywhere: the preview rail re-runs the real render() over the sample on every change (debounced). Limitless capability = the matrix; simplicity = the preset fills it and most users only touch the cards. Review screen is a true review, not the workbench pushed down with counts appended (§7j reversal).

2b. One model, three progressive views (the view-coherence law)

Section titled “2b. One model, three progressive views (the view-coherence law)”

Everything the translation engine does is one pipeline: split the source structure into ordered levels → regroup (merge adjacent, drop some, leave the rest packed) → assign each group to Obsidian primitives → set policies (ragged, placement, depth). The folder-tag-sync transfer primitives are the theory anchor for the regroup gestures (merge = aggregation, drop = truncation, remainder-in-name = flattening-to-leaf, re-root = promotion).

The model is a per-level rule table (StructureMapping = { levels: LevelRule[]; tail?: TailRule }), where a LevelRule carries plural destinations (a level can land as folder AND property AND tag at once), a naming mode, and a per-level missing policy; the TailRule IS the variadic block (“however many levels remain”).

ViewWho it servesWhat it does to the model
1. Preset pickereveryone, zero configfills the entire matrix (detection × preset); the matrix is never empty
2. Shape cardsmost userscoarse toggles — ticking “Tags” adds/removes a tag destination across structural rows; cards are a summary render of the matrix
3. The matrixpower users, complex sourcesfull per-level control: grouping + join text, multi-destination, naming incl. lookup, per-level missing policy, tail rule

The law: every view reads and writes the same StructureMapping — opening a deeper view always shows exactly what the shallower one wrote. No hidden state, no separate “simple mode” codepath. A simple import never opens the matrix; a hard import never needs raw JSON (which stays the escape hatch). This is the UI-first, config-as-escape-hatch mandate applied to one screen.

2c. Level-count-agnostic presets (quantifier semantics)

Section titled “2c. Level-count-agnostic presets (quantifier semantics)”

A preset is a policy document that never names columns or level counts — it speaks in quantifiers: every_level · leaf · tail (+ facets, links sections). That is what lets one preset serve any ontology, and what makes it a code-level escape hatch (a schema-validated presets/*.json, writable without the UI, shareable). The instantiation pipeline is the intentional relationship between the chooser and the matrix:

  1. Preset = generic quantified rules.
  2. Detection supplies the actual levels/facets of this source.
  3. Instantiation stamps preset rules onto concrete matrix rows: preset × detection → StructureMapping.
  4. The matrix IS the instantiated preset, made editable. Any edit beyond the preset flips its chip to Custom (based on X).
  5. Save-as offers two distinct artifacts: save as preset (re-generalizes to quantifiers) or as saved mapping (fingerprint-bound to this source). Never conflated in UI copy.

No-preset-selected is impossible by construction: a built-in default always instantiates. Provenance badges — Built-in · Yours · Custom (based on X) — carry through the preset dropdown and the review-screen line; the detection default is explicitly tagged Recommended, not silently preselected.

The palette is not four shapes — it is all six Obsidian structuring primitives, each an already-wired recipe region, each rendering the user’s own first rows through the real pipeline:

CardAffordanceRecipe regionInfo-sci role
Folders — nest inside each otherbrowse down in the file explorerlayout folder entries / variadicpre-coordinated hierarchy
File names — structure stays in the namekeep it flat; the id reads at a glancefile template (id keeps packed form)packed notation
Tags — nested labels, many per notefilter any combination in search/Basesalso_emit.tags (#technique/T1055)faceted classification
One file — outline of headingsread top-to-bottom; one portable docheading entries + level_depthdocument order
Links — each note points at its parenthop between notes; local graph shows the familymanaged frontmatter wikilink (parent: "[[T1055]]")polyhierarchy
Properties — levels as structured fieldsgroup/sort/filter by level in Basesmanaged frontmatter plain valuesfaceted metadata

Cards are toggles, not radios — the recommended default is a mix (folders + facet tags + a parent link is the expected common trio). Every chooser arrives with the recommended option ticked, and accept-all-defaults must produce a good vault (the magic path); every tick is changeable (the control path). The UI speaks affordances; the info-sci lineage appears only as an optional whisper linking the KB (progressive disclosure). This grounds the wizard in the same data-morphology vocabulary the query layer already speaks.

2e. The single-structural constraint + unique-leaf gate

Section titled “2e. The single-structural constraint + unique-leaf gate”

One recipe = one primary address per row, so exactly one mapping may carry structural destinations (folder/file/heading); all others must be metadata-only. Two structural mappings concatenate into one layout and compose garbage paths (T1055/T1055.011.md/defense/defense-evasion.md). Enforcement is layered: detection’s distinctness guard keeps facets from reading as packed ids (the eligibility gate), the view-model disables structural options on other mappings while one owns the path (disabled-with-reason), and toRecipeRegions rejects a second structural mapping loudly. This is the model-level statement of the interleave bug fixed in 5957c159 / e10bb8e3.

3. The connectedness mandate — vaults must be graphs, not filing cabinets

Section titled “3. The connectedness mandate — vaults must be graphs, not filing cabinets”

The trigger (project owner, 2026-07-10): imported the MITRE subset, filtered the graph to Frameworks, and got zero interconnection. Verdict — “the import process is way more limited than what I’m expecting.” The bar is the owner’s ICSB pipeline: edges as a first-class artifact, rich documents (real body prose, headings, aliases). Crosswalker’s output must be a hyperconnected knowledge base — simple out front via defaults, deeply extendable underneath (the preset philosophy applied to output richness).

Autopsy — the four causes:

#CauseSeverityResolution
1Unquoted YAML wikilinks — parent: [[T1078]] parses as a nested array; Obsidian indexes NO link. Every parent edge silently deadcritical bug✅ 7118faba — quote YAML-structural leads; regression-pinned
2Empty bodies — description crammed into a property; body destinations never flowed to generationhigh✅ b612918e — body columns become documents
3Dropped tags/aliases — render() emitted them, the note writer dropped themhigh✅ b612918e — engine now emits tags + aliases
4Link poverty by design — only child→parent; no children lists, facets are dead text, no hub notesarchitectural→ Pass 1.5 enrichment (deferred, §5)

The principle: every relation the source encodes must exist as a graph-visible artifact — folders alone are invisible to the graph. The enforcement is a link-count guardrail: the review screen shows a connectedness stat (N notes · M folders · K graph edges) so shipping 0 edges is impossible to do silently (a run with 0 renders the stat in warning color). Causes 1–3 are fixed; cause 4 is the Pass 1.5 batch-enrichment design — children lists on parents, materialized facet/hub notes, folder-note placement — one deterministic batch pass that wires the schema-reserved graph_edges region without breaking render() purity or the closed 5-mechanism grammar.

4. The testing doctrine — assert the consumer’s view

Section titled “4. The testing doctrine — assert the consumer’s view”

The trigger (project owner): “How is this not caught in testing?” The YAML-quoting bug shipped through 745 green tests and screenshot-verified E2E, and the generated vault had zero graph edges. Root cause: module-boundary assertions, never consumer-boundary assertions. The real consumers — a YAML parser, Obsidian’s link indexer, the vault graph, a human reading a note — had no voice in CI. Screenshots verified the wizard, never the output vault.

LayerAssertsStatus
L1 Uniteach module’s own contract✅ healthy (745)
L2 Consumer contractsgenerated text as parsed by the consumer — every note’s frontmatter round-trips through a real YAML parser and deep-equals the intended object; link values parse back as [[...]] strings (the quoting bug fails here instantly)✅ 26fec0ac
L3 Golden vaults + invariantsthe full headless pipeline over real corpus subsets → committed golden snapshot + corpus-independent invariant suite✅ 26fec0ac
L4 Output screenshotsscreenshot the generated vault — graph filtered to output (a dead graph is visually unmissable) + one note in reading view (empty bodies obvious)partial — extend after UX agent lands
L5 Product guardrailreview screen shows “K graph edges” before generate; 0 renders as a warningspecced (§3)

L3 invariants (the vault-level truths): every note’s YAML parses; every emitted wikilink resolves to a generated path; connectivity — no unexpected orphans (MITRE parent column → orphan count 0, edge count over 0); no .md/ inside any path; determinism (run twice → byte-identical); golden drift is a review gate. These invariants caught the interleaved-path bug on the real corpora before a human saw it — the composition-seam bug that every module’s own green tests were blind to. Process rules now: every corpus in test-vault/Crosswalker Test Data/ gets a golden test; a manual-testing bug becomes a failing test first, then a fix; a new emission surface ships its L2 contract in the same PR.

Prior decisionHow this refines it
Ch 42 — the variable-depth question (open-question log)Answered for §1–3: variadic folder expansion + the tail rule shipped (9ffd5fd3). Ragged ids (the case the old 80% uniform threshold silently flattened — why ATT&CK imported flat) now nest via the TailRule. Ch 42’s deeper materialize/own-note surface stays open (§5 table below)
Parent-note placement (variadic design §4)Resolved as a user choice — sibling vs folder-note, default sibling, shown as side-by-side mini file-trees built from the user’s own ids. Folder-note relocation gated on Pass 1.5 (batch knowledge required)
Wizard-parity direction (UI-parity audit, essence log)Concretized: the wizard now speaks the shape vocabulary the query layer already spoke; every UI state serializes to a recipe region 1:1 (the parity contract); useAs values became derived serialization, not user-facing primitives
Global shape chips (M0 first mockup)Superseded — a global “tags ✓” toggle is indeterminate the moment matrix rows diverge (§7a tri-state incoherence). The mapping zone is a list of per-detection mappings, each with its own scoped cards; matrix = truth, cards = per-mapping summary
ItemShapePointer
Pass 1.5 folder-note relocationX.md → X/X.md + inbound-link rewrite; the risky re-import curie-lookup integration pointPass 1.5 design §3–4; waits on the engine round
materialize / own-note serializationmaterialize: boolean per level — does a category/parent become a real concept note? The biggest new design surface; interacts with curie-per-intermediate-level and re-import identityspec §7b; spike alongside Pass 1.5
Per-level missing policy surfacemissing: skip | fallback | error per level needs a recipe surface (today global-ish)spec §3 serialization gaps
Lookup namingnaming.lookup — a level’s label drawn from a sibling column (GV — Govern); needs the level’s sibling columns in scopespec §3a½
R6 positioning questiongeneral-ontology engine vs GRC-first framing — where the shape-first workbench lands the product storywhat makes Crosswalker unique; decision-spine Related