Skip to content

fix(watcher): suppress echo view-rebuild (U2b) + refuse mis-splittable id recovery (U1d)#3

Merged
JustMaier merged 2 commits into
mainfrom
fix/decorated-extract-hardening
Jul 3, 2026
Merged

fix(watcher): suppress echo view-rebuild (U2b) + refuse mis-splittable id recovery (U1d)#3
JustMaier merged 2 commits into
mainfrom
fix/decorated-extract-hardening

Conversation

@JustMaier

Copy link
Copy Markdown
Owner

Follow-up to #1/#2. Two latent-in-main issues from Arabella's review + Donovan's live probe. Do not merge until liz + arabella review this diff and Donovan re-runs his probe (per team norm).

U2b — own-write echo triggered a redundant view rebuild

The self-write ledger suppressed the index mutation for a recognized echo, but apply_watcher_events still marked the collection affected → rebuilt the view → notify_view. So on_view_change fired 2× per API write (Donovan measured; he corrected an earlier JOIN hypothesis). Fix: process_single_watcher_event returns Result<bool> (did the index change); mark affected only on true. Echo / benign-skip / stale-remove → false → no rebuild. Now exactly 1 fire.

U1d — id recovery from filenames is unsound for separator-colliding alphabets

extract() splits a non-terminal field at the next delimiter — only unambiguous when the field's alphabet excludes that delimiter. ulid (Crockford base32) + date-format fields are safe; uuid (-), nanoid (-,_), and slugged string/ref values (-) are not → wrong ids on reindex → the U1 duplicate-row bug returns. And because ids are path-derived (not stored), such a store can't survive index reconstruction from disk.

Fix — invariant is "no silent wrong ids", not "no store opens":

  • Detect mis-splittable collections at open (generalized non-terminal-delimiter rule); loud 3-part warn per collection (reads may be empty, reindex refused, migration hint). Open never fails.
  • id_for_path looks up the index by path first (authoritative for any id strategy once indexed); extract is a fallback for unindexed files, treated as untrusted for mis-splittable collections.
  • Reindex/rebuild refuse mis-splittable collections (full/incremental scan skip so boot succeeds; explicit rebuild + watcher-index of an unindexed file → targeted error → ChangeEvent::Error). API writes unaffected (id known at write).
  • Reconcile skipped for mis-splittable collections → a mis-split value can't overwrite correct front matter (Arabella finding fix(watcher): suppress echo view-rebuild (U2b) + refuse mis-splittable id recovery (U1d) #3, test confirms).

U1c (explicit-id decorated + on_conflict:suffix clobber) is subsumed: such templates are mis-splittable → reindex refused → clobber impossible; ulid-decorated never suffixes (unique ids).

Adds SystemDb::get_id_by_path. +4 tests. 151 grounddb / 173 workspace green, 0 new warnings.

🤖 Generated with Claude Code

JustMaier and others added 2 commits July 3, 2026 00:09
… id recovery (U1d)

U2b — own-write echo no longer triggers a redundant view rebuild:
process_single_watcher_event now returns Result<bool> (did the index
change), and apply_watcher_events marks a collection affected only on a
real change. A suppressed echo, benign vanished-file skip, or stale Remove
returns false, so on_view_change fires exactly once per API write instead of
twice (the ledger already prevented the index mutation; this stops the
redundant rebuild+notify). Root cause confirmed empirically by Donovan.

U1d — id recovery from filenames is only sound when a non-terminal field's
value alphabet excludes the delimiter that follows it. ulid (Crockford
base32) and date-format fields are safe; uuid ('-'), nanoid ('-','_') and
slugged string/ref values ('-') are not. A mis-splittable template silently
produces wrong ids on reindex — and since ids are path-derived, not stored,
such a store cannot survive index reconstruction from disk.

Fix, honoring "no silent wrong ids" without "no store opens":
- Detect mis-splittable collections at open (generalized non-terminal-
  delimiter rule); warn loudly per collection with a 3-part message (reads
  may be empty/incomplete, reindex refused, migration hint). Open never
  fails on this.
- id_for_path now looks up the index BY PATH first (authoritative for any id
  strategy once indexed); extract is only a fallback for unindexed files, and
  is treated as untrusted for mis-splittable collections (returns None).
- Reindex/rebuild refuse mis-splittable collections (full/incremental scan
  skip with a warn so boot still succeeds; explicit rebuild + watcher-index
  of an unindexed file return a targeted error → ChangeEvent::Error) rather
  than write wrong ids. API writes are unaffected (id known at write time).
- Reconcile (directory-wins frontmatter rewrite) is skipped for
  mis-splittable collections, so a mis-split extraction can't overwrite
  correct front matter (Arabella finding #3).

U1c (explicit-id decorated + on_conflict:suffix clobber) is subsumed: such
templates are mis-splittable → reindex refused → the clobber can't occur;
ulid-decorated never suffixes (unique ids, no path collision).

Adds SystemDb::get_id_by_path. +4 tests (U2b echo-no-rebuild, mis-splittable
detection across schemas, open+reads-work/reindex-refused, reconcile-no-
corruption). 151 grounddb / 173 workspace green, 0 new warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Amendment from PR #3 review (liz + arabella).

Two-tier enforcement (liz): a mis-splittable template no longer always
"opens and degrades". The tier depends on whether the collection already
has documents on disk:
- Has data → open succeeds, loud warn, reindex/rebuild/reconcile refused
  (never lock a user out of data they must migrate).
- No data (new config / template change before first write) → HARD reject
  at load. Nothing to lock out, and a config that cannot survive index
  reconstruction from disk is broken, so fail fast. Adds
  collection_has_documents() to pick the tier.

Review nits (arabella):
- The ulid alphabet arm now checks is_ascii_alphanumeric() rather than
  returning false unconditionally, so an alphanumeric literal delimiter
  (e.g. {id}x{title}) is correctly flagged (ulid can emit 'x').
- Documented the intentional rebuild(None) warn-skip vs rebuild(Some) error
  asymmetry.

README updated: the constraint is now enforced at load (reject new /
warn+refuse existing), not "planned".

+2 tests (hard-reject on no data; ulid-before-alphanumeric-delimiter
hazard) and the existing degrade-path tests now seed on-disk data so they
exercise the open-and-degrade tier. 152 grounddb / 174 workspace green,
0 new warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JustMaier
JustMaier merged commit 4d9cd49 into main Jul 3, 2026
1 check passed
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