|
| 1 | +# Cycle 1 (wp1, Bug A #871) — P-phase re-verification record |
| 2 | + |
| 3 | +## Stale check vs pre-written 010 doc (2026-08-02, worktree codex/wt1-update-path @ dev tip) |
| 4 | + |
| 5 | +- `src/update/notify.ts` `isNewer` confirmed unfixed on dev: latest channel does |
| 6 | + `const c = parseStable(current)` → `null` for `2.8.2-preview.20260731` → returns |
| 7 | + `false` → GUI reports `already_latest`. Bug present, doc not stale. |
| 8 | +- Consumers confirmed: `src/update/badge.ts:69` (`updateAvailable: isNewer(cache.latest_version, current, channel)`), |
| 9 | + `src/update/job.ts:317` `checkForUpdate` (GUI one-click path), `src/update/job.ts:1377`. |
| 10 | +- Existing tests: `tests/update-notify.test.ts` has latest/preview channel tables; |
| 11 | + `tests/update-job.test.ts` has `checkForUpdate("latest", ...)` fixtures with |
| 12 | + injectable `currentVersion`/`detectInstall`/`latestVersion` deps — the PR's test |
| 13 | + slots fit without new fixtures. |
| 14 | + |
| 15 | +## Semantics decision (aligned with PR #871 diff) |
| 16 | + |
| 17 | +- Current side, latest channel: `parseStable(current) ?? parsePreview(current)?.slice(0, 3)` — |
| 18 | + an installed preview compares by its `major.minor.patch` core. |
| 19 | +- Target side stays strict: `parseStable(latest)` only — a preview registry target is |
| 20 | + never accepted on the latest channel (parity with codex-rs, existing doc comment). |
| 21 | +- Same-base case: `2.9.1` vs installed `2.9.1-preview.N` → NOT newer. Although semver |
| 22 | + precedence says stable > its prerelease, the product rule matches the preview |
| 23 | + channel's existing O3 decision (same base = no nag); the release train promotes a |
| 24 | + preview to the same-base stable, so the update is content-lateral. This mirrors |
| 25 | + PR #871's test expectations exactly; deviating would fork behavior from the |
| 26 | + contributor PR under review. |
| 27 | + |
| 28 | +## External verification (sol-medium lane, cxc-search) |
| 29 | + |
| 30 | +| Claim | Result | |
| 31 | +|-------|--------| |
| 32 | +| semver: prerelease < associated release (2.9.1-preview.N < 2.9.1) | verified — SemVer §9/§11.3 (semver.org) | |
| 33 | +| semver: numeric prerelease identifiers compare numerically | verified — SemVer §11.4.1 | |
| 34 | +| npm: bare install resolves the `latest` dist-tag; preview train belongs on its own tag | verified — npm-dist-tag docs (Description/Purpose/Caveats) | |
| 35 | +| npm: `2.9.1-preview.N` does NOT satisfy `^2.9.1` | verified — node-semver Prerelease Tags + Caret Ranges | |
| 36 | + |
| 37 | +Tension resolved: strict semver says same-base stable (2.9.1) IS newer than |
| 38 | +2.9.1-preview.N, but this repo's comparator deliberately treats same-base as |
| 39 | +not-newer on BOTH channels (preview-channel O3 rule predates this fix; PR #871 |
| 40 | +encodes the same expectation for the latest channel). Rationale: the release |
| 41 | +train promotes a preview to its same-base stable, so the update is |
| 42 | +content-lateral and offering it is a nag. This is a product decision, recorded |
| 43 | +here so a future "strict semver" refactor can find it. |
| 44 | + |
| 45 | +## Known limitation (audit blocker 1, folded) |
| 46 | + |
| 47 | +Respin preview tags exist in the wild: `v2.7.9-preview.20260712.1` / `.2` — |
| 48 | +i.e. `x.y.z-preview.YYYYMMDD[.r]`. `parsePreview`'s |
| 49 | +`/^(\d+)\.(\d+)\.(\d+)-preview\.(\d+)$/` rejects the trailing `.r`, so installs |
| 50 | +on a respin preview remain stuck at `already_latest` even after this fix. This |
| 51 | +gap predates the fix on BOTH channels and expanding the comparator would fork |
| 52 | +behavior from PR #871, so it stays out of scope here. Follow-up candidate: |
| 53 | +widen `parsePreview` to `(\d+)(?:\.(\d+))?$` and treat the respin counter as an |
| 54 | +extra `gt` tuple element (needs its own cycle + tests). |
| 55 | + |
| 56 | +## Implementation delta (diff-level) |
| 57 | + |
| 58 | +- MODIFY `src/update/notify.ts` — one line in `isNewer` latest-channel branch: |
| 59 | + `const c = parseStable(current) ?? parsePreview(current)?.slice(0, 3);` |
| 60 | + plus doc-comment update naming the preview-core rule. |
| 61 | +- MODIFY `tests/update-notify.test.ts` — add latest-channel case: |
| 62 | + `isNewer("2.9.1", "2.8.2-preview.20260731", "latest") === true`, |
| 63 | + `isNewer("2.9.1", "2.9.1-preview.20260731", "latest") === false`. |
| 64 | +- MODIFY `tests/update-job.test.ts` — add `checkForUpdate("latest", ...)` case: |
| 65 | + older preview → `updateAvailable: true, canUpdate: true`; same-base preview → |
| 66 | + both false (`already_latest`). |
| 67 | + |
| 68 | +## Activation scenarios (C) |
| 69 | + |
| 70 | +1. Red: new tests fail on unmodified tree (preview current → `already_latest`). |
| 71 | +2. Green: all three files' tests pass after the one-line change. |
| 72 | +3. No regression: full `bun run test` + `bun run typecheck`. |
0 commit comments