Skip to content

fix(distribute): keep a user's edit to a distributed rule file - #189

Merged
arcaven merged 3 commits into
mainfrom
build/sideshow/90-rules-receipt
Oct 8, 2026
Merged

arcaven merged 3 commits into
mainfrom
build/sideshow/90-rules-receipt

Conversation

@arcaven

@arcaven arcaven commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

A user's edit to a distributed rule file was silently discarded on the next init --scope project, reported as a routine updated (sideshow-managed), because rules: decided ownership by its marker alone. This makes rules: compare the file against the receipt sideshow already records for it, as files: does since #84, so an edit is kept and reported instead of dropped.

Behavior change: a rule file an earlier run wrote and nobody edited updates as before; an edited one is skipped with "modified since sideshow wrote it (user edit preserved)". A marker file with no receipt (a lost registry, say) is compared with what this run would write: equal records a receipt and reports "unchanged (receipt recorded)"; different is left as is with "has a sideshow marker but no receipt; left as is; delete it to take the pack's version" and nothing is recorded. There is no --force; deleting the file takes the pack's version. An unedited, current file is now skipped as "already current" instead of rewritten with identical bytes.

Closes #90

Receipts are read per type: PriorRuleChecksums returns rules entries and PriorChecksums still returns files entries, so a rule and a file at the same path never read each other's receipt. RecordResults also keeps the receipt of an action that asks for it (RecordReceipt), so the receipt of an edited rule survives a run that writes another rule; without that the list is replaced by what the run wrote.

Repro, before and after, with an isolated SIDESHOW_HOME and a one-rule pack: at main 2ce9fe46 the second run printed updated (sideshow-managed) and the appended line was gone. With this branch the second and third runs print modified since sideshow wrote it (user edit preserved) and the line stays.

Acceptance:

  • an edit after the first run: the second run skips and the bytes are kept, and so does the third (TestRules_EditAfterFirstRunIsPreserved)
  • a lagging receipt with current bytes (the receipt holds the old version's sha, the disk holds the new version's exact bytes, as after a run killed before the registry saved) reports "already current" and moves the receipt; an edit after it is preserved
  • an unedited file plus a pack version bump updates and the receipt moves to the new sha
  • no receipt, bytes equal: the receipt is recorded, and an edit after it is preserved
  • no receipt, bytes differ: skipped with the no-receipt message, nothing recorded, the same on rerun
  • a rule and a files: entry at the same path keep separate receipts
  • dry-run reports each of those as "would ..." and changes neither file nor receipt

Commits: (1) tests with a stub reader, red; (2) the fix and the CLI wiring; (3) the case order: the current-bytes case is tested before the edited case, whether or not a receipt is known.

Blast radius: distributeRule, RecordResults, one added reader, and one line in runInitProject. Two existing tests changed because they relied on the old rule: TestDistributeRule_UpdatesMarkedFile and the rule scene of TestDistributeRule_AtomicPins now give the file a receipt, since a marker file with no receipt and different bytes is now left alone.

Mutants, each compiles and fails a test:

  • the edited-file case removed: killed by TestRules_EditAfterFirstRunIsPreserved
  • PriorRuleChecksums reads files entries: killed by TestRules_ReceiptsAreKeyedByType
  • RecordResults ignores RecordReceipt: killed by TestRules_NoReceiptBytesEqualRecordsTheReceipt
  • no receipt, bytes differ, overwrites: killed by TestRules_NoReceiptBytesDifferIsLeftAsIs
  • no receipt, bytes equal, not recorded: killed by TestRules_NoReceiptBytesEqualRecordsTheReceipt
  • dry-run drops the "would" prefix: killed by TestRules_DryRunMirrorsEachCase
  • an edited rule does not carry its receipt: killed by TestRules_AnEditedRuleKeepsItsReceiptWhileAnotherRuleIsWritten; the single-rule tests alone did not catch it, because RecordResults keeps the old list when a run records nothing
  • the onDisk == wantSum case tested after the edited case, as in the first head: killed by TestRules_ALaggingReceiptWithCurrentBytesIsMovedNotReadAsAnEdit; it arrived with the reorder, so the mutant is the old code and the test is its red evidence
  • the current file is rewritten: killed by TestRules_CurrentFileIsLeftAloneAndKeepsItsReceipt

Opportunity, not touched: files: skips for a user edit do not carry their receipt either, so after a run that writes another file the edited one reads as "no record (user-authored)". It stays skipped, so nothing is lost, only the message changes.

@arcavenai arcavenai left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving. A user's edit to a distributed rule now survives every path I could construct, where main drops it on the next init --scope project. The 8736668 reorder also closes the one case where unedited current bytes read as an edit forever.

End to end, main against this head. I built both binaries and ran install and init --scope project with an isolated SIDESHOW_HOME and a two-rule pack. Each path:

Path main 8736668
Edit, then run twice edit lost on run 2 modified since sideshow wrote it (user edit preserved) on runs 2 and 3
Edit, then upgrade the pack edit lost kept; the other rule updates
Edit, remove the rule from the pack, re-add it a version later edit lost on re-add kept; the re-add reports the no-receipt message
Edit, with the second rule written in the same run edit lost kept on that run and the next, still reported as modified (receipt carried)
Edit, then --dry-run, then a real run edit lost dry run says would skip: modified ...; the real run keeps it
User removes the marker line left alone left alone (user-authored)

Upstream changes still write. An unedited rule updates on a pack version bump, then reads already current on the next run.

Registry written by main. A registry first written by the main binary works with this one. An edit made under main is kept, and an unedited file reads already current, because main already recorded rule checksums on write.

Lagging receipt (9, 10, 11). I wrote the 1.0.1 bytes over a 1.0.0 receipt, then installed 1.0.1, and read the registry after each run:

  • --dry-run reports would skip: already current and leaves the receipt at the 1.0.0 sha.
  • The real run reports already current and moves the receipt to the sha of this run's bytes. The next run is unchanged.
  • A later edit is skipped as modified on two runs, the receipt stays, and the edit stays.

At 7645298 the same sequence reported modified forever. On the reorder: the write path is reached only with a known receipt that equals the bytes on disk, while those bytes differ from this run's. An edit with no receipt, or one that differs from both the receipt and this run's bytes, cannot reach it.

Red, and mutants. At cd0f8cb, with the stub reader, every new TestRules_ test fails for the stated reason: updated (sideshow-managed) where a skip is wanted. Head passes the full suite and two runs of -count=20.

The eight listed mutants and the old-order mutant each fail a test. So do these, which I added:

  • the lagging case keeping the old receipt;
  • the lagging case not recording;
  • PriorChecksums reading rules entries.

The last is the reverse cross-contamination, killed by TestRules_ReceiptsAreKeyedByType and TestPriorChecksums_RoundTripsWithRecordResults.

The two changed tests. TestDistributeRule_UpdatesMarkedFile and the rule scene of TestDistributeRule_AtomicPins keep every assertion they had; only the setup adds a receipt for the seeded bytes. The guarantee they no longer exercise, that a marked file with no receipt is overwritten, is the behavior the ruling removes. It is now pinned the other way by TestRules_NoReceiptBytesDifferIsLeftAsIs.

CI on 8736668 passes.

Non-blocking:

  • The lagging already current case sets RecordReceipt = true even under --dry-run, where the no-receipt case uses !opts.DryRun. Today the only RecordResults caller skips dry runs (cmd/sideshow/main.go:425), so nothing records. Matching the other case would keep that true for a future caller.
  • As the body notes, a files: skip for a user edit still drops its receipt when another file is written in the same run. Nothing is lost there, only the message.

Seat: reviewer / reviewer

@arcaven
arcaven marked this pull request as ready for review October 8, 2026 11:02
@arcaven
arcaven merged commit 6fdd2c8 into main Oct 8, 2026
10 checks passed
@arcaven
arcaven deleted the build/sideshow/90-rules-receipt branch October 8, 2026 11:16
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.

rules: distribution silently discards user edits to sideshow-managed files

2 participants