feat(ledger): retention and compaction learned from the ledger's own history - #155
Open
CodeWithJuber wants to merge 4 commits into
Open
CodeWithJuber wants to merge 4 commits into
CodeWithJuber wants to merge 4 commits into
Conversation
Pruning archived a tombstoned or dormant claim only after a fixed 2 x 45-day window, clusters() (tau 0.7) was never called, and nothing recorded which claims were ever served — so the per-session summaries the Stop hook mints, which nothing ever contradicts, grew without bound. src/ledger_retention.js is pure and derives every cut-off from the ledger it is given: - a tombstoned or dormant claim is archived at once: retrieve() never serves it, so its chance of being served is zero by construction; - a live claim is archived once idle longer than the LONGEST gap after which any claim here was active again, and only once the usage log spans longer than that gap (before then "not used" only means "not recorded"); - near-duplicates of one kind: each claim's nearest-neighbour MinHash similarity is modelled as one Gaussian or two (Otsu split), BIC picks the model, and only a two-component fit yields a boundary. A member is dropped only against the survivor's own pair, so a chain A-B-C never archives a claim that is no duplicate of the survivor. A fitted cut-off (F1 over a replay) was tried first and rejected: with a 3-day claim in the ledger it learned "idle > 2 days is dead" and archived a 10-day claim on the day it fell due. An archived claim is no longer served and so cannot prove itself useful again, which makes that mistake self-confirming — hence the longest comeback, not a typical one. Everything is reversible: the claim bytes move to attic/, its logs stay, new evidence un-archives it, and show/blame now read the attic. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Retention can only learn from use if use is recorded; retrieve() is pure and
every caller discarded the ids. Each place that SERVES claims now appends one
line to .forge/ledger/.usage.jsonl: the session-start lesson block, the
pre-edit lesson advisory, the deja-vu advisory (only when a hit is surfaced),
`forge ledger query` and the MCP forge_ledger_query.
The log is machine-local (gitignored) and outside ledgerSignature, so
appending never invalidates the snapshot cache. Writes go through appendLine,
which terminates a line a killed process left torn, and the .gitignore entry
is appended rather than rewritten so two processes cannot drop each other's.
pruneLedger (Stop hook, merge, import) now applies the learned plan without
the pairwise duplicate pass; `forge ledger compact [--dry-run] [--json]` adds
it and prints every learned number. getClaimByPrefix takes {attic:true},
used only by the read-only show/blame.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rules GUIDE gains the command with real --dry-run output and the three archive rules; ARCHITECTURE lists src/ledger_retention.js; the README command table and the repo map are re-rendered; CHANGELOG records the behaviour change, including that the bound is the ledger's longest comeback, so one long-silent claim that came back raises it for every claim. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CI failed the impact-label ground-truth check on every job: the new module imports and calls claimText, so `git grep claimText` names it, and the bench's labels are asserted to be exactly what grep finds. It passed locally only because the file was still UNTRACKED when the suite ran — git grep searches tracked files. Run the suite after staging when a change adds files. The label is a real dependency (src/ledger_retention.js:29 imports it, :184 calls it), so it belongs in `expected`, not in MENTION_ONLY. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ledger retention and compaction, learned from the ledger's own history. Every cut-off comes from the data; there is no fixed window and no tuned threshold.
What it replaces: pruning archived a tombstoned or dormant claim only after a fixed 2 × 45-day window, the near-duplicate grouper (τ = 0.7) was never called, and nothing recorded which claims were ever served. The per-session summary claims that nothing ever contradicts therefore grew without bound.
The three archive rules
retrieve()never serves it, so its chance of being served is zero by construction, not by a threshold.A fitted cut-off was tried first and rejected. Fitting the cut-off by F1 over a replay of history learned "idle more than 2 days is dead" when a 3-day claim was present, and would have archived a 10-day claim on the day it fell due. An archived claim is no longer served and so cannot prove itself useful again, which makes that mistake self-confirming. Hence the longest comeback rather than a typical one.
test/ledger_retention.test.jspins both properties.Everything is reversible: the claim bytes move to
attic/, its logs stay in place, new evidence un-archives it, andshow/blamenow read the attic.The usage log
Retention can only learn from use if use is recorded, and
retrieve()is pure: every caller discarded the ids. Each place that serves claims now appends one line to.forge/ledger/.usage.jsonl:forge ledger query;forge_ledger_query.The log is machine-local (gitignored) and outside
ledgerSignature, so appending never invalidates the snapshot cache. Writes go throughappendLine, which terminates a line a killed process left torn, and the.gitignoreentry is appended rather than rewritten so two processes cannot drop each other's.Command
forge ledger compact [--dry-run] [--json]prints every learned number and what it would archive. Real output:The Stop hook applies the first two rules; the pairwise duplicate pass runs only in this command.
Verification
npm testnpm run lintnpm run typecheckforge docs checkTests that prove the rules adapt rather than hide a constant:
Independent review
An independent reviewer (fresh context, diff + spec + test results only) returned allow and raised six findings. All are addressed in this branch:
test/ledger_usage.test.jsdrives the real entrypoints.appendLine.deja.jsstill described the 2·T window → rewritten..gitignorewriter could race → append-only now.🤖 Generated with Claude Code