Skip to content

Establish managed Git vault lifecycle foundation - #106

Draft
alemhnan wants to merge 60 commits into
developmentfrom
docs/managed-git-vault-foundation
Draft

Establish managed Git vault lifecycle foundation#106
alemhnan wants to merge 60 commits into
developmentfrom
docs/managed-git-vault-foundation

Conversation

@alemhnan

@alemhnan alemhnan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator

#94 — Safe Local-history Git behavior

Implements #94 only, from eb9e819 feat: preserve search semantics across Vault scope (#93) through 3bed450 feat: scope local Git history to Vault subtree (#94).

Scope

  • Local history discovers an existing enclosing checkout, while Remote validation remains rooted at the configured Vault.
  • It commits only detected, non-ignored drift below the canonical configured Vault path; Git pathspecs are not used for Vault selection.
  • The commit is built from a HEAD-seeded index. Existing staged work outside the Vault is retained, and a pre-existing staged Vault version is retained rather than overwritten.
  • When there was no Vault staging, only the Vault index entries are refreshed after the commit so they stay clean against the new HEAD.
  • Local mode skips destructive interrupted-merge recovery and returns before all remote phases.

Intentional exclusions

No Git remains an absent task/configuration and is unchanged. No HTTP, MCP, frontend, managed checkout acquisition, pull-only/two-way lifecycle, fetch, push, merge, or per-Vault remote Git work is included. Markdown remains authoritative; SQLite/cache state remains disposable.

Validation

Before every Cargo invocation, exported and verified the justfile-resolved paths:

CARGO_TARGET_DIR=/home/alemhnan/Documents/Work/Hatchdoor/target
TMPDIR=/home/alemhnan/Documents/Work/Hatchdoor/target/tmp
CARGO_BUILD_JOBS=1

All Cargo commands used the direct Rust 1.97.1 binary.

  • focused regressions: contained subtree / literal * Vault path; preserved staged Vault blob
  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --all — 560 library, 7 eval, and 3 CLI tests passed
  • module-map check — 173 production source files assigned exactly once
  • git diff --check

Review

Parallel standards and specification reviews found and drove fixes for: enclosing-repository containment, destructive Local recovery, outside staging, literal Vault path selection, index/HEAD consistency, and pre-existing staged Vault content. Both final re-reviews reported no actionable findings.

#98 — Expose Vault discovery, management, status, and events under API v1

Implements #98 only, from 6d6d8fd docs: explain why the registry never gets a test-only URL escape through efb96c4 feat: expose Vault discovery, management, status, and events under API v1 (#98).

Scope

New file src/handlers/vaults.rs is the entire surface, authenticated and mounted independently of the legacy single-configured-Vault require_vault_ready gate:

  • GET /api/v1/vaults — discovery: redacted definitions, per-Vault runtime status, derived capabilities, registry_revision, collection_revision. Reachable at zero enabled Vaults; reports a persisted-registry recovery state (corrupt/future/unsupported schema) as a normal 200 with an explicit recovery object rather than erroring.
  • POST /api/v1/vaults, PATCH /api/v1/vaults/{vault_id}, POST .../enable, POST .../disable, DELETE /api/v1/vaults/{vault_id} — collection management with optimistic concurrency (expected_registry_revision) and one stable VaultApiError{code, message, vault_id?, retryable} shape on every error path.
  • POST /api/v1/vaults/{vault_id}/sync, POST .../retry — manual managed-Git turns via ManagedGitScheduler, gated on the Vault being enabled and managed-Git.
  • GET /api/v1/vaults/events (SSE) — one collection-wide invalidation stream carrying collection_revision, the affected Vault IDs, and a broad definition/status change category.

src/vault_runtime.rs gained VaultChangeCategory/VaultCollectionRevisionEvent so the collection-revision watch channel carries that category and affected-ID information instead of a bare u64. AppState gained vault_work/managed_git so this adapter can reconcile a registry mutation into live runtime effects (reconcile_and_reconstruct) and request an immediate Git turn without a second execution lane; reconciliation is spawned and awaited with a bounded 10s timeout rather than blocking the HTTP response indefinitely on a retiring Vault's in-flight Git/Index turn.

Intentional exclusions

MCP discovery (#103) and Vault-scoped content reads/mutations and one-or-all collection reads (#99#101) are separately owned later packets. No frontend integration (#67, after /api/v1 freezes at #101). Credentials are never returned, only credential_configured.

Validation

CARGO_TARGET_DIR=/home/alemhnan/Documents/Work/Hatchdoor/target
TMPDIR=/home/alemhnan/Documents/Work/Hatchdoor/target/tmp
CARGO_BUILD_JOBS=1
  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --all — 618 library, 7 eval, and 3 CLI tests passed. router_accepts_attachment_between_2mb_and_configured_max reproduces identically on the unmodified base commit (this sandbox's /tmp tmpfs is 100% full) — pre-existing and unrelated, confirmed via git stash.
  • node scripts/check-module-map.mjs — 177 production source files assigned exactly once
  • git diff --check

Review

Required parallel Standards and Spec reviews: Spec review found and drove fixes for two gaps — enable/disable/disconnect bypassing the structured error shape on a missing/malformed revision query string, and duplicate-name/path-overlap errors using 400 where issue #62's HTTP-meaning buckets place current-state conflicts at 409. Standards review found and drove a fix for one reliability issue — an unbounded HTTP block on a retiring Vault's in-flight background turn, now a spawned bounded wait. A self-review pass additionally found and fixed a double collection-snapshot read in the mutation response path. Remaining Standards notes (auth-gating boilerplate already duplicated by three pre-existing router sections, an SSE-event-encoding helper duplicated from handlers/api.rs, and add()'s new-Vault-ID recovery via before/after ID diffing) were judged out of this packet's declared scope or accepted style tradeoffs, documented rather than applied.

#99 — Expose Vault-qualified exact reads and contained resources under API v1

Implements #99 only, from efb96c4 feat: expose Vault discovery, management, status, and events under API v1 (#98) through b525706 feat: expose Vault-qualified exact reads and contained resources under API v1 (#99).

Scope

New file src/handlers/vault_content.rs is the whole HTTP surface, mounted in the same /api/v1/vaults router group as vaults.rs (#98) and sharing its demo-mode/auth posture, VaultApiError, and rejection-mapping helpers:

  • GET /api/v1/vaults/{vault_id}/notes/{slug}, GET .../notes/{slug}/links, GET .../notes/{slug}/download — exact note, link, and download reads.
  • GET /api/v1/vaults/{vault_id}/resolve, POST .../resolve-batch — wikilink resolution.
  • GET /api/v1/vaults/{vault_id}/assets/{*path} — contained asset/attachment serving, with the same containment (extension allowlist, traversal rejection, canonicalize + starts_with) the legacy unscoped route applies.
  • /v/{vault_id}/n/{slug} — the canonical browser Note URL now dispatches to the SPA shell, mirroring the existing /n/{slug} registration.

Every response is Vault-qualified and uses the shared VaultApiError{code, message, vault_id?, retryable} shape. Exact reads always inspect the requested Vault's authoritative Markdown directory via VaultReadCore, never the disposable cache, and are gated per-request by that Vault's own vault_not_found/vault_disabled/vault_unavailable status rather than the legacy require_vault_ready gate.

src/vault_read.rs gained three additive VaultReadCore methods: resolve_wikilinks (resolves a whole batch against one authoritative-index build instead of one build per target — the standards review's first-round finding), vault_directory (the Vault's directory under the same gating as exact reads, including a directory-existence check so a managed-Git Vault that hasn't materialized yet reports the same retryable vault_read_unavailable code an exact-note read would, rather than a raw filesystem error surfacing as a non-retryable 500 — the third-round finding), and exact_note_for_download (a Note and its directory from one Vault control-block fetch, since two independent lookups could otherwise observe different Vault generations across a concurrent edit — the second-round finding). src/vault_runtime.rs's VaultControlBlock::ensure_accepting_operations is widened to pub(crate) for reuse.

src/handlers/assets.rs, downloads.rs, vaults.rs, and api.rs's existing path containment, export, response-building, and error-mapping helpers are widened to pub(crate) and, where duplicated across rounds, factored into shared helpers (asset_response, download_response, asset_error_parts) reused by both the legacy unscoped routes and this ticket's Vault-scoped routes — unchanged behavior for the legacy routes, confirmed by their existing tests.

Intentional exclusions

No one-or-all collection reads (#100), no Vault-control mutations beyond #98, no removal of old unscoped routes (#101), no MCP (#103), no frontend integration (#67).

Validation

CARGO_TARGET_DIR=/home/alemhnan/Documents/Work/Hatchdoor/target
TMPDIR=/home/alemhnan/Documents/Work/Hatchdoor/target/tmp
CARGO_BUILD_JOBS=1
  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --all — 632 library, 7 eval, and 3 CLI tests passed. One git::task test failed once under full-suite parallel load, passed on isolated and repeated reruns and on a second full-suite run — a pre-existing timing flake unrelated to this change (untouched src/git/task.rs), not a regression.
  • node scripts/check-module-map.mjs — 178 production source files assigned exactly once
  • git diff --check

Review

Required parallel Standards and Spec reviews: Spec review reported no conformance gaps against the wire-contract spec, issue #62's Implementation Decisions, and the ADRs. Standards review ran four rounds: round 1 found and drove fixes for a resolve-batch performance bug (full Vault re-walk per target) and an unwrapped-blocking-I/O gap in the new asset handler, plus a duplicated response-header block; round 2 found and drove a fix for a TOCTOU race (note content and its export directory could observe different Vault generations across a concurrent edit) plus further helper-sharing cleanup; round 3 found and drove a fix for a genuine correctness bug (the asset route reporting a non-retryable 500 instead of the retryable 503 an exact-note read reports for an identical not-yet-materialized-Vault condition) plus a control_block/authoritative_index de-duplication, and judged two remaining notes (per-handler parse_vault_id/clone prologue duplication, an unused relative_path clone in a discarded batch-resolve field) as accepted style tradeoffs mirroring the codebase's existing per-handler boilerplate convention, documented rather than applied; round 4 could not complete (background-agent session limit) after the round-3 fixes were already validated clean by the full test suite, clippy, and module-map check.

#100 — Expose one-or-all collection reads and search under API v1

Implements #100 only, from b525706 feat: expose Vault-qualified exact reads and contained resources under API v1 (#99) through 65684e0 feat: expose Vault-qualified one-or-all collection reads and search under API v1 (#100).

Scope

New file src/handlers/vault_collection_reads.rs is the entire surface, mounted in the same /api/v1/vaults router group as vaults.rs (#98) and vault_content.rs (#99) and sharing their demo-mode/auth posture, VaultApiError, and rejection-mapping helpers:

  • GET /api/v1/vaults/{scope}/tree, .../stats, .../graph — grouped per Vault.
  • GET /api/v1/vaults/{scope}/recent, .../search — flattened across Vaults; search returns one global ranking.

{scope} is one immutable Vault ID or the literal all, parsed by a new parse_vault_scope helper (invalid_scope/400 otherwise). Every route reuses the path parameter name {vault_id} rather than {scope} for router-tree consistency — axum/matchit requires one consistent parameter name per position across every route sharing it.

This is a thin HTTP adapter with no collection-read domain logic of its own: it calls straight into already-implemented, already-unit-tested shared-core methods — VaultReadCore::{trees, statistics, graphs, recently_modified} (src/vault_read.rs) and VaultSearchCore::search (src/search/vault_scoped.rs) — which already implement every one-or-all/partial/zero-Vault/grouped-vs-flattened/participant behavior issue #62 and the wire-contract spec require. search's layers query parameter (a comma-separated token list) is parsed by this file's own parse_layer_selection and applied identically to every participant without consulting any one Vault's known-layer catalog, matching issue #62's decision that a name valid in one Vault and absent from another is not an error — only a name absent from every usable participant is invalid_layer_selection.

vault_read_error_response (vault_content.rs, #99) is widened to pub(crate) and extended with invalid_search_query/invalid_layer_selection (400) and search_unavailable (503) arms, reused rather than duplicated here.

Fixed a real wire-contract gap surfaced by wiring the first HTTP consumer of VaultReadProjection: VaultScope (src/vault_read.rs) previously derived serde's externally-tagged representation ({"one": "<uuid>"} for the data-carrying variant), which does not match docs/migrations/vault-scoped-clients.md's flat-scalar scope field. It now has a hand-written Serialize impl producing the Vault ID's canonical text or the literal "all", mirroring exactly what a caller passes as the scope path segment.

Intentional exclusions

No MCP (#103), no Vault-control mutations beyond #98, no removal of old unscoped routes (#101), no frontend integration (#67).

Validation

CARGO_TARGET_DIR=/home/alemhnan/Documents/Work/Hatchdoor/target
TMPDIR=/home/alemhnan/Documents/Work/Hatchdoor/target/tmp
CARGO_BUILD_JOBS=1
  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --all — 641 library, 7 eval, and 3 CLI tests passed. router_accepts_attachment_between_2mb_and_configured_max failed once under this sandbox's /tmp tmpfs at 100% full; reproduced identically against the unmodified base commit via git stash — pre-existing and unrelated.
  • node scripts/check-module-map.mjs — 179 production source files assigned exactly once
  • git diff --check

Review

Required parallel Standards and Spec reviews both reported no conformance gaps or defects against the wire-contract spec, issue #62's Implementation Decisions, the ADRs, and this codebase's established handler conventions. Standards review noted one accepted style tradeoff — this file's own layer-token parser duplicates some of LayerSelection::parse's token-classification logic, deliberately, since the single-Vault-degrade-with-warning and cross-Vault-deferred-validation semantics genuinely differ — documented rather than applied.

#101 — Expose scoped mutations, retire the unscoped API, and freeze API v1

Implements #101 only, from 65684e0 feat: expose Vault-qualified one-or-all collection reads and search under API v1 (#100) through 9e1b40e feat: expose Vault-scoped mutations and retire the unscoped API (#101).

Scope

New file src/handlers/vault_write.rs is the entire Markdown-mutation surface, mounted in the same /api/v1/vaults router group as vaults.rs/vault_content.rs/vault_collection_reads.rs and sharing their demo-mode/auth posture, VaultApiError shape, and parse_vault_id/rejection helpers:

  • POST .../notes, PUT .../notes/{slug}, PATCH .../notes/{slug}/rename|move|move-rename|archive, DELETE .../notes/{slug}, POST .../attachments, GET .../write-capabilities — each targets exactly one Vault ID (never all), routes through the unchanged vault/write/** layer (ADR-03) calling the same functions the legacy write API used, and preserves the legacy verb per route exactly (issue Implement multi-vault workspace and managed Git #62: "preserve current write verbs").

Every mutation gates on the requested Vault's own VaultControlBlock: acquire_mutation() is a genuine per-Vault lock (concurrent mutations to different Vaults no longer serialize against each other), and capabilities.mutate is checked explicitly — a Pull-only (or otherwise non-mutable) Vault now returns a structured capability_unavailable/409, a case the legacy single-Vault write API never had to handle. Every index build (initial lookup and the post-write refresh) runs off the async runtime via spawn_blocking, matching the legacy current_index's behavior. Noise-exclusion is checked against that Vault's own exclude_patterns, not the legacy instance-wide HATCHDOOR_EXCLUDE setting. VaultReadCore::control_block and the free function vault_read::runtime_error are widened to pub(crate) so this file reuses the exact same not-found/disabled/no-runtime gate exact reads already use, instead of duplicating it.

The entire legacy unscoped application API is retired in the same change (issue #62: "do not run a legacy duplicate API"): src/handlers/write_api.rs is deleted; src/server.rs's protected/attachment router groups and the require_vault_ready/reject_demo_layer_query middleware are removed; src/handlers/api.rs/assets.rs/downloads.rs/diagnostics.rs are pruned to their still-used pub(crate) helpers (asset/download containment logic reused by vault_content.rs, and diagnostics.rs's build_layer_diagnostics still called by the MCP diagnostics tool); dead wire types are pruned from api_types.rs; the now-unreachable refresh_coalescing is removed from app_state.rs.

Minor wire-shape changes versus the legacy responses: every mutation response now includes vault_id; git_sync_warning is dropped (the managed-Git scheduler has no debounced-on-write hook to report from, unlike the legacy single git_sync task); write-capabilities' settings_enabled field is dropped (it was only ever false in the demo-mode branch, which the route is now structurally unreachable from in every reachable case).

Two scope decisions confirmed with the user before implementation, both recorded as deliberate, documented gaps rather than silent omissions or opportunistic fixes:

  • refresh and diagnostics are retired with no Vault-scoped replacement. A working per-Vault refresh needs VaultWorkKind::Index dispatch, which is a stub (vault_work_kind_not_yet_implemented); a Vault-scoped diagnostics route needs new per-Vault cache-query domain methods that don't exist. Both are out of scope for an adapter-only packet. docs/migrations/vault-scoped-clients.md is updated to state this explicitly rather than continuing to promise both the same Vault prefix.
  • Demo mode (HATCHDOOR_DEMO_MODE=true) is left with zero working content-serving routes: vaults_v1 (including these new mutation routes) is unconditionally disabled under demo mode, exactly as Expose Vault discovery, management, status, and events under API v1 #98Expose one-or-all collection reads and search under API v1 #100 already left it, and the legacy protected group it depended on for actual content-serving is now gone. Fixing this needs new design (how a demo Vault gets registered/exposed over /api/v1) that issue Implement multi-vault workspace and managed Git #62 never decided; not attempted here.

Known limitation surfaced during review, also not fixed here (see docs/architecture/module-map.md's "Vault mutation" boundary): the new per-Vault VaultControlBlock::acquire_mutation lock and the legacy single instance-wide AppState::vault_write_lock (still used by MCP write tools and the legacy Git-sync task) don't exclude each other. For a Vault that is both the legacy single-configured Vault and reachable through the registry, an HTTP mutation and a concurrent MCP write/Git-sync commit aren't mutually exclusive today. Unifying them needs MCP's own migration onto the Vault collection runtime (#103); src/mcp/** is untouched by this ticket.

Intentional exclusions

No MCP (#103) — verified cargo test mcp still passes unchanged; MCP still calls the old unscoped shared-core functions directly, never the removed HTTP handlers. No frontend integration (#67/#102) — a known, already-accepted broken consumer per #62's sequencing.

Validation

CARGO_TARGET_DIR=/home/alemhnan/Documents/Work/Hatchdoor/target
TMPDIR=/home/alemhnan/Documents/Work/Hatchdoor/target/tmp
CARGO_BUILD_JOBS=1
  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --all — 632 library, 7 eval, and 3 CLI tests passed. git::task::tests::spawning_flushes_accumulated_drift_in_local_mode failed once under full-suite parallel load, then passed on immediate re-run — the pre-existing, documented timing-sensitive flake, unrelated to this change.
  • node scripts/check-module-map.mjs — 179 production source files assigned exactly once
  • git diff --check

Review

Required parallel Standards and Spec reviews both ran against the actual diff. Standards review found one must-fix (index builds inside two handlers were running synchronously on the async runtime instead of via spawn_blocking, unlike the legacy current_index — fixed) and flagged the MCP/Git-sync lock-coexistence gap above as moderate (documented, not fixed — MCP migration is #103) plus two minor nits (a 2-line duplicated glue pattern, accepted as a documented style tradeoff since it isn't cleanly reusable as one of the existing pub(crate) seams; an undocumented settings_enabled field drop, now documented above). Spec review found the update-note route had silently changed verb from the legacy PUT to PATCH with no record anywhere (fixed — reverted to PUT, matching issue #62's "preserve current write verbs") and that the route-absence test's own header comment overclaimed coverage it didn't have for the four legacy PATCH-only action routes (fixed — added). Re-ran the full validation suite after every fix; all green.

#130 — Give each Vault its own archive folder and commit identity, and stop requiring a Git username

Implements #130 only, from b5a0290 fix(index): serialize vault snapshot publication through bf2e2c9 feat: give each Vault its own archive folder and commit identity (#130).

Scope

  • VaultRecord/VaultDefinition gain an optional per-Vault archive_folder (normalized to a single-trailing-slash form) and an optional per-Vault commit_identity ({ name, email }, name and email together only). Both default to absent via #[serde(default)], mirroring the poll_interval_secs precedent, so an on-disk registry written before these fields existed keeps loading under the unchanged REGISTRY_SCHEMA_VERSION.
  • AppState::vault_archive_prefix resolves a Vault's own folder first, else runtime_archive_prefix; wired into all three archive-prefix call sites (handlers/vault_content.rs, handlers/vault_write.rs, mcp/tools/write.rs).
  • git::config::resolve_commit_identity resolves a Vault's own identity first, else the instance-wide HATCHDOOR_GIT_AUTHOR_NAME/HATCHDOOR_GIT_AUTHOR_EMAIL defaults; called once in vault_runtime.rs's dispatch_managed_git_turn_with so every Git turn for a Vault (managed-Git, existing-Git remote-sync, and existing-Git Local-history) honors it.
  • HTTPS credentials accept a token alone: normalize_credentials now rejects only an empty token, substituting the documented HTTPS_CREDENTIALS_USERNAME_PLACEHOLDER when no username is supplied. Credentials remain write-only, read back only as credential_configured.
  • /api/v1 Vault create/edit/discovery payloads and MCP create_vault/edit_vault/list_vaults carry the two new optional fields with identical names and semantics; MCP's create_vault tool already shared the HTTP CreateVaultRequest type directly, so it inherited the fields for free.

Intentional exclusions

No SSH transport. No mandatory per-Vault values and no migration of existing server-wide values into every Vault record. mcp/config.rs's SERVER_INSTRUCTIONS is untouched — the new fields are additive and don't change what an agent must know before calling a tool.

Validation

CARGO_TARGET_DIR=/scratch/cargo-target/hatchdoor-vault-identity-130
  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test --all — 697 library, 7 eval, and 3 CLI tests passed. 22 library tests in git::managed_sync/git::managed_task/vault_runtime failed; confirmed pre-existing and unrelated via git stash against the unmodified base commit — this sandbox's global init.defaultBranch=main conflicts with these fixtures' hardcoded "master" branch pushes.
  • node scripts/check-module-map.mjs — 179 production source files assigned exactly once

Review

Parallel Standards and Spec reviews found no hard violations and no scope creep beyond the required mechanical updates to every existing NewVaultDefinition/VaultDefinitionEdit/CreateVaultRequest/EditVaultRequest construction site (exhaustive struct literals, mostly test fixtures). Spec review found two real test-coverage gaps — git::config::resolve_commit_identity and AppState::vault_archive_prefix had no direct tests — both fixed with focused unit tests. Spec review also confirmed the issue's premise that src/mcp/tools/read.rs has a separate CreateVaultArgs struct was inaccurate: create_vault_tool parses directly into the shared vaults::CreateVaultRequest, so the HTTP struct's new fields already cover MCP create_vault for free.

@alemhnan

alemhnan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

#86 follow-up review hardening

Tested commit: a793b39bc0a70cea52ab641d3c50e4399604df39.

The required two-axis review of 162f83c...HEAD found five #86 contract gaps in the previously published implementation. This follow-up keeps the same packet and changes only src/vault_registry.rs:

  • redact repository URLs from VaultSource debug output, including rejected credential-bearing inputs;
  • permit non-identity definition edits while an unchanged local source is temporarily unavailable;
  • reject case-equivalent names whose Unicode case mapping expands, such as Straße and STRASSE;
  • resolve the nearest existing canonical ancestor so an absent managed checkout cannot bypass overlap checks through a symlinked state parent;
  • retain a credential-free repository URL in existing-Git Local history mode so Git behavior can change normally without changing checkout identity.

Validation from the repository root:

  • cargo test vault_registry — passed (37)
  • cargo check --all-targets — passed
  • cargo fmt --all -- --check — passed
  • cargo clippy --all-targets -- -D warnings — passed
  • cargo test --all — passed (520 total: 510 library, 7 evaluation, 3 CLI; 0 doc tests)
  • node scripts/check-module-map.mjs — passed (168 production files assigned exactly once)
  • git diff --check — passed
  • final independent Standards review — clean; one pre-existing non-blocking repeated-enum-dispatch judgment call retained to avoid an ADR-13 refactor outside Manage validated Vault definitions without exposing credentials #86
  • final independent Spec review — clean

The first focused test attempt rebuilt without the established vendored-OpenSSL environment and failed in openssl-sys before compiling Hatchdoor. Validation was rerun with the branch's documented Rust 1.97.1 and existing vendored OpenSSL artifacts; all commands above passed.

No later ticket was begun. CONTEXT.md and docs/migrations/ were moved from the main worktree into this branch worktree at the user's request but remain untracked and are not part of this commit. #67, #102, and #103 were not modified.

@alemhnan

alemhnan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

#87 validation evidence — 369c1b1cc6420afb95323638e804a436710fd1ea

The legacy-import packet is implemented and pushed.

  • Focused: vault_migration 13, runtime_config 10, vault_registry 38, config 32, cache 95, and git::config 4 — all passed.
  • Full Rust gates: format, all-target check, strict Clippy, and cargo test --all — passed (525 library + 7 evaluation + 3 CLI; 0 doc-test failures).
  • Structural/deployment gates: module map passed with 169 production files assigned exactly once; Compose resolved the new /data/state bind; git diff --check passed.
  • Independent Standards and Spec re-reviews are clean after all findings were fixed.
  • The Git-preservation regression covers a genuine merge conflict, dirty and untracked files, another branch, and a local commit ahead of its remote-tracking reference, then proves the complete Vault tree (including .git) is unchanged.
  • One initial full-suite run was contaminated by 22 stale PID-named /tmp/hatchdoor-settings-test-*.json fixtures and reported 3 server-setting failures. Only those generated fixtures were removed; the clean full rerun passed.
  • No Activate isolated per-Vault runtimes #88 startup integration was added. Import legacy single-Vault deployments once #87 exposes the migration boundary; Activate isolated per-Vault runtimes #88 remains its first production caller.

@alemhnan

alemhnan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Pre-#88 development-tooling housekeeping — aed8ee2

This user-requested maintenance commit changes no product, migration, runtime,
HTTP, MCP, frontend, or deployment behavior.

  • Replaces the unavailable hardcoded /scratch Cargo paths with portable
    defaults derived from Git's shared common directory, so linked worktrees use
    the primary checkout's existing target.
  • Preserves explicit CARGO_TARGET_DIR and CARGO_HOME overrides.
  • Keeps recipe temporary files off small system tmpfs mounts through an
    overridable HATCHDOOR_TMPDIR.
  • Ensures Cargo directories exist before recipes run and updates the matching
    developer documentation.

Validation:

  • linked-worktree and primary-checkout path assertions — passed
  • all three explicit path overrides — passed
  • just _prepare-cargo — passed; reused the existing shared target
  • just --dry-run dev-start — passed
  • git diff --check — passed
  • normal authenticated git push — passed through the repaired stable
    /usr/bin/gh auth git-credential helper

The separate stale-settings-fixture defect observed during #87 is recorded in
#107. It is not implemented here, and #88 has not begun.

@alemhnan

alemhnan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

Issue #107 validation at commit 7f685a9: TDD red/green complete; runtime_config 11 passed; settings 10 passed twice; format and strict
Clippy passed; full suite passed with 526 library, 7 evaluation, and 3 CLI tests; Standards and Spec reviews clean. No #88 work included.

@alemhnan

alemhnan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

#89 validation evidence — 957c63b44fcbed83cdf391134642c33b05d348ea

Issue #89 is implemented and pushed through one additive background-work
coordination boundary.

  • One cloneable request side feeds one non-cloneable worker through a single
    in-memory FIFO.
  • A Vault occupies at most one queue position and executes one operation per
    turn; remaining Git, index/embedding, or repair work returns to the tail.
  • Queued duplicates coalesce, and requests repeated during active work retain
    exactly one required rerun.
  • Returned failures complete the turn and remain attributable through a
    Vault-qualified outcome, so the next Vault proceeds.

Interface change checklist

Interface change: instance-wide Vault background-work coordinator
Producer boundary: Background work coordination (src/vault_work.rs)
Kind: additive internal contract
Old contract: no shared admission boundary for per-Vault Git, indexing/embedding, and repair work
New contract: VaultWorkCoordinator and the unique VaultWorkWorker expose deterministic one-position FIFO admission, coalescing, one-operation turns, tail requeueing, and Vault-qualified outcomes
Consumer boundaries: none in #89; repository references and the complete branch diff were searched. #90 owns the first lifecycle/composition consumer, followed by the routed cache and Git packets
Compatibility/migration: no existing runtime, HTTP, MCP, frontend, cache, Git-task, watcher, or persisted contract changes; the new contract is additive
Rollout/rollback: remains on the draft integration branch; revert 957c63b before #90 to remove the unused boundary
Evidence: focused scheduler tests, full backend gates, module-map validation, and independent Standards and Spec reviews

Architectural invariants checked: the coordinator adds no service, trait,
framework, package, state library, persistent queue, priority, throttle, timeout,
forced cancellation, or second execution lane. Markdown remains authoritative,
SQLite disposable, search unchanged, and HTTP/MCP mutations remain routed through
src/vault/write/.

Validation

Every Cargo invocation exported and verified
CARGO_TARGET_DIR=/home/alemhnan/Documents/Work/Hatchdoor/target and
TMPDIR=/home/alemhnan/Documents/Work/Hatchdoor/target/tmp exactly as resolved
by justfile, with CARGO_BUILD_JOBS=1.

  • TDD red: focused tests failed on the absent coordinator contract
  • cargo test vault_work — passed (3)
  • cargo check --all-targets — passed
  • cargo fmt --all -- --check — passed
  • cargo clippy --all-targets -- -D warnings — passed
  • cargo test --all — passed (538 library, 7 evaluation, 3 CLI; 0 doc-test failures)
  • node scripts/check-module-map.mjs — passed (170 production files assigned exactly once)
  • git diff --check 9a7c2cc...HEAD — passed
  • Standards review — clean
  • Spec review — clean

The VM initially lacked system OpenSSL development metadata, reproducing the
previously recorded validation setup issue. libssl-dev and pkg-config are
now available; pkg-config --modversion openssl reports 3.5.6. No project
dependency or configuration changed.

#90 was not started. Disable/disconnect draining, restart reconstruction, and
graceful shutdown remain deferred exactly as routed.

@alemhnan

alemhnan commented Aug 6, 2026

Copy link
Copy Markdown
Collaborator Author

#91 validation evidence — 7c37ca6

Implements #91 only: a shared, Vault-ID-qualified SQLite snapshot seam with candidate build then atomic per-Vault publication. Markdown remains authoritative; cache rows and in-process attempt ordering are disposable. There is no default-Vault shim, MCP/frontend work, or per-Vault Git work.

TDD coverage:

  • Equal note identities and links remain isolated by Vault ID.
  • Readers observe old-or-new snapshots during publication; disconnect removes only its Vault rows.
  • Failed reindex retains the previous snapshot as stale; disabled Vaults require a successful rebuild to rejoin.
  • An older failed attempt cannot stale a newer published attempt.
  • Derived FTS/vector rows carry Vault identity.

Validated using direct Rust 1.97.1 Cargo with CARGO_BUILD_JOBS=1, CARGO_TARGET_DIR=/home/alemhnan/Documents/Work/Hatchdoor/target, and TMPDIR=/home/alemhnan/Documents/Work/Hatchdoor/target/tmp (each resolved/verified from justfile):

  • cargo fmt --all -- --check
  • cargo check --all-targets
  • cargo clippy --all-targets -- -D warnings
  • cargo test vault_ — 107 passed, 0 failed
  • cargo test --all — 550 library + 7 eval + 3 CLI tests passed, 0 failed
  • node scripts/check-module-map.mjs — 171 production source files assigned exactly once
  • git diff --check

Required spec and standards re-reviews of 9be44f1..7c37ca6: clean.

BattermanZ and others added 4 commits August 13, 2026 12:05
## #130 — Give each Vault its own archive folder and commit identity, and stop requiring a Git username

Implements #130 on top of the managed-Git vault lifecycle foundation, from
`b5a0290 fix(index): serialize vault snapshot publication`.

### Scope

- `VaultRecord`/`VaultDefinition` gain an optional per-Vault `archive_folder`
  (normalized to a single-trailing-slash form) and an optional per-Vault
  `commit_identity` (`{ name, email }`, name and email together only). Both
  default to absent via `#[serde(default)]`, mirroring the `poll_interval_secs`
  precedent, so an on-disk registry written before these fields existed keeps
  loading under the unchanged `REGISTRY_SCHEMA_VERSION`.
- `AppState::vault_archive_prefix` resolves a Vault's own folder first, else
  `runtime_archive_prefix`; wired into all three archive-prefix call sites
  (`handlers/vault_content.rs`, `handlers/vault_write.rs`, `mcp/tools/write.rs`).
- `git::config::resolve_commit_identity` resolves a Vault's own identity first,
  else the instance-wide `HATCHDOOR_GIT_AUTHOR_NAME`/`HATCHDOOR_GIT_AUTHOR_EMAIL`
  defaults; called once in `vault_runtime.rs`'s `dispatch_managed_git_turn_with`
  so every Git turn for a Vault (managed-Git, existing-Git remote-sync, and
  existing-Git Local-history) honors it.
- HTTPS credentials accept a token alone: `normalize_credentials` now rejects
  only an empty token, substituting the documented
  `HTTPS_CREDENTIALS_USERNAME_PLACEHOLDER` when no username is supplied.
  Credentials remain write-only, read back only as `credential_configured`.
- `/api/v1` Vault create/edit/discovery payloads and MCP `create_vault`/
  `edit_vault`/`list_vaults` carry the two new optional fields with identical
  names and semantics; MCP's `create_vault` tool already shared the HTTP
  `CreateVaultRequest` type directly, so it inherited the fields for free.

### Intentional exclusions

No SSH transport. No mandatory per-Vault values and no migration of existing
server-wide values into every Vault record. `mcp/config.rs`'s
`SERVER_INSTRUCTIONS` is untouched — the new fields are additive and don't
change what an agent must know before calling a tool.

### Validation

```
CARGO_TARGET_DIR=/scratch/cargo-target/hatchdoor-vault-identity-130
```

- `cargo fmt --all -- --check`
- `cargo clippy --all-targets -- -D warnings`
- `cargo test --all` — 697 library, 7 eval, and 3 CLI tests passed. 22 library
  tests in `git::managed_sync`/`git::managed_task`/`vault_runtime` failed;
  confirmed pre-existing and unrelated via `git stash` against the unmodified
  base commit — this sandbox's global `init.defaultBranch=main` conflicts with
  these fixtures' hardcoded `"master"` branch pushes.
- `node scripts/check-module-map.mjs` — 179 production source files assigned
  exactly once

### Review

Parallel Standards and Spec reviews found no hard violations and no scope
creep beyond the required mechanical updates to every existing
`NewVaultDefinition`/`VaultDefinitionEdit`/`CreateVaultRequest`/`EditVaultRequest`
construction site (exhaustive struct literals, mostly test fixtures). Spec
review found two real test-coverage gaps — `git::config::resolve_commit_identity`
and `AppState::vault_archive_prefix` had no direct tests — both fixed with
focused unit tests. Spec review also confirmed the issue's premise that
`src/mcp/tools/read.rs` has a separate `CreateVaultArgs` struct was inaccurate:
`create_vault_tool` parses directly into the shared `vaults::CreateVaultRequest`,
so the HTTP struct's new fields already cover MCP `create_vault` for free.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvBqEngeDUXPTj1udizyF4
…d sync errors

Closes #132.

- Lower MIN_MANAGED_GIT_POLL_INTERVAL_SECS from 3600 to 60, moving
  BACKOFF_MAX down with it so the backoff cap never exceeds the shortest
  allowed schedule. BACKOFF_BASE and DEFAULT_TICK_INTERVAL stay put; both
  ratios that justified their current values still hold at the new floor.
- Give VaultSource::ExistingGit its own poll_interval_secs (serde-defaulted,
  floor-checked only in PullOnly/TwoWay). managed_git_poll_interval now
  returns it for those modes, which — since ManagedGitScheduler
  registration, POST /sync, POST /retry, and the credential-replacement
  retry trigger were already written generically against that accessor —
  brings a remote-backed ExistingGit Vault onto the scheduler with no
  further branching needed at those call sites.
- Add optional structured detail to VaultRuntimeError (affected paths,
  capped at 50 with a true total; local-commits-ahead count), populated by
  classify_sync_error for the three sync failures a caller cannot act on
  from the message string alone. Carried by VaultWorkError as the
  plumbing between them. Reaches HTTP and MCP list_vaults for free, since
  both already serialize the same VaultRuntimeError.
- Update module-map.md's now-stale "ExistingGit is never scheduler-tracked"
  claims.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvBqEngeDUXPTj1udizyF4
Adds one focused test per acceptance bullet that wasn't already proven
literally rather than by inference:

- A one-minute (60s) schedule is accepted for both ManagedGit and
  ExistingGit, not just rejected below it.
- Ten consecutive transient failures plateau at the new BACKOFF_MAX (60s),
  not the pre-#132 one-hour cap.
- POST /sync and POST /retry now admit an ExistingGit PullOnly Vault and
  the scheduler tracks its interval on activation.
- The same two endpoints still refuse an ExistingGit LocalHistory Vault,
  the other half of that acceptance bullet.
- edit_vault/create_vault's MCP args parse poll_interval_secs on an
  existing_git source (the ticket's premise that inputSchema would reject
  it doesn't hold in this codebase — verified by direct deserialization
  rather than schema inspection).
- VaultRuntimeError's detail serializes as the documented tagged JSON
  shape and is omitted, not null, when absent — the actual wire contract
  list_vaults and HTTP discovery both publish.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvBqEngeDUXPTj1udizyF4
)

GET /api/v1/vaults gains an instance-level, always-serialized demo_mode
boolean on VaultDiscoveryResponse, true only under HATCHDOOR_DEMO_MODE.
Both list_vaults_handler construction sites (the Ready arm and the
registry-Recovery arm) carry it, so #122's recovery screens know the
instance posture too, not just the ordinary discovery path.

Router tests cover zero/one/many enabled Vaults and the registry-recovery
arm, each on both a demo instance and an ordinary one.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvBqEngeDUXPTj1udizyF4
BattermanZ and others added 9 commits August 13, 2026 19:08
Implements #137 only. The frontend on the integration branch still called the
unscoped endpoints #101 retired, so nothing in the browser worked. This is the
slice that restores it and is the foundation every other #102 slice builds on.

## Scope

Every browser call now targets `/api/v1/vaults/**`. Collection reads (tree,
recent, search, graph) declare `scope` explicitly as one Vault ID or `all`;
exact reads and every mutation name exactly one Vault. The canonical note URL
is `/v/{vault_id}/n/{slug}`; slug-only browser state (recent notes, last-note
restore, drafts, wikilink resolve cache) is now `(vault_id, slug)`-qualified
throughout. Vault scope exists as state/storage only (`useVaultScope`) with no
chrome yet — no Scope zone, no per-Vault status, no provenance, no accordion;
those are #114-#126. On a single-enabled-Vault instance every surface renders
exactly what single-Vault Hatchdoor renders today.

Shared multi-Vault test fixtures (`frontend/src/test/fixtures/vaults.ts`) land
here too: one, three, and eight Vaults, and a builder for each non-healthy
per-Vault condition the API can report, for the rest of the 16-ticket set to
assert against.

## Backend addition: rich per-Vault statistics

StatsPage's existing UI needs word counts, top tags, most-linked notes,
activity by month, and more — data the frozen `{scope}/stats` collection
route intentionally never carried (it stays lean: note/tag/link counts only).
The only producer of that richer report, `SqliteCache::vault_stats`, was
already unreachable from any route since #101 and queries the legacy
single-Vault-shaped cache tables the multi-Vault snapshot has moved past.

Added `GET /api/v1/vaults/{vault_id}/stats/detail` — an exact, single-Vault
read (never `all`, like `notes/{slug}`) — with a new `VaultReadCore::statistics_detail`
computing every field from the same published snapshot the lean `statistics`
projection already reads, reusing `collection`'s `VaultScope::One` gating for
identical not-found/disabled/unavailable behavior. `docs/migrations/vault-scoped-clients.md`
and the module map document the new route and the one legitimate deviation
from this file's own "exact reads never touch the disposable cache" rule
(this route needs mtime/size data only the snapshot carries).

## Judgment calls (documented, not silent)

- Multi-Vault `all`-scope explorer/graph rendering (no accordion/island chrome
  yet): grouped-by-Vault responses are concatenated at the top level without
  deep-merging same-named folders across Vaults. Every note/node still carries
  its own `vault_id`, so links and writes target the correct Vault. Accepted,
  temporary, superseded by #117/#118.
- Default write-target Vault with no active note open on a multi-Vault
  instance: the first enabled Vault in discovery order
  (`resolvePrimaryVaultId`). Superseded by #114's Scope zone.
- Topbar "Tree Stale" badge kept (its removal is #116/#117's job), now driven
  by the resolved scope's `partial` flag instead of the retired signal.
- Settings-nav visibility: `write-capabilities` dropped `settings_enabled` in
  #101; now derived from `GET /api/v1/vaults`'s `demo_mode` field.

## Validation

```
CARGO_TARGET_DIR=/scratch/cargo-target/hatchdoor-vault-identity-130
```

- `cargo fmt --all -- --check`
- `cargo clippy --all-targets -- -D warnings`
- `cargo test --all`
- `cd frontend && npm run format:check && npm run lint && npm run typecheck && npm test && npm run build`
- `node scripts/check-module-map.mjs`

## Review

Parallel Standards and Spec reviews both ran against the full diff (54 modified files, 3 new). Neither found hard violations, spec gaps, or scope creep — the explicitly excluded chrome (#114-#126) does not appear anywhere in the diff, and no backend surface beyond the one approved stats endpoint was touched. Standards review noted minor judgment calls, addressed here: `resolvePrimaryVaultId`'s docstring was too narrowly scoped to its write-target use and didn't explain its reuse for Stats' single-Vault default (both reviewers independently flagged this; fixed by widening the docstring), and three `/v/{vaultId}/n/{slug}` link constructions (Explorer.tsx x2, ChangesPanel.tsx) were missing `encodeURIComponent` unlike the rest of the diff (fixed for consistency, harmless in practice since Vault IDs are canonical UUIDs). Remaining notes accepted as documented judgment calls, not applied: the legacy `cache::queries::metadata::vault_stats` SQL function stays as dead code (already unreachable since #101, removing it is outside this ticket's declared owned paths); `writeApi.ts`'s `(vaultId, slug, hash)` positional-parameter pattern is left as-is rather than introducing a new bundling type, matching the ticket's "avoid new abstraction without demonstrated concrete value" guidance.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvBqEngeDUXPTj1udizyF4
Vault scope becomes readable and changeable in exactly one place on the
desktop: a collapsible zone pinned above the explorer rail, listing "All
Vaults" plus every enabled Vault in Vault-management order. The current
scope takes the same active treatment as the active note; the zone folds
with collapse state persisted per browser (default expanded), naming the
current scope where the count normally sits when collapsed. The open
note's own Vault carries a "Viewing" marker separate from the selected
scope, expanded on its row and as a second line under the collapsed head.
The topbar gets a matching muted, read-only echo of the Vault or scope
before the breadcrumb, with no scope control of its own. Absent entirely
at one enabled Vault or on mobile, where scope has nothing to offer yet
(mobile placement is #145).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148m68MTJK9124oCV4NSqEP
Adds a shared VaultPrefix component (hot ink, middot separator, never
elides) for the "marked path root" grammar and wires it into every
surface that can flatten notes across Vaults: Recently viewed, Changed
on disk, and search results — each shown only when scope is "all" and
more than one Vault is enabled, with the note page instead showing a
synthetic Vault property row whenever more than one Vault is enabled,
independent of scope, since an exact read is never ambiguous about its
own Vault. Search's result path now elides head-first via a dedicated
result-path-text span, so the file name (not the Vault name) survives
truncation. A single-Vault instance renders every surface unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148m68MTJK9124oCV4NSqEP
Each Vault's row in the sidebar Scope zone ends in one trailing slot:
its note count when healthy, a shimmering placeholder while indexing
(including a first index that has never published a snapshot — the
API reports unavailable for that and for a vanished directory alike,
but activation stays active for the former, verified against
activation_snapshot in src/vault_runtime.rs), or one condition word
otherwise — never a count and a condition together. The red tier
(sync stopped, conflict, unavailable) takes a bordered ground and
medium weight; the amber tier (stale, sync failed) stays plain ink,
since --warn-fg and --err-fg aren't reliably tellable apart at this
size. Hover text and the accessible name carry the Vault's own
runtime error message; the word on screen stays terse.

The All Vaults row and the collapsed Scope head share one aggregate:
the enabled-Vault count when everything is healthy or indexing, or
the shortfall against the worst tier present otherwise. The collapsed
head's scope name now takes that worst ink too, so narrowing scope
never hides trouble elsewhere.

Adds a new lightweight useVaultNoteCounts hook that fetches the lean
collection-scope GET /api/v1/vaults/all/stats (VaultStatistics — its
first frontend consumer) to feed the healthy count, always at "all"
scope and gated on more than one enabled Vault, refetched on the same
vaultRevision useVaultTree already tracks rather than opening a
second SSE subscription.

Deletes the topbar's Tree Stale badge with nothing replacing it, per
#116 (amended by #117): indexing runs on every save, so treating "the
index is behind" as a warning produced noise several times an hour.
Offline is the only condition left there, since it is about the
workspace and not about any one Vault.

Documents the new primitive in docs/design/design-system.html per
#116's explicit instruction.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148m68MTJK9124oCV4NSqEP
…ocks (#141)

Never a banner, never a change to ranking. A partial collection read
(Changed on disk, Search) shows results in the API's own order with
one trailing warn-ink line below the last row naming only the Vaults
that did not answer; with nothing usable, that line replaces the
empty state entirely via a new StateBlock tone="error" (the
documented red-heading §23 variant, previously unreachable — the
component had no way to opt into it). New shared lib/vaultParticipants.ts
reduces a VaultReadProjection's participants to the missing names and
the shared sentence, consumed by both ChangesPanel and SearchDialog.

Escalation is triggered by the write attempt, not by the condition
alone. NotePage now imports deriveVaultSlot from #139's
app/vaultSlotLogic.ts to detect the open note's own Vault being
git-unavailable with a sync-stopped or conflicted condition: the
save-state slot reads "Not saving" and a full-bleed notice appears
before a doomed save is ever attempted (autosave's own enabled flag
is gated on the same check, so nothing pointless hits the network;
editing itself stays on, and the drafts safety net keeps the edit).
A note that fails to read now also renders the same StateBlock error
tone. Every other non-healthy condition, or trouble in a Vault that
is not the one being read or written, raises nothing beyond its
sidebar slot.

Covered at three and eight Vaults per the acceptance criteria, plus a
regression test confirming a non-blocking condition (stale) stays
quiet everywhere but its own slot.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148m68MTJK9124oCV4NSqEP
The search dialog carries its own filter — a lens over the answer in
front of you — which dies when the dialog closes and never touches the
sidebar's browsing scope (#119). Structurally enforced: SearchDialog
has no onScopeChange prop to call.

Desktop gets a facet rail beside the results: every Vault the search
reached, in Vault-management order, each ending in #116's own
count-or-condition slot (a plain count, "0" inert when a Vault
answered with nothing, or "no answer" in error ink and aria-disabled
when it didn't — a missing facet would be an absence, a named one is
a fact). "All results" is the permanent widening row. Absent when
scope is narrowed or at one enabled Vault, since there's nothing to
filter across and the Scope zone is already on screen. Selecting a
facet is a client-side filter over the already-fetched results — no
re-fetch, no re-ranking.

Phone gets the same filter as a Scope field beside Mode in a field
strip under the input (reusing App.css's .field grammar), replacing
the desktop Mode checkbox below 920px via the same CSS breakpoint
responsive.css already uses. Scope stays even when the browsing scope
is narrowed, because the panel covers the Scope zone there and the
always-visible-scope rule needs the field to carry it regardless.

Tapping a tag now hands the dialog that note's own Vault to pre-select
in its filter (tags are per-Vault vocabularies): NotePage's onTagSelect
became (tag, vaultId) => void, wrapping the call to NoteProperties with
its own vaultId. useSearch exposes the new searchParticipants and a
searchInitialVaultFilter that's cleared the moment the dialog closes,
so a later plain open never inherits a stale preselection.

Weighted the tests toward the invariant that's easy to quietly break:
an explicit test asserts a facet click invokes none of the component's
actual callback props, on top of coverage for row order, counts, the
0-vs-no-answer distinction, filtering, remount-resets-the-filter, and
the mobile field strip sharing state with the desktop rail.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148m68MTJK9124oCV4NSqEP
Under `all` scope with more than one enabled Vault, every participating
Vault's component is laid out on its own and placed as a captioned,
dash-enclosed island on one shared canvas with one zoom and one pan
(#118's resolution). The shape never changes with Vault count or with how
many actually answered: a Vault down doesn't collapse the field back to
plain, it just leaves one fewer island and a warn-ink line naming the gap.
A single-enabled-Vault instance or any narrowed scope stays byte-identical
to today's single-Vault graph.

Reuses #139's count-or-condition slot (`deriveVaultSlot`) for each
island's caption and #141's partial-read pattern for the not-drawn line's
tone, with new pure layout primitives (`computeIslandCenters`,
`buildIslandGraphs`, `createIslandSimulation`) alongside the untouched
single-graph path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0148m68MTJK9124oCV4NSqEP
Under all scope with more than one enabled Vault, the explorer tree
becomes an accordion of per-Vault sections: exactly one Vault's tree
shows at a time, every other Vault keeps a permanent one-line head
using the documented section head with #139's own count-or-condition
slot trailing. Unfolding only unfolds — it never calls setScope, and
an unavailable Vault's head stays aria-disabled and never opens.

useVaultTree now exposes vaultTrees (each participating Vault's own
tree, grouped rather than merged) alongside the existing merged tree,
which narrowed-scope and single-Vault-instance rendering keep using
unchanged. The accordion's own state (app/vaultAccordion.ts) resolves
the three landing defaults directly off the URL and stored last-note,
rather than the open note's own content fetch, so it never races
App.tsx's last-note redirect; narrowing scope always sets the
accordion's remembered Vault to the one just left, so widening
restores it. Folder-open memory is namespaced per Vault so two
Vaults' identically-named folders don't share open/closed state.

SideHead (Explorer.tsx) gains optional slot/disabled/className props
to carry the count-or-condition slot and the aria-disabled treatment;
existing callers are unaffected. pathToNoteIdentity moves out of
Explorer.tsx into lib/notePath.ts, shared with the accordion's landing
resolution and fixing a lint rule against non-component exports from
a component file. storage.ts gains getStoredLastNote(), extracted
from App.tsx's own last-note redirect so both read the same parse.

Documents the new primitive in docs/design/design-system.html and
docs/architecture/module-map.md.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SvBqEngeDUXPTj1udizyF4
Below 920px the topbar's second row becomes the scope row (browsing
scope name, viewing marker, count-or-condition slot), replacing the
note path — the note's own Vault property row/title and the search
magnifier already cover that. Tapping the row opens a bottom sheet of
scope rows at 46px, All Vaults first, reusing the desktop Scope zone's
.scope-row grammar; picking one sets scope and dismisses the sheet.

The viewing marker only fires when an exact read's Vault differs from
a *narrowed* scope — never at "all", where every open note is already
in scope by definition. Truncation under width pressure is staged via
differential flex-shrink (rule collapses first, then the name
ellipsizes, then the status word, which never truncates).

scopeName moved from ExplorerPane.tsx to vaultSlotLogic.ts so both the
desktop Scope zone and the new mobile row can share it without an
eslint react-refresh violation.

Not verified in a live browser: the shared Playwright session was
unavailable for the whole session, so the truncation-priority CSS is
implemented and reasoned through but not screenshot-confirmed at the
narrowest supported width — flagged as an open acceptance criterion
on the issue rather than checked off.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcmFEh4ybZ5cZyzK89eyXX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant