edamame 0.1.6

A Terminal Markdown editor that stays rendered while you edit
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Post-upgrade notice

> Part of the edamame contributor deep-dives. Index and project-wide conventions: [`AGENTS.md`](../../AGENTS.md). Sibling docs live in [`docs/dev/`](.).


- **A second, unrelated notion of "new".** [`update_check`](update-check.md) asks whether a newer release exists on GitHub; `app::post_upgrade` asks whether *this build* is newer than the one that last ran, and answers from the `CHANGELOG.md` compiled into the binary. No network, no consent setting, no throttle — so `ReleaseStatus` gains no variant, since every state it has names the outcome of a fetch. The two share `parse::sanitize_notes` and `ui::update_check`'s renderer, so a changelog entry and a release body reach the screen with one vocabulary and one set of caps.
- **Synchronous, not park-and-tick.** `update_notice` parks its finding only because a network result lands long after `App::new` has fixed the startup modal ordering. Config, changelog and `CARGO_PKG_VERSION` are all in hand in the constructor, so this modal joins that ordering directly — one slot below the welcome — with no `App` field, no per-frame poll and no "wait for the welcome" carve-out.
- **`last_version_seen` lives on `State`**, in `state.toml` alongside the update-check stamps; `stamp_last_version_seen` writes it through `save_state_bookkeeping` → `State::save`. See [machine-state.md](machine-state.md).
- **The decision belongs to `App::new`; the write does not.** `test_utils::make_app` builds an `App` through `App::new` and most callers hold no `config_isolation()` guard, so a `State::save` there would rewrite *the developer's own* `state.toml` on every `cargo test` run. `App::stamp_last_version_seen` therefore runs from `App::run`. Deciding is exempt because `config_writes_allowed()` is an atomic load, not I/O. A new startup modal recording that it fired owes the same split.
- **An empty `last_version_seen` means "no version recorded", and `show_welcome` is the tell.** A fresh install and an upgrade from a build predating the field both have the empty string; guessing wrong either greets a brand-new user with notes for a version they never ran, or eats the first notice for every existing user. Only a returning user could have turned `show_welcome` off. Once a real version is recorded every later upgrade takes the non-empty branch, so there is no migration path to delete later.
- **The stamp is unconditional on whether anything was shown, and `--no-config` refuses the notice outright.** A release with no matching changelog section is silent but still records the version — otherwise "nothing to say" would be re-evaluated forever. Under `--no-config` the stamp cannot persist, so the notice is gated at the decision (`config_writes_allowed()`) rather than left to `State::save`'s own refusal, or a one-time notice becomes an every-launch one.
- **One body, two entry points, differing only in the opening line.** `PostUpgradeOccasion::Upgrade` opens on `Updated to vX.` with no version row; `OnDemand` — the About page's `[ Release notes ]` button — opens on an `edamame version: vX` row with no headline, because a verdict there would claim an upgrade that never happened. `PostUpgradeModal` carries the occasion, since `render` is where the two paths meet. The on-demand path *always* answers, rendering `PostUpgradeReport::NotFound` for a version with no section, and reads and writes no bookkeeping — looking is not being notified. `Some(vec![])` (an empty section) and `None` (no section) must stay distinct, or that modal reports an absence for a release that was merely quiet.
- **The section terminator includes a link-reference definition, not just the next `## `.** The last section in a Keep a Changelog file is followed only by the `[0.1.0]: https://…` block, so without that arm (`is_reference_definition`) the oldest release's notes end in raw URLs. A `### Added` subheading is deliberately not a terminator (third character `#`, not a space). Versions match on the bracketed text exactly (`heading_version`), so `0.1.2` cannot claim `## [0.1.20]`.
- **A test building an `App` with `show_welcome = false` must record a version too.** An empty `last_version_seen` is exactly the "upgraded from a pre-field build" case, so the notice lands on the stack and breaks every assertion about what is on top. `test_utils::app_with_welcome` and `nav`'s builder both pass a `State { last_version_seen: INSTALLED_VERSION, .. }` to `App::new`.