Skip to content

Quality delta review 2026-07-14: moments N+1 batch, token-formula drift, DB atomicity (D1-D5) - #9

Merged
codenamev merged 7 commits into
mainfrom
quality/delta-review-2026-07-14
Jul 16, 2026
Merged

Quality delta review 2026-07-14: moments N+1 batch, token-formula drift, DB atomicity (D1-D5)#9
codenamev merged 7 commits into
mainfrom
quality/delta-review-2026-07-14

Conversation

@codenamev

Copy link
Copy Markdown
Owner

Applies the findings from the 2026-07-14 delta quality review (docs/quality_review.md). A two-agent expert-lens pass (Metz/Grimm/Bernhardt + Evans/Beck) over the 32-commit delta since 2026-07-01, every finding re-verified against source. The prior review's structural fixes all held (SQLiteStore 600, api.rb 622, maintenance.rb 296; bare rescues 6, rescue Exception 0); these are the new items it surfaced.

Stacked on #8. D3 modifies the C015 audit check introduced in #8, so this branch is based on feature/influencer-restudy-2026-07-09, not main. GitHub will retarget the base to main automatically once #8 merges.

Fixes

  • D2 (High) — batch the moments recall/context N+1. enrich resolved top-facts per row for recall_hit/recall_empty/context_injection moments — one facts.where(id:) + one FactPresenter entity load each. A 50-moment recall page fired ~100 queries; worst case ~400/request. Added ScopedFactResolver.merge_scoped_ids + build_fact_index (one facts query + one entity load per scope for the whole page) + pure resolve_from_index; Moments#list builds the index once. This closes a second N+1 the prior review's Q7 batching left open (it only covered the extraction/ingest path). ~400 → ~3 queries.
  • D1 (Med) — token-formula drift. ObservationStats#source_tokens_for re-derived the token estimate with .round while the observation token_count denominator uses TokenEstimator.from_chars (.ceil) — the compression ratio compared ceil-vs-round. Now calls from_chars. Bundled with value-object nits: TokenBudget#min/max return 0 (not nil) on empty + freeze; Jaccard dead-branch removal; TokenEstimator#estimate defers to from_chars, classmodule.
  • D3 (Med) — stream C015 with paged_each. The truncation audit loaded every content item's raw_text (largest column, both DBs) into memory at once. Now streams rows_per_fetch: 500, the pattern LexicalFTS#rebuild! already uses.
  • D4 (Med) — VectorIndex#backfill_batch! atomicity. vec0 rows were INSERTed (auto-committing) with vec_indexed_at set in a batch update after the loop; a mid-loop crash left vec0 rows flagged nil, so the next backfill re-INSERTed them without a DELETE → duplicate embeddings that never self-heal. Wrapped the loop + flag update in one @db.transaction, mirroring insert_embedding.
  • D5 (Med) — Reflector#reflect! retryable. Used a raw @db.transaction, not transaction_with_retry; reflection runs in the sweep hook racing ingest for the WAL writer. Now retries the whole transaction on SQLITE_BUSY. The individual mutators stay un-wrapped by design — they run inside this transaction, so retry belongs at the boundary, not the statement.
  • Test — LexicalFTS#rebuild! rollback contract. The transaction wrap had happy-path-only coverage; added a spec that raises after the DROP and asserts the old index still answers.

Verification

  • Full suite 2435 examples, 0 failures, 2 pending (+8 new tests, incl. D2 batch-resolution specs and the rebuild! rollback test).
  • rake audit:error_handling green (0 blocking).
  • Standard lint clean; every commit passed the pre-commit hook chain.

Deferred

HistoricalCleanup#decide_restore extraction (Low) — readability polish on one-shot historical-cleanup code; the gain doesn't justify the refactor risk. Noted in docs/quality_review.md.

Delta-review (2026-07-14) value-object cluster:

- D1: ObservationStats#source_tokens_for re-derived the token formula with
  .round while the observation token_count denominator uses
  TokenEstimator.from_chars (.ceil). Compression ratio now compares like with
  like — call from_chars so the 4-chars/token constant lives in one place.
- TokenEstimator#estimate now defers to from_chars (arithmetic in one place);
  class -> module (only self. methods, never instantiated).
- TokenBudget#min/#max return 0 (not nil) on an empty budget, matching avg/
  p50/p95; the value object now freezes itself.
- Jaccard.score: removed the unreachable union.zero? guard (the empty-set
  guard already covers both-empty).
The prior N+1 fix batched the extraction/ingest content+facts path, but
recall_hit/recall_empty/context_injection moments still resolved their
top-facts per row: each build_moment called ScopedFactResolver.resolve, one
facts.where(id:) query PLUS one FactPresenter entity load per moment. A
50-moment recall page fired ~100 queries; up to 2xlimit events are enriched
before the trim, so worst case ~400 queries per /api/moments request.

- Add ScopedFactResolver.merge_scoped_ids + build_fact_index (one facts query
  and one entity load per scope for the whole page) + pure resolve_from_index.
- Moments#list builds the index once and threads it through build_moment/
  enrich; the per-row resolve_scoped_facts is gone.
- ScopedFactResolver.resolve stays (still used by api.rb's single-event path).

Batch behavior pinned by new specs (per-scope query, pure lookup, order
preserved); existing moments top_facts specs unchanged and green.

Refs: docs/quality_review.md D2 (2026-07-14)
truncated_source_content loaded every content item's raw_text (the largest
column, up to the ingest cap per row) from both DBs into memory at once to
regex-scan. On a mature DB that's an unbounded full-table load. Stream a page
at a time (rows_per_fetch: 500), accumulating flagged ids — the same pattern
LexicalFTS#rebuild! uses. Behavior unchanged; runner_spec C015 green.

Refs: docs/quality_review.md D3 (2026-07-14)
- D4: VectorIndex#backfill_batch! INSERTed vec0 rows (each auto-committing)
  and set vec_indexed_at in a batch update AFTER the loop. A crash mid-loop
  left vec0 rows with vec_indexed_at nil, so the next backfill re-INSERTed
  them without a DELETE -> duplicate embeddings that never self-heal. Wrap the
  loop + flag update in one @db.transaction, mirroring insert_embedding; the
  'no DELETE needed' invariant now genuinely holds.
- D5: Reflector#reflect! used a raw @db.transaction, not transaction_with_retry.
  Reflection runs in the sweep hook racing ingest for the WAL writer; retry the
  whole transaction on SQLITE_BUSY. The individual mutators stay un-wrapped by
  design (they run inside this transaction; retry belongs at the boundary).

Refs: docs/quality_review.md D4/D5 (2026-07-14)
The transaction wrap on rebuild! (DROP + recreate + reinsert) had only
happy-path coverage; the load-bearing branch — a mid-rebuild failure rolls
back the DROP and leaves the old content_fts intact — was untested. Add a
spec that raises after the DROP and asserts search still answers, so the
atomicity guarantee can't silently regress.

Refs: docs/quality_review.md (2026-07-14, Beck)
Base automatically changed from feature/influencer-restudy-2026-07-09 to main July 16, 2026 19:57
… C015 var

- resolve_from_index now uniqs each event's id list so a repeated id emits its
  fact once, matching the row-set dedup the query-based ScopedFactResolver.resolve
  gets for free. Regression spec added.
- Drop the unused 'total' accumulator in the C015 check (dead before and after
  the paged_each change; the Finding only uses flagged).
@codenamev
codenamev merged commit ec7bacf into main Jul 16, 2026
1 check failed
@codenamev
codenamev deleted the quality/delta-review-2026-07-14 branch July 16, 2026 20:10
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