Skip to content

Bound cairn prime cost on payload size and freshness-check count#57

Merged
quad341 merged 4 commits into
mainfrom
deploy/crn-ouoi-gate
Jul 25, 2026
Merged

Bound cairn prime cost on payload size and freshness-check count#57
quad341 merged 4 commits into
mainfrom
deploy/crn-ouoi-gate

Conversation

@quad341

@quad341 quad341 commented Jul 25, 2026

Copy link
Copy Markdown
Owner

What changed

Reworks cairn prime to bound its cost on two independent axes regardless of
store size: response payload bytes and freshness-check git shell-outs.

Prime now returns a structured result (deterministically ordered by hit
count, then recency, then ID) with itemized entries truncated once they
exceed a byte budget — but aggregate freshness counts (fresh/stale/unknown)
always cover the entire visible set, not just the truncated slice that made
it into the itemized output. Freshness checks that require a git shell-out
are capped per call (past the cap, remaining entries are reported as
"unknown" rather than skipped silently or fabricated as fresh). Rendering to
human-readable text is now a separate step over the structured result rather
than being baked into the computation itself.

Also brings prime's "no topic" display in line with the shared label used by
map, get, and list, so the four commands can't drift out of sync on
that string independently.

Why

prime is meant to be injected automatically at session start. Without a
byte budget, a large store could bloat every session's starting context;
without a cap on freshness checks, a large store could make session start
slow. Both bounds needed to hold regardless of how big the store grows.

What to review

  • Truncation semantics: once an entry doesn't fit the remaining byte budget,
    itemization stops entirely rather than skipping ahead to smaller,
    lower-priority entries — this preserves priority order in what gets shown.
    Freshness aggregate counts are computed before truncation is applied, so
    they stay accurate regardless of what made it into the itemized list.
  • The freshness-check budget: only the one classification path that actually
    shells out to git is metered: every cheaper classification is free and
    unaffected by the cap.
  • The rendering function's separation from computation, and its signature.

Test plan

  • go test ./... -race -count=1 — all packages green.
  • gofmt -l ., go build ./..., go vet ./..., golangci-lint run — all clean.
  • Manual: ran cairn prime against a store with more entries than the byte
    budget allows and confirmed the truncation note and freshness totals both
    read correctly.

quad341 added 4 commits July 25, 2026 02:27
…ness)

Prime now returns a structured PrimeResult (deterministically ordered by
hit_count desc, created_at desc, id asc) instead of a preformatted string,
itemized up to a caller-supplied byte budget while always computing
fresh/stale/unknown counts over the entire visible set regardless of
truncation. Freshness checks that need a git shell-out are capped per call
(maxFreshnessChecksPerPrime) and fail toward unknown past the cap, never
fabricating fresh. RenderPrimeText produces the human-readable text `cairn
prime` prints by default; cmd/prime.go gains a --budget-bytes flag.

In-scope side fix: Check()'s branch order now checks anchor shape, then
never-verified, before the expensive ComputeFingerprint call, so a
never-verified files anchor no longer pays for a git shell-out whose answer
doesn't depend on the result. Benefits all four Check() callers, not just
Prime.

Status()'s SELECT is extended to also read title/summary/hit_count --
already-indexed columns at zero marginal query cost -- so Prime can render
entries without a body read. This is a deliberate, reviewed exception per
crn-0vqk.1's design, not a precedent for further ad hoc columns.

Implements crn-0vqk.2 per the crn-0vqk.1 design.
…RenderPrimeText signature)

Addresses cairn/reviewer's REQUEST-CHANGES verdict on crn-0vqk.2:

- Finding 1 (blocking): Prime()'s truncation loop used continue instead
  of stopping, so a later lower-priority-but-cheaper entry could slip
  into Items past a pricier higher-priority one that got skipped.
  Latches a truncating flag on the first over-budget item so itemization
  stops there while freshness classification (and its aggregate counts)
  still runs for every visible entry. New regression test uses
  varying-cost fixtures across the priority order, verified to fail
  under the old continue-only behavior and pass under the fix.
- Finding 2: corrected orderByRelevance's doc comment, which claimed
  CreatedAt is RFC3339 -- the only production stamping call site
  (remember.go) uses time.DateOnly.
- Finding 3 (blocking addendum): dropped RenderPrimeText's redundant
  store/identity parameters (store was already dead, discarded via
  `_ = store`) in favor of reading r.Store/r.Identity directly, per the
  reviewer's resolution of the crn-0vqk.1 prose-vs-ER-diagram spec
  disagreement. Updated the cmd/prime.go call site and all test
  call sites.

refs crn-0vqk.2
Post-rebase reconciliation onto origin/main (PR #53 added
cairn.UntopicedLabel as the shared "no topic" sentinel for map/get/list;
this branch's Prime() rewrite predates that and had its own literal
"(untopiced)" string in RenderPrimeText). Same string value, no behavior
change -- keeps prime consistent with the other three commands so they
can't drift on this sentinel independently.
@quad341
quad341 merged commit 24a45b3 into main Jul 25, 2026
2 checks passed
@quad341
quad341 deleted the deploy/crn-ouoi-gate branch July 25, 2026 09:37
quad341 added a commit that referenced this pull request Jul 25, 2026
…post-rebase reconciliation)

Supersedes this branch's earlier gate evaluation (eef42ec), which
assessed criterion 6 as a trivial clean merge before origin/main
advanced again (PR #57) and produced a real feature-interaction
conflict. This document reflects the actual final reconciled state.
quad341 added a commit that referenced this pull request Jul 25, 2026
…eshness checks (#54)

* test(cairn): RED — cover git-invocation-failure vs confirmed-negative (crn-fdjc.1.1)

Add Incomplete status constant and new tests at the stable Check/Sweep
entry points asserting the FR-5 failure-class distinction from
crn-fdjc.1's design: a genuine git invocation failure (PATH unreachable,
canceled context) must classify as Incomplete, never collapse into the
same Unknown verdict as a confirmed negative (missing repo, no commits
yet, unmatched glob, staged-but-uncommitted path).

New tests fail against the current implementation, which still folds
every failure into Unknown/"?" — expected RED. Confirmed-negative
regression tests pass unmodified, confirming FR-2 (today's behavior for
those classes is preserved). ComputeFingerprint/objectHash/expand/
untrackedPaths keep their current signatures for now; direct tests of
their error-returning form land in the GREEN commit since they can't
compile until the signatures change.

* cairn: propagate git-invocation-failure vs confirmed-negative (GREEN)

ComputeFingerprint/objectHash/expand now return an explicit error for a
genuine git invocation failure, distinct from a confirmed negative
(untracked path, no anchor, etc.) which still returns ("", nil). Check
surfaces invocation failures as a new Incomplete status; Sweep's
untrackedPaths enrichment leaves status/detail untouched when the
underlying check was itself incomplete, instead of misreporting it as
a confirmed Unknown. cmd/commands.go: freshnessCmd/verifyCmd now hard-error
on Incomplete/invocation failure; statusCmd adds an "!X " flag; getCmd
needed no change (already prints any status generically).

Implements crn-fdjc.1.1 per architect design in crn-fdjc.1.

* chore: release gate PASS for freshness-invocation-incomplete-status

* fix: reorder Check() so Incomplete wins over never-verified, fix budget gate

Rebasing crn-fdjc.1.1 (git-invocation-failure -> Incomplete propagation)
onto crn-0vqk.2's already-merged Check() reorder (never-verified
short-circuit before the git shell-out, for performance) produced a
textual, mechanically-resolvable conflict -- but the obvious resolution
(keep the never-verified check first) silently defeated crn-fdjc.1.1's
own pinning tests: a files anchor that has never been fingerprinted yet
still needs its git invocation attempted, so a genuine failure (broken
PATH, canceled context) surfaces as Incomplete instead of being masked
behind "never verified".

Reordered Check() to attempt ComputeFingerprint (surfacing Incomplete on
error) before the never-verified check, which now runs only after a
successful call -- preserving crn-0vqk.2's original fix against a false
Fresh/Stale null-comparison for a never-verified anchor, while letting
crn-fdjc.1.1's error propagation win first.

freshnessBudget.classify (prime.go) gated its shell-out cap on
`a.Fingerprint != ""`, which was only ever correct under the old
ordering where a never-verified anchor short-circuited for free. Now
that Check unconditionally shells out for any shape-verifiable files
anchor, that clause under-metered the exact cost FR-5's budget cap
exists to bound -- dropped it so every files anchor that will actually
invoke git counts against the cap, never-verified or not.

Also fixes a stale call site in prime_test.go: ComputeFingerprint's
pre-crn-fdjc.1.1 signature returned a single value; the two-return form
landed via this same rebase but prime_test.go (merged to main earlier,
unrelated to this branch) wasn't touched by the rebased commits, so the
break was invisible to git's textual merge.

* chore: release gate PASS for freshness-invocation-incomplete-status (post-rebase reconciliation)

Supersedes this branch's earlier gate evaluation (eef42ec), which
assessed criterion 6 as a trivial clean merge before origin/main
advanced again (PR #57) and produced a real feature-interaction
conflict. This document reflects the actual final reconciled state.
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