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

Query state location synthesis — Layout B+ (query-pack folder) locked

Created Updated

Where this log fits in the query-engine mental model

Section titled “Where this log fits in the query-engine mental model”
  • Layer A — Query primitives (the verbs; mechanism-neutral): filter / project / traversal / closure / anti-join / pivot / aggregate + joins (inner / left / right / full / anti)
  • Layer B — View shapes (the visuals; mechanism-neutral): table / list / pivot / hierarchy / graph / timeline
  • Layer C — Recipes (compositions; user-facing): primitives + shape + params (e.g. “Coverage Matrix” = filter + traverse + anti-join + pivot)
  • Mechanism axis (Obsidian rendering): Bases-native / registerBasesView custom view / codeblock processor / materialized snapshot

This log decides where query STATE lives (the durable artifact backing Layer C recipes); see Phase 5 scope log for where the Layer A primitive implementations live.

Layout B+ locked — every Crosswalker query is a per-query folder under _crosswalker/queries/<slug>/ with index.md as canonical state and view.base as generated sibling. Embeds are always explicit path-qualified ![[<slug>/view.base]]. The host note (the user-authored note that displays the query) gets no frontmatter from the plugin — just the embed. The folder is the durable envelope for Phase 5 materialization, v0.1.7 exports, and v0.1.8 audit snapshots.

This re-homes the Phase 4.5 architecture (frontmatter-canonical, recipe + params is the durable contract — all preserved); it does not revert it. The frontmatter just lives on a query-owned index.md, not on a user-authored host note.

Phase 4.6 sub-phase lands the migration before Phase 5 starts. Estimated effort: ½–1 day. Existing 359 Phase 4.5 tests guard the regenerator code path migration is built on; net ~+40 tests for slug derivation + collision + migration idempotency.

DateDecisionSource
2026-05-04Architectural commitments locked (5 + #6 Bases-not-Dataview)Foundation synthesis
2026-05-07Bases query layer architecture — Pattern B+DSynthesis
2026-05-08Ch 32a+b — intuitive query UXSynthesis
2026-05-15Phase 4.5 — frontmatter + .base + embed (current state being stress-tested)Phase 4.5 briefing
2026-05-18Ch 38 filed — stress-test Phase 4.5 query state locationBrief
2026-05-18Ch 38 deliverables A + B landed in parallel — convergent recommendationCh 38a · Ch 38b
2026-05-18 (this log)Layout B+ locked; Phase 4.6 sub-phase openedThis log

1. What both deliverables agree on (locked)

Section titled “1. What both deliverables agree on (locked)”
#ConclusionLocked
1.1Folder-pack layout wins. Phase 4.5’s flat _crosswalker/views/q-<id>.base dump has no answer for “where does the snapshot/export/audit go?” Layout A doesn’t scale to Phase 5+.
1.2Folder-note magic embeds are NOT vanilla Obsidian. ![[csf-coverage]] does not resolve to csf-coverage/index.md without the LostPaul Folder Notes community plugin. That violates Commitment #3 (mobile parity, no plugin dependencies).
1.3recipe + params regenerates load-bearing artifacts but 5+ adjacent artifacts are NOT regenerable. Non-regenerable: (a) user annotations, (b) audit snapshots (v0.1.8), (c) Bases hand-edits, (d) exports (v0.1.7), (e) materialized results (Phase 5). All need per-query sibling slots.
1.4Slug is display; query_id (existing q-YYYY-MM-DD-<hex8>) is durable identity. Rename-safe by keying scanner + writers on query_id, not folder path.
1.5Layout C (hybrid back-pointer) rejected. Two-source drift cost; backlinks pane already gives “where embedded?” UX.
1.6”No folder” steelman rejected. Loses Commitment #5 (runtime-agnostic recipe schema), Commitment #2’s single coupling point, and has no per-query home for Phase 5+ derivatives.
1.7Migration before Phase 5. Phase 4.5 state is fully regenerable from crosswalker_query: frontmatter; migration is mechanical + idempotent. New sub-phase: Phase 4.6.
1.8.base hand-edit preservation is intentionally NOT solved. Refresh query views overwrites view.base — by design. Frontmatter on index.md is canonical. Users who want overrides get a view.user.base slot in v0.2+, not v0.1.6.
1.9Backward compat for one minor version. Phase 4.5 host-note frontmatter is auto-migrated on next save; the picker checks new location first, falls back to old, migrates on touch.

2. Where deliverables diverged → user decision

Section titled “2. Where deliverables diverged → user decision”
AspectA (B′ same-name --<id8>)B (B+ index.md + explicit embed)Locked
Folder namecsf-coverage--a1b2c3d4/csf-coverage/B+ clean folder names
Canonical filecsf-coverage--a1b2c3d4.md (same-name)index.mdB+ index.md
Embed user types![[<slug>--<id8>]] (resolves natively)![[<slug>/view.base]] (explicit)B+ explicit embed
Collision policyAlways --<id8> suffix on folderRefuse-and-prompt (picker) + -<hex4> (programmatic)B+ mixed policy
Filename aestheticUgly — --<id8> always visibleCleanB+

Why B+ over B′: matches the user’s “embed is just ! + wikilink, it shouldn’t be checked” intuition more honestly. The explicit ![[<slug>/view.base]] is literal about what’s being embedded — there’s no resolver hidden trick. --<id8> in A is a constant cosmetic tax users in the common case never hit. B+‘s query_id lives in frontmatter (durable) without polluting filesystem names.

_crosswalker/queries/<slug>/                ← one folder per query
├── index.md                                  ← CANONICAL: crosswalker_query: frontmatter
│                                                 (query_id, recipe, shape, params, schema_version)
│                                                 + optional user annotations in body
├── view.base                                 ← generated; rewritten by Refresh
├── README.md                                 ← optional, USER-AUTHORED prose (plugin never writes/reads)
├── materialized/                             ← Phase 5: pivot result + staleness flag
│   ├── result.json
│   └── stale.flag                            ← presence = recompute needed
├── exports/                                  ← v0.1.7: per-format exporter outputs
│   ├── 2026-05-18.oscal.json
│   ├── 2026-05-18.sssom.tsv
│   └── 2026-05-18.strm.json
└── snapshots/                                ← v0.1.8: timestamped audit copies
    └── 2026-05-18T12-00-00Z/
        ├── index.md                          ← frozen frontmatter
        ├── view.base                         ← frozen view
        └── run.json                          ← closure-cache hash, trace_id, OTS receipt

Host note (user-authored, anywhere in vault):

# My coverage analysis

Some prose...

![[csf-coverage/view.base]]                  ← explicit Bases-native embed

More prose...

The host note has no crosswalker_query: frontmatter. The plugin doesn’t write to the host. Backlinks from view.base show every host that embeds it — Obsidian-native, no plugin code needed.

The user’s explicit ask: “We need to account for the weird issues and edge cases.” This table is the contract for Phase 4.6 implementation.

#Edge casePolicyWhy
Slug derivation
1Slug derivation from recipe nameKebab-case, ASCII only, lowercase, max 48 chars. From the recipe.name field, then strip non-[a-z0-9-], collapse runs of -, trim leading/trailing -.Filesystem-safe everywhere; mirrors existing query_id slug convention
2Slug derivation produces empty stringFallback to query-<id8>Avoid .md filenames that start with --
3Reserved Windows names (con, aux, nul, prn, com1-9, lpt1-9, ., ..)Append -q suffix on hitCross-platform safety
4Slug > 48 chars after derivationTruncate at last - ≤ 48 chars; if no -, hard truncate at 48Keep file explorer readable; preserve word boundaries
5Slug with leading digitsAllow — Obsidian + filesystems tolerate 1-coverage/No special handling
Collisions
6Interactive picker — user creates same slug twiceModal: 3 options: Open existing (default), Pick new name (text input), Force suffix (auto <slug>-2/-3/…). Default focus on Open existing.The common case (90%) is “user forgot they made it already” — Open existing covers it. No silent rename.
7Programmatic create (agent / future MCP / wizard-draft)Append -<hex4> random suffix. Never silently -2/-3.Avoids the brief’s flagged anti-pattern (“user can’t find auto-renamed query”). Suffix is detectable.
8User pre-creates the folder/file at canonical pathRead existing index.md. If valid crosswalker_query: → treat as the existing query (UPDATE flow). If not → refuse with Notice: “A folder exists at _crosswalker/queries/<slug>/ but is not a Crosswalker query. Rename it or pick a different slug.”Defensive; never overwrite user content
9Case-insensitivity (macOS HFS+ vs Linux ext4)Slug is always lowercased before write. Collision check is case-insensitive.Cross-platform identity-stability
Rename / move
10User renames folder in file explorerquery_id in frontmatter is unchanged; scanner picks up new path. Obsidian’s “Automatically update internal links” (on by default) rewrites ![[<old>/view.base]]![[<new>/view.base]] everywhere.Identity ≠ path; rename is free
11User moves folder OUT of _crosswalker/queries/regenerateAll() walks all markdown files for crosswalker_query: frontmatter (not path-restricted). The query continues to work wherever it lives. Picker’s “Browse” shows queries from default location PLUS any out-of-band locations.Don’t dictate vault organization beyond defaults
12Auto-update-links disabled in user settingsDocument in SKILL.md as a known limitation: renames break embeds; user must run Crosswalker: Refresh query views to rebind. Optionally surface a warning Notice when picker detects this setting is off.Honest about non-default Obsidian state
Embed behavior
13Host note moves to different folderEmbed is vault-relative (![[<slug>/view.base]]) — works wherever the host livesObsidian wikilink standard
14User adds aliases to index.md frontmatterPlugin respects aliases. ![[<alias>]] resolves to index.md → user can transclude ![[index.md/view.base]] if they like? Wait — embeds target .base files explicitly. Aliases on index.md don’t help; document in SKILL.md that the embed-syntax is always ![[<slug>/view.base]].Don’t fight Obsidian’s resolver
15User wants to embed [[<slug>]] (link-not-embed)Resolves to index.md. Shows the query’s prose body. Fine — that’s the canonical query page; users CAN link to it.Native Obsidian; no plugin handling needed
16User wants to embed the FOLDER itselfNot a vanilla Obsidian feature. Document as out-of-scope. Use ![[<slug>/view.base]] instead.No plugin dependency
Phase 4.5 → 4.6 migration
17Migration runs but a host note has DELETED its embedPlugin re-inserts at end of note (or skips with Notice “host note <path> lost its embed — re-run picker to re-embed”). User chooses. Don’t silently re-insert into edited content.Respect user edits
18Migration runs against vault with v0.1.6-RC-1 crosswalker_query: (schema_version 1)Auto-migrate to schema_version 2 (canonical location moved). Old frontmatter on host stays as crosswalker_query_legacy: for one minor version then removed on next migration run.Backward compat without indefinite cruft
19Migration partial failure (one query fails, others succeed)Per-query atomic — successes commit; failures logged with trace_id; user can re-run migration and only failed entries re-attempt. Per Phase 3.5c observability.Idempotent rerun
Misc
20User manually creates view.md inside <slug>/ folderPlugin leaves it alone. view.md and view.base are different files. User’s prose stays.Plugin owns only index.md + view.base + auto-managed subfolders
21User deletes view.base but keeps index.mdregenerateAll() recreates view.base from frontmatterIdempotent recovery
22User deletes index.md but keeps view.base + folderPlugin treats the folder as orphaned (no canonical source). Notice on next scan: “Folder <slug>/ is missing index.md — query is orphaned. Delete folder or re-run picker.”Don’t silently re-create state user explicitly removed
23Multiple shape renderings (pivot + list of same query)v0.1.6 scope: one shape per query. v0.2+ adds view-<shape>.base siblings (view-pivot.base, view-list.base). Reserve the naming pattern now; don’t implement.YAGNI for v0.1
24sync providers / OneDrive / iCloud / Obsidian Sync quirks with pathsSlugs are kebab-case ASCII only — survives every sync provider tested in Obsidian community use. Avoid -- in folder names (B+‘s clean filenames already do).Choice of B+ over B′ avoids the -- concern
CommitmentHow B+ respects it
#1 Schema-as-primitivecrosswalker_query: frontmatter is part of Tier 1 markdown. Anyone emitting valid Tier 1 + the frontmatter shape is a first-class producer. No change.
#2 Closed 5-mechanism recipe grammarrender(Recipe, ConceptIdentity) → Address unchanged. view.base is the addressable artifact; canonical query is index.md.
#3 TypeScript in-plugin engine, mobile parityExplicit ![[<slug>/view.base]] embed works in vanilla Obsidian Desktop + Mobile (Capacitor). Zero plugin dependencies. Bases on mobile confirmed working in 1.9.6+.
#4 Tier 2 sqlite-wasm sidecarTier 2 sidecar location is unchanged — out-of-band from per-query folders.
#5 Runtime-agnostic recipe schemarecipe + params in index.md frontmatter remains the portable contract. AJV-validated. Engine is swappable.
#6 Bases-not-DataviewEmbed is Bases-native ![[<slug>/view.base]]. No Dataview surfaces.

6. What this re-homes vs. what it preserves

Section titled “6. What this re-homes vs. what it preserves”

Re-homed (changes path/location only):

  • Canonical query state: host note frontmatter → _crosswalker/queries/<slug>/index.md frontmatter
  • Generated .base file: _crosswalker/views/q-<id>.base_crosswalker/queries/<slug>/view.base
  • Embed format: ![[_crosswalker/views/q-<id>.base]]![[<slug>/view.base]]

Preserved (no semantic change):

  • crosswalker_query: frontmatter shape (schema bumps 1 → 2 to record canonical-location change)
  • query_id format (q-<YYYY-MM-DD>-<hex8>)
  • AJV validation
  • Idempotent regeneration via Refresh query views
  • All Phase 3.5c observability events (category=view)
  • app.fileManager.processFrontMatter() as canonical safe-edit API
  • Plugin command surface (Insert query / Refresh query views) — same names, different write targets
  • Recipe schema + recipe loader + recipe-templates renderer

7. Phase 4.6 — scope + acceptance criteria

Section titled “7. Phase 4.6 — scope + acceptance criteria”

Goal: ship Layout B+ + a migration command before Phase 5 (Materialization + sparse-pivot HARD guard) starts. Single milestone, single commit.

Source files to change (5 src + 1 new):

ActionPathChange
EDITsrc/views/query-frontmatter-schema.tsschema_version: 1 → 2; rename view_file field semantically (same key, different default value); add slug field; add slug-derivation helper; update viewFileFor()queryFolderFor()
EDITsrc/views/apply-query-to-note.tsWrite to _crosswalker/queries/<slug>/{index.md,view.base}; refuse-and-prompt collision modal; insert ![[<slug>/view.base]] embed at cursor
EDITsrc/views/regenerate-query-views.tsWalk _crosswalker/queries/**/index.md; key on query_id not path; fall back to old host-note frontmatter for one minor version
EDITsrc/views/insert-base-block.tsbuildEmbed(viewPath) emits ![[<slug>/view.base]]
EDITsrc/views/reference-base-files.tsRewrite SKILL.md content for the new layout pattern
NEWsrc/views/migrate-query-layout.tsImplements Crosswalker: Migrate queries to folder layout command per migration plan

Tests (~+40 new, all 359 existing pass):

SurfaceTests
Slug derivationPure-function tests for cases 1-5 from §4 above (kebab-case, fallback, reserved names, truncation, leading digits)
Collision policyPicker modal tests for case 6 (3 options surface); programmatic tests for case 7 (-<hex4> suffix added)
Folder layout writerapply-query-to-note.test.ts extended for new write target
Folder layout readerregenerate-query-views.test.ts extended for _crosswalker/queries/**/index.md walk
Migration idempotencyNew migrate-query-layout.test.ts — mock vault with Phase 4.5 queries → migrate → assert new layout → migrate again → no-op
Backward-compat readReader test confirms picker still finds Phase 4.5 host-note frontmatter for one minor version
Defensive edge casesCases 8 (pre-existing folder), 17 (deleted embed), 22 (orphaned folder), 21 (deleted view.base)

Acceptance criteria:

  • All 359 existing tests pass + ~40 new tests pass
  • Crosswalker: Refresh query views is idempotent on second run (no writes when YAML body unchanged)
  • ![[<slug>/view.base]] renders on Obsidian Desktop + Mobile (Capacitor) with no Folder Notes plugin
  • Slug collision via picker surfaces the 3-option modal (Open / Pick new / Force suffix); default focus on Open existing
  • Programmatic create produces -<hex4> suffix when slug collides
  • Migration command moves test-vault’s 3 existing queries to new layout without breaking existing embeds
  • Phase 3.5c trace-IDs thread through migration command for diagnosis
  • bun run lint + bun run build clean
  • cd docs && bun run build clean (docs reflect new layout)

Estimated effort: ½–1 day, single commit. Reuses Phase 4.5’s idempotent-write infrastructure; the changes are mechanical path renames + one new command + one schema bump + slug derivation helpers.

8. Phase 5+ state-growth → mapped to Layout B+

Section titled “8. Phase 5+ state-growth → mapped to Layout B+”
PhaseDerivative artifactLocation in B+
5Materialized pivot result<slug>/materialized/result.json
5Sparse-pivot stale flag<slug>/materialized/stale.flag
v0.1.7OSCAL export<slug>/exports/<date>.oscal.json
v0.1.7SSSOM export<slug>/exports/<date>.sssom.tsv
v0.1.7STRM export<slug>/exports/<date>.strm.json
v0.1.8Audit snapshot (frozen index.md + view.base + run metadata + OTS receipt)<slug>/snapshots/<iso8601>/
v0.1.8Append-only per-query audit log<slug>/audit.ndjson
v0.2+Multi-shape view-<shape>.base siblings<slug>/view-pivot.base, <slug>/view-list.base, …
v0.2+User overrides for hand-edited views<slug>/view.user.base (never overwritten)

Every future per-query state has a stable home before it ships. That’s the whole point of the layout change.

ItemStatus beforeStatus after
Query state canonical location🚧 Phase 4.5 = frontmatter on host note; flagged for Ch 38 stress-testLocked: Layout B+ _crosswalker/queries/<slug>/index.md
Per-query derivative home (Phase 5/v0.1.7/v0.1.8)❓ undefinedLocked: sibling subfolders under <slug>/
Slug-collision policy❓ openLocked: refuse-and-prompt (picker) + -<hex4> (programmatic)
Embed format⚠️ Phase 4.5 uses opaque q-<id>.base pathsLocked: ![[<slug>/view.base]] explicit
Phase 5 gating❓ ready to start🚧 Blocked by Phase 4.6 migration
Phase 4.5 status✅ Done 2026-05-15⚠️ Re-homed by Phase 4.6 (does not revert; canonical state moves)

Out of scope for this synthesis; tracked for later:

  • .base hand-edit preservation — Phase 4.5’s overwrite-on-refresh is preserved. The view.user.base override slot is reserved naming but not implemented in v0.1.6.
  • Per-query Bases view-time state (column widths, sort) — Bases stores these in .obsidian/workspace.json, not in our files. Out of plugin scope.
  • Multi-shape rendering of same queryview-<shape>.base naming reserved; implementation deferred to v0.2+.
  • Cross-vault federation / marketplace — Layout B+ is friendly to this (one folder = one shareable query), but actual cross-vault import/export is v0.3+.
  • The deeper “should plugins emit files at all?” critique from Ch 32a — Crosswalker has committed to file-emission via Phase 4.5 + v0.1.8 audit trail; meta-question revisited at v0.3+ if relevant.

Test-vault has 3 queries (per file inspection 2026-05-18): q-2026-05-16-18d01bf9.base, q-2026-05-18-0baa0140.base, q-2026-05-18-9cc7e2d1.base (the last regenerated multiple times during rename testing today). All have corresponding host notes with crosswalker_query: frontmatter. Migration is one command, ~1 second, idempotent. No user-facing scripts.

User vaults in the wild: zero (v0.1.6 unreleased). No public migration concerns.