Skip to content

feat(L171): journal commits made where no vault is mounted - #77

Merged
chess-seventh merged 7 commits into
masterfrom
feat/L171
Aug 25, 2026
Merged

chess-seventh merged 7 commits into
masterfrom
feat/L171

Conversation

@chess-seventh

Copy link
Copy Markdown
Owner

Lane L171. D-98 ruled two paths to the commit journal; the hook was the first
and this is the second.

Why

The post-commit hook can only write the journal on a machine that mounts the
vault. Everywhere else the row is lost — and once the config was copied onto
those machines it started being lost silently, which is worse: a journal that
quietly stops looks exactly like a quiet week.

What

--reconcile <PATH> turns the same binary into a backstop pass. It reads the
history each named checkout's HEAD can reach and appends whatever row the day
note is missing. No broker, no network — so it works on the day the wire is
down, and on the day the wire does not exist yet.

rusty-commit-saver --reconcile ~/src/one --reconcile ~/src/two
rusty-commit-saver --reconcile ~/src/one --since 2026-08-01

What it guarantees

  • Append only. A row already in a note is never rewritten, reordered or
    reformatted.
  • The commit's own date, never today's — so a first backfill spreads across
    the months it actually happened in.
  • One row per commit. Identity is the commit hash in the last column, so a
    row the hook already wrote is recognised. A second pass appends nothing and
    leaves every note byte-identical.
  • The [exclude] list applies here too — it previously bound only the hook
    path.
  • A repository it cannot open is reported and skipped, so one broken clone
    does not stop the others catching up.

What it does not do

It sees only what HEAD reaches. A commit on an unmerged branch is not
journalled, and a squash-merge makes it unreachable for good. Widening the walk
to every local branch changes what belongs in the journal rather than fixing a
defect, so it is left as a decision rather than taken here. Documented in the
README and in the module docs.

Scope

No broker or wire code — that half was split to its own lanes. Cargo.toml and
Cargo.lock are untouched; clap was already a dependency.

The first two commits are pure refactors with no behaviour change, verified line
by line: they take the commit and the folder as arguments so a row can be built
for a commit that is not HEAD, from a directory that is not the process's.

Verification

Three independent blind review rounds. Round 1 green with non-blocking findings;
round 2 red on documentation and fixed; round 3 green. The gate — cargo test
(171), cargo clippy -W pedantic, cargo build --release, cargo fmt --check,
nix fmt -- --ci — is clean, and the lane adds no clippy warning of its own.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XMq7vQufRBhJqD2yBJoR54

The reconciler walks history and describes repositories it only points
at, so the two places that assume "the commit is HEAD" and "the folder
is where I am" have to take those as arguments instead.

- extract CommitSaver::from_commit, so a row can be built for any
  commit rather than only the one HEAD points at
- take the branch as an argument: a commit made months ago has no
  branch of its own to report, so the caller has to say
- split append_row_to_diary out of append_entry_to_diary, so the FOLDER
  column can come from the repository rather than from the process
- no behaviour change: the hook path still reads HEAD and still reports
  the current directory, and the existing suite covers it unchanged

Co-Authored-By: Vulcan <chess7th@pm.me>
The hook resolves a note path and makes sure the file exists, inline in
run_commit_saver. The reconciler needs exactly those two steps, so they
become named functions rather than a second copy.

- add CommitSaver::diary_path_for, which resolves the absolute note path
  from the COMMIT'S timestamp rather than the clock
- add ensure_diary_file, which creates the note from the template only
  when it is missing and reports whether it did
- run_commit_saver calls both instead of open-coding them
- no behaviour change: same path, same template, same append, and the
  existing suite covers it unchanged

Co-Authored-By: Vulcan <chess7th@pm.me>
D-98 ruled two paths to the journal and this is the second one: a pass
that reads a repository's history and appends whatever row the day note
is missing. It needs no broker and no network, so it works on the day
the wire is down and on the day the wire does not exist yet.

- add reconcile_repo, walking HEAD's history oldest commit first
- identity is the row's COMMIT HASH column, so a row the hook already
  wrote is recognised and never written twice - the dedup rule D-98
  named as the thing that must be exactly right
- read the hash from the LAST cell, so an escaped pipe in the commit
  message cannot be mistaken for a column boundary
- only ever append: existing bytes in a note are never rewritten,
  reordered or reformatted
- a row lands in the note for the COMMIT'S own date, never today's,
  so a backfill cannot collapse a month of history into one note
- honour the exclude list, which until now bound only the hook path
- add reconcile_all, which reports a repository it cannot open and
  carries on to the next rather than abandoning the run
- read each day note once per pass, however many rows it gains

Twelve tests, one per claim above, including the two-pass proof that
the second run appends nothing and leaves the note byte-identical.

Co-Authored-By: Vulcan <chess7th@pm.me>
The reconciler existed and nothing could reach it. This is the entry
point: a flag that turns the same binary into the backstop pass, so the
vault box can run it on a schedule.

- add --reconcile <PATH>, repeatable, naming the repositories to walk
- add --since <YYYY-MM-DD>, so a nightly pass need not re-read history
  a first backfill already covered
- the flag opts IN: with no --reconcile the binary behaves exactly as
  every installed post-commit hook already expects
- a --since the tool cannot read stops the run and names both the shape
  it wanted and the value it got, rather than silently backfilling years
  from a typo in a timer unit
- one stdout line per repository, because an unattended pass that
  appended nothing must look different from one that never ran

Proven end to end through the real binary: a commit made in a repository
the run is not standing in reaches the vault, and the repository the run
IS standing in is left alone.

Co-Authored-By: Vulcan <chess7th@pm.me>
- describe --reconcile: what it is for, how to run it, what its output
  means on a scheduled run
- state its four guarantees where a user will look for them: append
  only, the commit's own date, hash identity, and the exclude list
- add a build-and-test section, and record that `devenv shell` needs a
  checkout at $HOME/devenv_shared that the repository does not carry,
  while `nix develop` needs nothing outside it
- warn that the flake shell's rustfmt defaults to a different style
  edition, so formatting there without --style-edition=2024 reflows
  files it should not touch

Co-Authored-By: Vulcan <chess7th@pm.me>
The gate returned green with seven non-blocking findings. Four are
closed here; the fifth is documented rather than fixed, and two are
left as they are with the reason recorded.

- looks_like_object_id had a ceiling of 40. That reads as correct
  against SHA-1 and is a trap: a SHA-256 repository writes 64-character
  ids, none of them would match, and every pass would stop recognising
  its own rows and append duplicates for ever - the one outcome the
  dedup rule exists to prevent. The floor alone already excludes the
  header and the rule line, which is all the ceiling ever did. Test
  added for a 64-character id.
- the note-set lookup used expect() on a key it had just inserted. The
  entry API says the same thing without a panic path, and removes the
  second lookup.
- prepare_commit_entry_as_string was widened to pub(crate) for a caller
  that never materialised. Private again.
- run_commit_saver took its vault root by value and no longer consumes
  it, so it takes &Path. That was the last warning this lane added:
  clippy::pedantic is now down to the five that predate it.
- the docs said the pass reads "a repository's history" when it reads
  what HEAD can reach. That gap bites in exactly this fleet's workflow
  - a commit on an unmerged lane branch is not journalled, and a
  squash-merge makes it unreachable for good. Swept in all five places
  that said it: the module doc, reconcile_repo's doc, the crate doc,
  the README feature line and the README section.

NOT taken, and why: widening the walk to every local branch changes
what belongs in the journal rather than fixing a defect, so it is a
decision and not mine to make here. An unborn HEAD reported as an
unopenable repository, and --since ignored without --reconcile, are
both real and both cosmetic; they go to the follow-up rather than
growing this lane after its gate.

Co-Authored-By: Vulcan <chess7th@pm.me>
Verify-Round: 1
Verify-Class: docs claiming the pass sees more than HEAD reaches - swept in all five places
Round 2 was red, and correctly: the previous commit asserted the claim
had been swept in five places and left three standing, one of them in
src/. Closing a documentation-honesty finding with an overstatement
about its own scope is the same defect wearing the fix's clothes.

The three the round named:

- run_reconcile's doc still said "it reads history", the sixth site
  and the one the count missed
- the README's opening sentence still said "a repository's history".
  The last commit appended a corrective blockquote below it instead of
  editing it, so the section asserted the claim and contradicted itself
  four lines later
- the example comment said "every commit ever made in these
  repositories", three lines under the blockquote denying it

Two more the round did not name, found by sweeping for the idea rather
than for the sentence it quoted:

- --reconcile's own --help text said the binary "does not touch HEAD",
  which is the opposite of true: the reconcile path reads each named
  repository's HEAD. It meant "does not journal the repository it was
  started in", and now says that
- a test comment carried the same "every commit ever made"

And one claim of a different kind, corrected because the round measured
it: looks_like_object_id's comment said a SHA-256 repository would make
the pass append duplicates for ever. The git2 this crate builds against
refuses to open such a repository at all, so that harm is not reachable
today. Removing the ceiling is still right - it protected nothing the
floor does not - but the comment now says what was measured rather than
what sounded convincing.

Co-Authored-By: Vulcan <chess7th@pm.me>
Verify-Round: 2
Verify-Class: docs and help text claiming coverage wider than HEAD-reachable - swept by idea, not by quoted string
@codecov

codecov Bot commented Aug 25, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.00748% with 12 lines in your changes missing coverage. Please review.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
src/reconcile.rs 96.62% 11 Missing ⚠️
src/vim_commit.rs 98.07% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@chess-seventh
chess-seventh merged commit dc0edb4 into master Aug 25, 2026
7 checks passed
@chess-seventh
chess-seventh deleted the feat/L171 branch August 25, 2026 21:18
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