Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 94 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,19 +444,30 @@ to touch `CHANGELOG.md`.

### When to release

Each release is a user-visible Windows `.exe` artifact downloaded from
GitHub Releases. There is no auto-update channel and no nightly feed
— releases *are* the distribution. Tag deliberately, not on a fixed
cadence and not per commit.

**Bump rules (pre-1.0 SemVer):**

- User-facing feature added → **minor** bump (e.g., `v0.1.0` →
`v0.2.0`).
- Bug-fix-only batch → **patch** bump (e.g., `v0.1.0` → `v0.1.1`).
- Pre-1.0 breaking changes ride in minor bumps (standard SemVer
carve-out for `0.y.z`). Once the project hits `1.0.0`, breaking
changes require a major bump.
Each release is a user-visible Windows artifact downloaded from GitHub
Releases — and, for installs that came through the Velopack `Setup.exe`,
the payload that in-field copies upgrade *themselves* to. There is no
nightly feed; releases *are* the distribution. Tag deliberately, not on
a fixed cadence and not per commit.

**Bump rules (SemVer):**

- User-facing feature added → **minor** bump (e.g., `v1.9.0` →
`v1.10.0`).
- Bug-fix-only batch → **patch** bump (e.g., `v1.9.0` → `v1.9.1`).
- Breaking change → **major** bump (e.g., `v1.9.0` → `v2.0.0`). See
"What counts as 'breaking'" below.

The project is past `1.0.0`, so the pre-1.0 carve-out that let
breaking changes ride in minor bumps no longer applies.

**Major bumps are reserved for compatibility breaks.** Size is not a
trigger. A large feature, a rewritten pane, or a redesigned UI is
still a **minor** bump so long as it doesn't disturb an existing
install. Say "this one is big" in the `CHANGELOG.md` section and the
release title, not in the version number — a major that only means
"big" teaches users that majors are safe to take, which is precisely
backwards on the one occasion the signal matters.

**Skip releases for** doc-only commits, build hygiene, test-only
changes, and pure refactors that don't change shipped behavior. These
Expand All @@ -479,6 +490,76 @@ to tag. Recommending a release in conversation is fine and
encouraged when a meaningful delta has accumulated; pushing the tag
without an explicit command is not.

### What counts as "breaking"

DiffViewer publishes no library and has no external API consumers, so
the usual "a public signature changed" test says nothing useful here.
Use this instead:

> A breaking change is an upgrade that can disturb an existing
> install.

Six surfaces carry that risk. A change that violates any of them is
breaking no matter how small the diff is:

1. **The CLI argv contract.** Three launch forms, all documented in
`README.md` and dispatched by `Services/CommandLineParser.cs`: the
positional form (`DiffViewer.exe <repo> <base> <compare>`), the flag
form (`--repo` / `--left` / `--right` / `--file`), and a lone GitHub
pull-request URL, which `ParseLaunch` routes to the PR resolver
before positional parsing ever runs. Users wire these into shell
aliases, `git` aliases, and editor integrations that live outside
this repo, so changing what a positional slot means, dropping a
flag, or narrowing which URLs parse breaks setups we can neither see
nor migrate.
2. **On-disk state formats.** `settings.json` and `recents.json` under
`%APPDATA%\DiffViewer`. Both are versioned contracts, and they use
deliberately *opposite* strategies — a change here must not silently
break either:
- `settings.json` migrates forward and refuses to go backward.
`SettingsMigrations.MigrateUpTo` chains registered v(N) → v(N+1)
steps and throws if one is missing. When the file's
`schemaVersion` is newer than the binary, `SettingsService`
declines to read it, moves it aside to
`settings.json.bak.<unix-time>`, and starts from defaults.
- `recents.json` is forgiving instead. `RecentsJsonSerializer`
reads the stored version and discards it, keeps every row that
parses, ignores unknown sibling fields, and re-stamps the file at
`RecentsDoc.CurrentVersion` on the next write — so version drift
heals itself.

Break either and a user loses their setup on upgrade, or has it
quietly reset on downgrade.
3. **Install footprint.** `App.xaml.cs` wires `UserPathRegistrar` into
Velopack's after-install / after-update / before-uninstall
callbacks, putting `AppContext.BaseDirectory` — the stable Velopack
`current` directory — onto the per-user `PATH` through
`WindowsUserPathStore`, so `diffviewer` resolves from any new
terminal. Two distinct breaks live here: renaming the executable
leaves the `PATH` entry valid but stops the old command name from
resolving, while changing which directory gets registered strands a
stale entry pointing at nothing. These hooks only fire for the
Velopack-installed copy — portable and dev launches never register.
4. **Keyboard shortcuts.** `Models/KeyboardShortcutCatalog.cs`. Adding
a binding is a feature; repointing an existing binding at a
different action is a silent behavioural break — muscle memory
starts doing something the user never asked for, with no error to
read.
5. **The update channel.** An install finds its upgrades through the
identity `VelopackUpdateService.TryCreateForInstalled` resolves
against — a `GithubSource` pointed at this repo's Releases — paired
with the `vpk pack --packId DiffViewer` that stamps `release.yml`'s
output. Change either side and in-field installs stop matching the
feed. That is maximally breaking: it removes the very mechanism by
which a subsequent fix would have reached them.
6. **The platform floor.** `README.md` promises Windows 10 or later on
**x64**, and the Release config pins `RuntimeIdentifier win-x64`
with `SelfContained` on top of `net8.0-windows`. Raising the OS
floor or changing the architecture strands machines that run the app
today. This one compounds with surface 5: because updates install
themselves, an in-field copy can be upgraded into a build it can no
longer run without its owner ever choosing to.

## 13. Origin

DiffViewer was extracted from the [geevensingh/DevTools][devtools]
Expand Down
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
All notable changes to DiffViewer are documented in this file.

The format is based on [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
(with the pre-1.0 carve-out: breaking changes ride in minor bumps until v1.0.0).
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
Major bumps are reserved for changes that can disturb an existing install;
see "What counts as 'breaking'" in `AGENTS.md` §12 for the specifics.

`.github/workflows/release.yml` reads the section matching the pushed tag
(e.g. `## [0.2.0]` for `v0.2.0`) verbatim and uses it as the GitHub Release
Expand Down