Skip to content

Index-backed reads: Find/Visible/Prime/Status query index.sqlite instead of scanning entries#42

Merged
quad341 merged 2 commits into
mainfrom
deploy/crn-2xpm-gate
Jul 23, 2026
Merged

Index-backed reads: Find/Visible/Prime/Status query index.sqlite instead of scanning entries#42
quad341 merged 2 commits into
mainfrom
deploy/crn-2xpm-gate

Conversation

@quad341

@quad341 quad341 commented Jul 23, 2026

Copy link
Copy Markdown
Owner

What this changes

cairn find, cairn prime, and the store's Visible/Status checks now
answer from the SQL index (index.sqlite) instead of scanning and decoding
every entry's TOML body on every call. Behavior is unchanged from the
caller's side — same results, same CLI output — but lookups no longer pay
the cost of a full-store body decode, and Reindex gained two concurrency
fixes:

  • entry_tags' schema (re)creation now runs inside the same write
    transaction as the per-entry upsert work, instead of as separate
    autocommit statements — eliminating a race where two concurrent
    Reindex calls could hit a hard table entry_tags already exists error.
  • The SQLite DSN now sets _txlock=immediate, so Reindex's write lock is
    acquired at BEGIN (where busy_timeout's retry logic applies) rather
    than lazily at the first write statement — closing a second race where
    concurrent CLI invocations against a shared store could hit a spurious
    hard "database is locked" failure instead of waiting.

cairn prime's scope-mismatch diagnostic (warns when a topic's tags don't
match any configured scope dimension) is preserved on top of the new
index-backed read path — see Review notes.

Why one PR

This bundles what were originally several beads because they're one
continuous refactor of the same read path (Find, Visible, Prime,
Status, Reindex) — later pieces build directly on earlier ones' schema
and query changes, and none is independently shippable without the others.

Review notes

  • internal/cairn/entry.go's Visible and internal/cairn/prime.go's
    Prime both gained a leading context.Context parameter and now source
    their data from a single shared Status(ctx, store) call rather than
    walking every entry. This reconciles against main's independently-landed
    scope-mismatch diagnostic — the two features were built in parallel
    against incompatible Visible/Prime signatures, and this PR carries the
    merged result.
  • Two symbol collisions surfaced from other work landing on main
    mid-flight and are resolved by rename, not merge, since the contracts
    differ: splitFrontmatterForPatch ([]byte, keeps closing fence) stays
    distinct from entry.go's splitFrontmatter (string, no fence);
    ReviewMergeBranch/ListReviewMergeBranches (interactive review flow)
    stay distinct from branches.go's ReviewBranch/ListReviewBranches
    (stale-branch sweep).
  • No new config, no new endpoints, no schema migration required by
    operators — index.sqlite self-heals/rebuilds via the existing
    freshness-check path.

Test plan

  • go test ./... -race -count=1 — 0 regressions across all packages
  • golangci-lint run ./... — 0 issues
  • gofmt -l . / go vet ./... — clean

🤖 Deployed by actual-factory

quad341 and others added 2 commits July 23, 2026 12:18
…r rebase (crn-2xpm)

Squashes builder/crn-2xpm-reconcile's commits since merge-base f6970f5
(net diff only, unchanged) into one commit re-applied cleanly on current
origin/main, so a linear rebase no longer replays this branch's internal
merge commits' pre-resolution states as independent patches.

Deploy gate's attempt_bounded_self_rebase found a real conflict (rc=12)
rebasing this branch's history onto origin/main, even though a 3-way
git merge-tree of the same two trees was clean. Root cause: this branch
contains merge commits (9168234, 3479d32, cc6ceb2, 8d913c5) that already
3-way-resolved conflicts between parallel feature branches and against
origin/main. A linear rebase instead replays each original commit
individually -- including pre-resolution commits like 9724ae9, whose
lone-patch content still conflicts with what origin/main looks like now,
even though that conflict was already resolved once, downstream, via
those merge commits. Merging origin/main again (a third time) would not
fix this: it resolves the conflict at the merge point but leaves the
same problematic commit replayable-and-conflicting on any subsequent
rebase. Flattening the branch's own history is the only durable fix.

Net change (unchanged from e9cdcd2, verified identical via `git diff
--cached --stat` vs `git diff f6970f5 e9cdcd2 --stat`): the index-backed
reads stack (Status/Visible/Prime/Find all index-backed, ctx-threaded,
zero full-body TOML decode or hit_count writes on read paths), the
entry_tags DROP+CREATE DDL race fix + txlock=immediate (crn-j3k4), the
busy_timeout/WAL locking fix (crn-t250), crn-ln1's scopeMismatchWarnings
diagnostic reconciled onto the index-backed Status read, and the 4
review_test.go/cmd/review_test.go call sites updated to the new
Find(ctx, store, id) signature after PR #40 landed with the old one.

New SHA per crn-fie5 SHA-pinning mandate -- requires fresh reviewer
verdict; e9cdcd2's PASS does not carry over. Old branch
builder/crn-2xpm-reconcile (tip e9cdcd2, already-open PR #41) left
untouched as historical reference, same treatment as the PR it
superseded.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
@quad341
quad341 merged commit ac691c3 into main Jul 23, 2026
2 checks passed
quad341 added a commit that referenced this pull request Jul 24, 2026
…itive, recall stamping + reporting (#46)

* test(cairn): red — cairn review merge: add --kind/--auto-actionable reviewer-patch flags (refs crn-28ge.1.2)

* feat(cairn): green — cairn review merge: add --kind/--auto-actionable reviewer-patch flags (refs crn-28ge.1.2)

* test(cairn): red — extract dedup primitive; expose kind/auto_actionable/conflicts via cairn get (refs crn-28ge.1.3)

internal/cairn/dedup_test.go: coverage for a not-yet-existing exported
Conflicts(candidate, others) []DedupFinding — topic_key match, content
match, shadow-exemption on both signals, incomparable-scope-still-flagged,
both-signals-fire-for-one-pair (documents Conflicts is deliberately not
tier-scoped, unlike topicKeyCollisions), no-signal, self-skip, sort order.

cmd/commands_test.go: execRoot-based coverage of `cairn get`'s new
kind/auto_actionable/conflicts output lines.

internal/cairn fails to compile (undefined: Conflicts); cmd's new get
tests fail on missing output — expected red.

* feat(cairn): green — extract dedup primitive; expose kind/auto_actionable/conflicts via cairn get (refs crn-28ge.1.3)

internal/cairn/dedup.go: extract pairSignals (topic_key equality +
Title/Summary Jaccard similarity, shadow-exemption-aware) as the single
shared primitive; contentSimilarityPairs (Dedup's whole-store scan) now
calls it per-candidate instead of duplicating the comparison, and the
new exported Conflicts(candidate, others) reuses it for single-candidate
callers. Per NFR-05 this is one shared implementation, not a second
independently-drifting one. Conflicts is deliberately tier-agnostic
(match OR similarity, no tier restriction) for the recall-time
conflict-check use case; topicKeyCollisions' same-tier-only behavior for
Dedup's own bead-filing path is intentionally left unrefactored.

cmd/commands.go: getCmd now prints kind (defaulting to "note") and
auto_actionable, and computes conflicts against other visible entries
sharing the candidate's topic. Sources "others" data from IterEntries
(filtered to the ID set from Visible) rather than Visible's output
directly, since Visible never populates Title/Summary — passing it
straight to Conflicts would make the content-similarity signal always
silently miss.

Existing cairn dedup output/tests are unaffected (all 12 pre-existing
dedup/similarity tests pass unmodified); all 8 new Conflicts tests and
5 new getCmd tests pass. gofmt/vet/build/golangci-lint/test all clean;
internal/cairn race-enabled tests pass.

* test(cairn): red — Find() stamps last_recalled_at alongside hit_count (refs crn-28ge.1.5)

TestFindStampsLastRecalledAt fails: Find never sets Entry.LastRecalledAt
today, though the field/column have existed since crn-28ge.1.1. Also locks
in the same never-touched-by-Visible/Status scoping hit_count already has.

* feat(cairn): green — Find() stamps last_recalled_at alongside hit_count (refs crn-28ge.1.5)

Extends Find's UPDATE ... SET hit_count = hit_count + 1 ... RETURNING
transaction to also set last_recalled_at = ? (RFC3339) and RETURN it into
Entry.LastRecalledAt, in the same statement. Scoping is identical to
hit_count by construction: getCmd/freshnessCmd/verifyCmd all route through
Find and pick this up automatically; mapCmd/primeCmd go through
Visible/Prime and never touch it (crn-6az.6.1.3's dependency, previously
believed unmerged per this bead's own note, is confirmed landed on
origin/main via superseding PR #42, commit ac691c3).

* test(cairn): red — recall-stats/promote-candidates reporting (refs crn-28ge.1.6)

RecallStats and PromoteCandidates don't exist yet: compile-error RED
against internal/cairn.RecallStats/PromoteCandidates and their finding
types.

* feat(cairn): green — recall-stats/promote-candidates reporting (refs crn-28ge.1.6)

internal/cairn/recall.go: RecallStats and PromoteCandidates, both
built on a shared loadEntryRecallRows index read (NFR-05) rather than
each hand-rolling its own SELECT.

RecallStats reports every entry's HitCount/LastRecalledAt (FR-08) --
report-everything, not a filter. PromoteCandidates filters entries
where RecurrenceCount >= threshold AND PromotedBeadID == "" (FR-07,
NFR-02 idempotency), always including Anchor.Repo so a downstream
consumer files the resulting bead in the entry's own repo/tracker
(crn-28ge.1.8) rather than defaulting to wherever the librarian runs.

Both are strictly read-only: no store mutation, no bd create -- that
happens downstream in the librarian formula step.

* feat(cairn): green — cairn recall-stats + cairn promote-candidates commands (refs crn-28ge.1.6)

Thin cobra wrappers mirroring cmd/dedup.go's shape: identity-guard,
call into internal/cairn, nil-guard the findings slice, JSON-encode.
promote-candidates gets a --threshold int flag (default 3, NFR-06);
recall-stats takes none. Neither filters by identity -- both cover
every entry, same as dedup/stale-branches.

* chore: release gate PASS for cairn-kind-dedup-conflict-recall (re-cut to include crn-28ge.1.6)
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