fix(index): re-derive task IDs when the derivation rules change - #56
Merged
Conversation
The index now records the ID-derivation version it was built under and re-derives every file on a mismatch, since a rules change moves every unpinned ID while leaving content hashes identical. check-index compares stored IDs against freshly derived ones, lint names the cause, and the new lash migrate-ids rewrites references left dangling. Fixes #54
Rejoining split lines with \n rewrote every line ending in a CRLF file as a side effect of changing one reference. Discovery now goes through FileWalker so the rewritten set matches what the project indexes.
--force wipes the database, which is the only place a task's previous ID survives. Pending renames are now read out (and undetected drift drawn out) before the wipe, and restored after.
…drift # Conflicts: # CHANGELOG.md # devlog.md
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.
Fixes #54.
A derived value cached behind a hash of its input, where the derivation is the
other input and nothing was watching it.
A task's ID comes from its title and is not written to the Markdown unless the
author pins it with
@id:. 0.3.0 changed the derivation — underscores becameseparators instead of vanishing — so every unpinned ID moved while every
content hash stayed byte-identical. Incremental indexing keys off those hashes,
so a file nobody had edited since the upgrade was never re-parsed and kept
serving IDs derived under rules no longer in force.
The failure was silent in four directions at once.
lash showread the storedrecord and printed the old ID.
lash lintderived a new one and rejected thereference.
check-indexcompared hashes, found them equal, and said in sync.lash indexsaid "Unchanged: 1". Assuming lint was wrong is the onlyconclusion the output supports.
The index records what derived it
ID_DERIVATION_VERSIONnames the current rules and is stamped into themetadatatable. On mismatch — or absence, the same thing — the hash diff isignored and every file is re-parsed. An upgrade repairs itself on the next
lash index.The stamp is written only after a run that can vouch for the whole project:
unscoped, no parse errors. A scoped run re-derives part of it and a failed
parse leaves that file's old rows in place; stamping after either claims a
freshness the index does not have, and the next run skips the repair.
The repair captures what it moved
Correcting the stored IDs is half of it. A
@depends-onwritten against an oldID is text in a file and stops resolving the moment the stored IDs move — all
of them together, which is why
--forcemade the rebuild look like the causeof the damage rather than the fix.
The re-derive is the only moment both spellings exist, so the mapping is taken
there, into a new
id_migrationstable (schema v9).Matching old rows to new tasks without using the ID — the thing under suspicion
— is by title plus structural position (
depth,order_index), none of whichthe rules touch. That is exact only because the file's hash is unchanged, which
is checked first. Any key claimed twice on either side is dropped rather than
guessed: an ambiguous pairing becomes a rename written into someone's Markdown,
whereas a missed rename surfaces as an unresolved reference the author fixes.
check-index re-derives instead of trusting hashes
It now parses each file whose hash already matches and compares the IDs — the
expensive path for the otherwise-cheap case, and the only one that catches
this, because an unchanged file is precisely the file whose IDs never get
re-derived. Reports
IssueKind::StaleTaskIds; opt out withVerifierConfig::with_task_id_check(false).lint names the cause
An
E_LINK_NOT_FOUNDwhose target the index still recognises, or that matchesa pending rename, is not a typo. The note goes in the diagnostic's
helpforJSON and
-v, and once more after the summary — help is hidden at normalverbosity, and this is the difference between "lint is wrong" and "here is what
happened".
lash migrate-ids
Consumes the recorded renames. Previews by default, writes with
--write,--forgetdiscards them for a project repairing by hand. Touches wholereferences on
@depends-on:lines and nothing else: prose mentioning an old IDis someone's notes, and the unqualified
old-idform is left alone because abare token can name a file as readily as a task. That gap is printed rather
than left implicit.
--forceno longer destroys the mapping--forcewipes the database, and the database is the only place a task'sprevious ID survives. It was the workaround the issue documented, and it used
to correct the stored IDs while leaving every reference dangling with nothing
left to explain them.
Pending renames are now read out before the wipe — and undetected drift drawn
out first, with one incremental pass — then restored afterwards. Best-effort
throughout: a database too broken to read is exactly why someone reached for
--force, so a failed salvage does not block the rebuild.Line endings
apply_rewritessplits lines with their terminators attached and puts themback untouched. Splitting on content and rejoining with
\nwould haverewritten every line ending in a CRLF file as a side effect of changing one
reference, and would have added a trailing newline to a file that had none.
End to end, from the reported state
Tests
id_derivation_drift_test.rscovering all foursurfaces and the full recovery, including that a genuine typo is not
dressed up as a derivation change and that an edited file is not guessed at
version stamp, scoped runs)
migrate-idsunit tests (reference matching, line endings, line-number agreement)
Full workspace suite passes (61 suites); clippy and fmt clean. The agent-prompt
snapshot is updated for the new subcommand.