Skip to content

Make versioning rules post-1.0 and define "breaking" - #25

Merged
geevensingh merged 4 commits into
masterfrom
geevensingh-versioning-policy
Sep 8, 2026
Merged

Make versioning rules post-1.0 and define "breaking"#25
geevensingh merged 4 commits into
masterfrom
geevensingh-versioning-policy

Conversation

@geevensingh

@geevensingh geevensingh commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Why

Two real defects in the repo's own versioning rules, surfaced by the question "is a large feature change enough to justify a major version bump".

The rules were factually stale. The project is at v1.9.0, but AGENTS.md §12 was headed **Bump rules (pre-1.0 SemVer):** and CHANGELOG.md's preamble claimed "breaking changes ride in minor bumps until v1.0.0". Both would actively mislead a future reader.

"Breaking" was never defined. For a library it self-evidently means a changed signature. DiffViewer is a WPF desktop app shipped as a single .exe with no published package and no external API consumers, so the rule gave no usable guidance — which is exactly the ambiguity that prompted the original question.

What changed

AGENTS.md §12:

  • Bump rules are now plain post-1.0 SemVer. Feature → minor, bug-fix batch → patch, breaking → major, with an explicit note that the pre-1.0 carve-out has expired.

  • New ### What counts as "breaking" subsection, built on a definition a client app can actually apply:

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

  • Enumerates the six compatibility surfaces that definition covers:

    # Surface Owner
    1 CLI argv contract (positional, flag, and PR-URL launch forms) Services/CommandLineParser.cs, README.md
    2 On-disk state (settings.json, recents.json) Services/SettingsMigrations.cs, Services/SettingsService.cs, Services/RecentsJsonSerializer.cs
    3 Install footprint (per-user PATH) App.xaml.cs, Services/UserPathRegistrar.cs, Services/WindowsUserPathStore.cs
    4 Keyboard shortcuts Models/KeyboardShortcutCatalog.cs
    5 Update channel Services/VelopackUpdateService.cs, vpk pack --packId in release.yml
    6 Platform floor (Windows 10+, x64) README.md Requirements, DiffViewer.csproj Release block
  • Settles a question the old rules were silent on: major is reserved exclusively for compatibility breaks. Size is not a trigger. A major that only means "this release is big" teaches users that majors are safe to take, which inverts the signal on the one occasion it matters. Significance goes in the release notes instead.

CHANGELOG.md: preamble no longer claims a pre-1.0 carve-out.

Follow-up commit: three surface claims corrected

The first commit described surfaces 2, 3, and 5 from nearby XML doc comments rather than from the implementations. Review caught it; f38566a re-verifies all of them against actual code.

Surface 2 was outright wrong. It claimed RecentsJsonSerializer loads an unknown future version as empty. Deserialize actually discards the version (_ = TryInt(obj, "version")), keeps every row that parses, and re-stamps at RecentsDoc.CurrentVersion on the next write — the comment directly above that line records that Phase 7 of the PR-review feature superseded the old "unknown version = empty" policy.

Re-verifying turned up something better than the original claim: settings.json and recents.json use opposite strategies, not a shared one. settings.json migrates forward through registered v(N) → v(N+1) steps and refuses to read a schemaVersion newer than the binary (moves the file aside, starts from defaults); recents.json is forgiving as described above. Both are now documented as the distinct contracts they are.

Surface 3 conflated two different failures. The PATH entry holds AppContext.BaseDirectory (the Velopack current directory), not the executable — so renaming the exe leaves the entry valid and only breaks the command name, while stranding the entry requires changing which directory gets registered. Now separated, with App.xaml.cs named as where the hooks are wired and a note that they only fire for the Velopack-installed copy.

Surface 5 named a file where it should have named an identity. What must stay stable is the pairing of the GithubSource that TryCreateForInstalled resolves against with the vpk pack --packId value stamped by release.yml.

The stale class-level XML doc on RecentsJsonSerializer still asserts the superseded rule and is what misled the original text. It is production code and is deliberately left untouched here — being raised separately.

Review rounds

Every Copilot review comment was valid; each was verified against source before acting.

28ad7da

  • Platform compatibility was missing. The list read as exhaustive but omitted the documented platform contract — README.md promises Windows 10+ on x64, and the Release block pins RuntimeIdentifier win-x64 / SelfContained on net8.0-windows. A platform drop plainly disturbs an existing install yet fell outside all five surfaces. Added as surface 6 rather than hedging the list as non-exhaustive: the section's value is that the call is mechanically decidable, and an "among others" escape hatch would reintroduce the ambiguity this PR removes. Surface 6 also names the compounding risk — because surface 5 installs updates on its own, a platform drop can push an in-field copy into a build it can no longer run.
  • File.Move, not a copy. Surface 2 said SettingsService "copies" a future-versioned file to settings.json.bak.<unix-time>; BackupAndUseDefaults calls File.Move (SettingsService.cs:151), so the original doesn't survive at its path. Reworded to "moves it aside to".

6eabd65

  • Surface 1 omitted the PR-URL launch form. It led with "Both" — a closed list of two — but DiffViewer accepts a third documented form: a lone GitHub pull-request URL. README.md:15-17 advertises DiffViewer.exe https://github.com/owner/repo/pull/123, and CommandLineParser.ParseLaunch (lines 37-40) matches it via PullRequestRef.TryParse and returns FromPullRequest before positional parsing runs. Same failure mode as the platform gap: an exhaustive list that quietly isn't would misclassify removing that form as non-breaking. Now names all three forms and covers narrowing which URLs parse.

Two corrections this change forced

  • The lead paragraph of "When to release" claimed "There is no auto-update channel" — false. VelopackUpdateService.cs exists, and release.yml packs a Setup.exe plus RELEASES metadata specifically for the updater to consume. Left as-is it would have contradicted compatibility surface Support 'git difftool' integration via CLI flags #5 two paragraphs below.
  • The new subsection is a sibling ### of "When to release" rather than a nested ####. Nesting it would have silently re-parented the existing skip-releases, group-commits, and agent-must-not-tag paragraphs under a heading they don't belong to.

Not changed

Bump semantics beyond post-1.0 + the "breaking" definition. Feature is still minor, bug-fix batch is still patch, the "skip releases for" list is intact, and a Copilot CLI session still must not push a release tag on its own judgment.

Notes for review


Session

  • AI-Local-Session: ada363aa-6c26-4bcf-b059-b369422de5a7
  • AI-Cloud-Session: 747f79eb-14cd-4937-9ae0-90a063986dd6

AGENTS.md §12 and CHANGELOG.md's preamble both still described the
project as pre-1.0, claiming breaking changes ride in minor bumps
until v1.0.0. The project has been past that since the v1.0.0 tag and
is now at v1.9.0, so both statements actively misled readers.

Separately, "breaking" was never defined. For a library it self-
evidently means a changed signature; DiffViewer ships a WPF desktop
app with no published package and no external API consumers, so the
rule as written gave no usable guidance. That ambiguity is what
prompted the question of whether a large feature justifies a major
bump.

Rewrite the bump rules as plain post-1.0 SemVer and add a "What counts
as 'breaking'" subsection built on a definition a client app can
actually apply: a breaking change is an upgrade that can disturb an
existing install. Enumerate the five compatibility surfaces that
definition covers — the CLI argv contract, the settings.json /
recents.json on-disk formats, the user-PATH install footprint, the
keyboard-shortcut bindings, and the Velopack update channel — each
cited to the type or file that owns it.

Settle the previously-unaddressed question of non-compatibility major
bumps: major is reserved exclusively for compatibility breaks. Size is
not a trigger. A major that only means "this release is big" teaches
users that majors are safe to take, which inverts the signal on the
one occasion it matters.

Two adjacent corrections this change forced:

- The lead paragraph claimed "there is no auto-update channel", which
  is false — VelopackUpdateService.cs exists, and release.yml packs a
  Setup.exe plus RELEASES metadata for the updater to consume. Left
  as-is it would have contradicted compatibility surface #5 two
  paragraphs below.
- The new subsection is a sibling ### of "When to release" rather than
  a nested ####, so the existing skip-releases, group-commits, and
  agent-must-not-tag paragraphs stay under the heading they belong to.

Bump semantics are otherwise unchanged: feature is still minor,
bug-fix batch is still patch, and a Copilot CLI session still must not
push a release tag on its own judgment. The CHANGELOG.md edit is
confined to the preamble to avoid conflicting with PR #24, which
touches [Unreleased].

Docs only; no .cs, .xaml, or .csproj files touched.

AI-Local-Session: ada363aa-6c26-4bcf-b059-b369422de5a7
AI-Cloud-Session: 747f79eb-14cd-4937-9ae0-90a063986dd6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@geevensingh
geevensingh requested a balanced review from Copilot September 7, 2026 19:24
Surfaces 2, 3, and 5 of the new "What counts as 'breaking'" section
described intent taken from nearby XML doc comments rather than what
the code actually does. Re-verified all five against implementation.

Surface 2 was outright wrong. It claimed RecentsJsonSerializer loads an
unknown future version as empty. Deserialize actually discards the
version outright (`_ = TryInt(obj, "version")`), keeps every row that
parses, and re-stamps at RecentsDoc.CurrentVersion on the next write —
the comment directly above that line records that Phase 7 of the
PR-review feature superseded the "unknown version = empty" policy. The
stale class-level XML doc on RecentsJsonSerializer still asserts the
old rule and is what misled the original text; it is production code
and is deliberately left alone here.

Re-verifying also turned up something better than the original claim:
settings.json and recents.json use opposite strategies, not a shared
one. settings.json migrates forward through registered v(N) -> v(N+1)
steps and refuses to read a schemaVersion newer than the binary,
backing the file up and starting from defaults. recents.json is
forgiving in exactly the way described above. Both are now documented
as the distinct contracts they are.

Surface 3 conflated two different failures. The PATH entry holds
AppContext.BaseDirectory (the Velopack `current` directory), not the
executable, so renaming the exe leaves the entry valid and only breaks
the command name; stranding the entry requires changing which
directory gets registered. Separate them, name App.xaml.cs as the
place the hooks are wired, and note the hooks only fire for the
Velopack-installed copy.

Surface 5 named a file where it should have named an identity. What
must stay stable is the pairing of the GithubSource that
TryCreateForInstalled resolves against with the `vpk pack --packId`
value stamped by release.yml.

Surfaces 1 and 4 re-verified as accurate: CommandLineParser really does
accept 0-3 positional args and reject any unrecognised leading-dash
flag in both grammars, and KeyboardShortcutCatalog really does hold the
KeyChord bindings mirroring MainWindow.xaml.

Docs only; no .cs, .xaml, or .csproj files touched.

AI-Local-Session: ada363aa-6c26-4bcf-b059-b369422de5a7
AI-Cloud-Session: 747f79eb-14cd-4937-9ae0-90a063986dd6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Changes recommended

The breaking-change list omits platform compatibility and contains an inaccurate description of settings backup behavior.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Updates release guidance for post-1.0 SemVer and defines application-specific compatibility boundaries.

Changes:

  • Replaces obsolete pre-1.0 versioning rules.
  • Defines five breaking-change surfaces.
  • Corrects the changelog’s SemVer statement.
File summaries
File Description
AGENTS.md Updates release and compatibility guidance.
CHANGELOG.md Aligns the preamble with post-1.0 versioning.
Review details
  • Files reviewed: 2/2 changed files
  • Comments generated: 2
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread AGENTS.md Outdated
Comment thread AGENTS.md Outdated
Both from Copilot review on PR #25; each verified against source
before acting.

The five-surface list was presented as exhaustive but omitted the
platform contract. 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 switching
architecture plainly "disturbs an existing install" yet fell outside
all five surfaces, so the rule as written would have misclassified it
as a minor bump.

Added as surface 6 rather than hedging the list as non-exhaustive.
The point of this section is that the major-bump call is mechanically
decidable; "among others" would put the judgment call straight back
in. The new entry also names the interaction the reviewer did not:
because surface 5 installs updates on its own, a platform drop can
push an in-field copy into a build it can no longer run without its
owner choosing to.

Separately, surface 2 said SettingsService "copies" a future-versioned
settings file to settings.json.bak.<unix-time>. BackupAndUseDefaults
calls File.Move (SettingsService.cs:151), so the original does not
survive at its path. Reworded to "moves it aside to", which is what a
reader needs in order to reason about the downgrade case.

Docs only; no .cs, .xaml, or .csproj files touched.

AI-Local-Session: ada363aa-6c26-4bcf-b059-b369422de5a7
AI-Cloud-Session: 747f79eb-14cd-4937-9ae0-90a063986dd6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The exhaustive CLI compatibility list omits the documented GitHub pull-request URL launch form.

Review details

Suppressed comments (1)

Previously missed (1) — in code that hasn't changed since the last review.

AGENTS.md:510

  • The CLI compatibility surface omits the documented single-argument GitHub pull-request URL launch (README.md:15-16, CommandLineParser.cs:35-40). Because this list is intended to be exhaustive, removing or changing that form could be incorrectly treated as non-breaking. Include it alongside the positional and flag forms.
  • Files reviewed: 2/2 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

From Copilot review on PR #25, verified against source before acting.

Surface 1 enumerated only the positional and flag forms, and led with
"Both" — a closed list of two. DiffViewer accepts a third documented
form: a lone GitHub pull-request URL. README.md:15-17 advertises
`DiffViewer.exe https://github.com/owner/repo/pull/123`, and
CommandLineParser.ParseLaunch (lines 37-40) matches it via
PullRequestRef.TryParse and returns
CommandLineLaunchPlan.FromPullRequest before positional parsing runs.

That omission matters for the same reason the missing platform floor
did: the list is deliberately exhaustive so the major-bump call stays
mechanically decidable. Leaving a documented launch form out means
removing or narrowing it would be misclassified as non-breaking, when
it would in fact break any alias, script, or handler a user has wired
to open a PR in DiffViewer.

Rewrote the entry to name all three forms and to cover narrowing which
URLs parse, not just positional-slot and flag changes.

Docs only; no .cs, .xaml, or .csproj files touched.

AI-Local-Session: ada363aa-6c26-4bcf-b059-b369422de5a7
AI-Cloud-Session: 747f79eb-14cd-4937-9ae0-90a063986dd6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@geevensingh

Copy link
Copy Markdown
Owner Author

The CLI compatibility surface omits the documented single-argument GitHub pull-request URL launch (README.md:15-16, CommandLineParser.cs:35-40). Because this list is intended to be exhaustive, removing or changing that form could be incorrectly treated as non-breaking.

Valid catch, addressed in 6eabd65. Verified against source before acting: README.md:15-17 advertises DiffViewer.exe https://github.com/owner/repo/pull/123, and CommandLineParser.ParseLaunch (lines 37-40) matches it via PullRequestRef.TryParse, returning CommandLineLaunchPlan.FromPullRequest before positional parsing ever runs.

Surface 1 had led with "Both" — a closed list of two forms. It now names all three and covers narrowing which URLs parse, not just positional-slot and flag changes.

Noting the disposition here because this arrived as a suppressed comment in a review summary rather than an inline thread, so there was no thread to resolve and nothing in the UI would otherwise show it as handled.

@geevensingh
geevensingh merged commit f6f3a2a into master Sep 8, 2026
1 check passed
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.

2 participants