From 05041c554bb1b82eb00ce8790406698d24e354a6 Mon Sep 17 00:00:00 2001 From: Mihhail Solovjov Date: Sat, 30 May 2026 09:06:11 +0300 Subject: [PATCH] docs(roadmap): add phased improvement roadmap documentation Add comprehensive roadmap documentation outlining the strategic direction for inup, including the north star vision and seven development phases (correctness, headless, trust & UX, intelligence, infra, ecosystem, distribution) with sequenced, verified tasks and implementation notes. --- docs/roadmap/00-north-star.md | 143 ++++++++++++++++++++++++++++++++ docs/roadmap/01-correctness.md | 54 ++++++++++++ docs/roadmap/02-headless.md | 51 ++++++++++++ docs/roadmap/03-trust-and-ux.md | 52 ++++++++++++ docs/roadmap/04-intelligence.md | 63 ++++++++++++++ docs/roadmap/05-infra.md | 48 +++++++++++ docs/roadmap/06-ecosystem.md | 41 +++++++++ docs/roadmap/07-distribution.md | 36 ++++++++ docs/roadmap/README.md | 108 ++++++++++++++++++++++++ 9 files changed, 596 insertions(+) create mode 100644 docs/roadmap/00-north-star.md create mode 100644 docs/roadmap/01-correctness.md create mode 100644 docs/roadmap/02-headless.md create mode 100644 docs/roadmap/03-trust-and-ux.md create mode 100644 docs/roadmap/04-intelligence.md create mode 100644 docs/roadmap/05-infra.md create mode 100644 docs/roadmap/06-ecosystem.md create mode 100644 docs/roadmap/07-distribution.md create mode 100644 docs/roadmap/README.md diff --git a/docs/roadmap/00-north-star.md b/docs/roadmap/00-north-star.md new file mode 100644 index 0000000..562a157 --- /dev/null +++ b/docs/roadmap/00-north-star.md @@ -0,0 +1,143 @@ +# 00 — North Star: from upgrader to upgrade co-pilot + +> The phase files (01–07) are *tasks* — they make inup **better**. +> This file is the *thesis* — what would make inup **inevitable**. + +## The reframe + +Nobody wants to "upgrade dependencies." They want to **stay current without breaking their app and +without spending an afternoon finding out the hard way.** The job inup is hired for isn't editing +version strings — it's *removing the fear*. Today inup, like `npm-check-updates`, `taze`, and +`npm outdated`, answers **"what's outdated?"** That question is commoditized. The unsolved, valuable +one is: + +> **"Is this upgrade safe *for my code*, and if not, what exactly do I have to change?"** + +A god-tier inup is the only tool that answers that — **locally, privately, before you commit.** +"Upgrade with confidence," not "upgrade and pray." That is the entire game. + +## Why inup specifically can win this + +inup already gathers, on most runs, much of the raw material the intelligence layer needs — it just +doesn't *reason* over it. **But be precise about what's actually in memory** (verified 2026-05-30), +because the previous version of this document over-claimed it: + +| Asset | Where | Status today | Unlocks | +|---|---|---|---| +| Every published version per package | [npm-registry.ts](../../src/services/npm-registry.ts) | ✅ fetched | semver-distance scoring | +| `deprecated` field | abbreviated packument | ⚠️ fetched, **dropped** by [package-metadata.ts](../../src/features/changelog/parsers/package-metadata.ts) | hard-"hold" signal — just extract it | +| `engines` field | manifest | ⚠️ present, **read nowhere** | runtime-compat warning — just extract it | +| Vulnerability advisories incl. `vulnerable_versions` | [vulnerability-checker.ts](../../src/services/vulnerability-checker.ts) | ⚠️ fetched, **never cross-referenced to the target** | "does this upgrade *fix* the CVE?" | +| Download counts | [npm-registry-client.ts](../../src/features/changelog/clients/npm-registry-client.ts) | ✅ fetched (`weeklyDownloads`) | adoption/maturity signal | +| Changelogs / GitHub releases | [features/changelog](../../src/features/changelog/) | ✅ fetched | breaking-change detection | +| Per-version publish **`time`** | npm registry | ❌ **NOT fetched** — inup requests the *abbreviated* doc ([npm-registry.ts:88](../../src/services/npm-registry.ts#L88)) | release-age scoring — **needs a fetch decision** ([04](04-intelligence.md) #5) | +| Full package.json graph | [package-detector.ts](../../src/core/package-detector.ts) | ✅ | the constraint set to solve | +| Streaming + persistent cache | [persistent-cache.ts](../../src/services/persistent-cache.ts) | ✅ | doing the above *fast* | +| Git working-tree state | [utils/git.ts](../../src/utils/git.ts) | ✅ (read-only) | safe, revertible application | + +The honest read: **most signals are a field-extraction away; `time` (release age) is the one that +isn't.** That distinction *is* the sequencing of [P3](04-intelligence.md). + +## The wedges, tiered by honesty + +They differ by an order of magnitude in cost and risk. **Build down the list; don't pretend the +bottom tier is plannable today.** + +### NEAR — plannable now, mostly arithmetic ([04-intelligence.md](04-intelligence.md)) + +**The Risk Score — turn a wall of versions into a verdict.** A single, **explainable** 0–100 number +per upgrade, composed from: semver distance, the vuln-fix cross-reference, deprecation, `engines.node`, +changelog breaking-section presence, adoption, and (once the fetch decision is made) release age. +Output: the list sorts and colours into **"safe now / review / hold."** The cheapest wedge with the +highest perceived-intelligence payoff — and, crucially, its **first three inputs need no new fetch** +(see [04](04-intelligence.md) Tier A). This is the next real feature after the headless spine. + +### FAR — large but independently shippable (each is its own project) + +**The Verify Loop — "upgrade and pray" becomes "upgrade, verified."** Apply the selected set in an +**isolated git worktree**, run `install` → `build` → `test`, report pass/fail; on failure +**auto-bisect** the culprit and offer to keep only the green subset. *Seed:* the git-revert + test-hook +work in [02-headless.md](02-headless.md) #8 is the first rung. + +**The Constraint Solver — never propose a set that won't install.** Treat a multi-select as a +constraint problem over **peer dependencies** and **ecosystem groups** (react + react-dom + +@types/react move together; all `@babel/*` align), and solve for a coherent version set. **Note:** +inup has *no* grouping primitive today (the previously-cited `scope-grouping.ts` is not in the +source) — so this builds the semantic-grouping layer from scratch; it doesn't extend an existing one. + +### SPECULATIVE — research, not a backlog item + +**Usage-Aware Impact — the holy grail.** Parse the user's own imports/call sites, intersect with each +package's **breaking surface** (changelog `BREAKING` entries + removed/renamed exports inferred from +shipped `.d.ts` between versions), and say: + +> "You call `foo.bar()` in 3 files. It was **removed in v3**. Here are the call sites." + +The difference between "react-router went 6→7 (risky)" and "you use two APIs from it and neither +changed — this major is safe *for you*." No competitor does this. It is also a genuine research +problem (JS/TS/ESM/CJS/JSX, re-exports, dynamic imports). **Treat it as a spike, not a sprint.** Start +absurdly narrow (static ESM named imports, TS-only) and prove signal before committing. + +**The AI migration co-pilot.** Once Usage-Aware Impact yields *a breaking change + the exact affected +call sites*, an LLM can draft the migration diff for *this* repo. The god-tier leap — and it must +honor inup's ethos (**no telemetry, no data collection**) as a hard constraint: + +- **Explicit opt-in**, off by default. +- **Bring-your-own-key**, provider-agnostic; nothing leaves the machine without consent. +- The deterministic engine is the product; AI is an *accelerator*, never a dependency. inup stays + fully useful with network and AI both off. + +> **Why this tier exists:** the most exciting ideas here are the least ready to plan. Keeping them +> visible but un-scheduled protects near-term work from being starved by ambition. + +## Non-negotiables (the identity that must survive) + +1. **Zero-config still works.** Intelligence is progressive: great with no setup, deeper if you opt + in. Never a wizard on first run. +2. **Privacy-first, no telemetry.** A tool that edits source-controlled files earns trust by keeping + computation local. +3. **Fast.** The streaming/cache architecture is a feature; the intelligence layer must stay off the + critical path (compute async, render progressively). +4. **Beautiful & multi-PM.** TUI polish and npm/yarn/pnpm/bun parity are table stakes we don't regress. + +## The roadmap arc + +``` +GOOD (today) GREAT (P0→P3) GOD TIER (the moat) +───────────── ─────────────────── ───────────────────────── +interactive multi-PM P0 protect correct writes Verify Loop (FAR) +upgrader, themes, P1 headless engine Constraint Solver (FAR) +vuln badges, P2 honest, discoverable UX Usage-Aware Impact (SPECULATIVE) +changelog viewer P3 Risk Score (NEAR wedge) + AI migration (SPECULATIVE) + + "what's outdated?" "what's safe?" "fix it for me, safely" +``` + +Sequencing logic: + +- **P0 is small now** — the feared write bug was already fixed; P0 just *locks* that with a test and + cleans up three small mis-detections ([01](01-correctness.md)). Correctness is still the floor; + it's just mostly already poured. +- **"Great" is mostly reasoning over data inup already fetches** — high leverage, low new + infrastructure. But it needs the **headless engine** ([02](02-headless.md)) underneath it first, so + a verdict has a consumer. +- **The NEAR wedge (Risk Score) is the only god-tier-adjacent feature worth scheduling now**, and its + first inputs are free. +- **FAR and SPECULATIVE need net-new capability** — each lands one at a time, after the critical path + is paid down. + +## The repositioning, in one line + +> **inup** — not "an interactive dependency upgrader," but **the dependency upgrade co-pilot: know +> what's safe, see what it touches, and fix it — locally, privately, before you ship.** + +## Honest risks + +- **Source parsing is hard** — why Usage-Aware Impact is *speculative*. Prove it on a spike first. +- **Verify Loop is slow & environment-dependent.** Opt-in, cache aggressively, bisect only on failure. +- **Risk scoring can mislead if it pretends to be precise.** Keep it *explainable* — always show the + why, never a bare number. +- **AI cost/accuracy/privacy.** Strictly opt-in + BYO-key; suggestions are drafts the user reviews. +- **The newest risk: drift between the plan and the code.** This revision exists because the prior + roadmap's headline rested on a bug that no longer existed. Every claim here is dated and verifiable — + keep it that way. diff --git a/docs/roadmap/01-correctness.md b/docs/roadmap/01-correctness.md new file mode 100644 index 0000000..f669d2b --- /dev/null +++ b/docs/roadmap/01-correctness.md @@ -0,0 +1,54 @@ +# P0 — Correctness: protect the writes that already work + +> **Read this first, because it overturns the old plan.** The previous roadmap opened by claiming +> inup *"silently strips the `^`/`~` range prefix and pins your dependency on every upgrade."* +> **That is false against the current code** (verified 2026-05-30). The write at +> [upgrader.ts:142](../../src/core/upgrader.ts#L142)/[154](../../src/core/upgrader.ts#L154) does +> write `choice.targetVersion` verbatim — but that value is **already prefixed** upstream: +> [selection-state-builder.ts:127-139](../../src/ui/session/selection-state-builder.ts#L127-L139) +> builds it through `applyVersionPrefix()` +> ([ui/utils/version.ts:6-10](../../src/ui/utils/version.ts#L6-L10)), which lifts the original +> operator and re-applies it. So `"^1.2.3"` → `"^1.5.0"`. The headline bug doesn't exist. +> +> That changes this phase's job. It is no longer *"stop producing wrong results"* — it's **"protect +> the correct behavior with a test, then fix three genuinely-silent mis-detections."** P0 went from a +> phase to an afternoon. The trust floor is mostly already poured; we're sealing it. + +See the [legend](README.md#legend) for rating definitions. + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 1 | **Lock the write path with a characterization test** | 🔴 | S | 0.5d | **New, and the most important item here.** Prefix-preservation works but has **no test guarding it** — one careless refactor of `applyVersionPrefix` or `createUpgradeChoices` silently reintroduces the exact bug the old roadmap feared. Golden-test the write end-to-end: feed a `package.json` with a `^` dep, a `~` dep, an exact pin, tabs *and* 4-space siblings; assert the output preserves each operator, the indentation, the trailing newline, and is a **no-op when nothing changed**. This is the cheapest insurance in the project and the guardrail for #2. Extend [test/unit/core](../../test/unit/core). | +| 2 | **Preserve `package.json` formatting on write** | 🔴 | S | 0.5–1d | [upgrader.ts:161](../../src/core/upgrader.ts#L161) does `JSON.stringify(pkg, null, 2) + '\n'`, normalizing tabs/4-space/odd-ordered files to 2-space and forcing a trailing newline. Real, but **S not M**: detect the existing indent (read the raw text before parse) and the original final-newline, round-trip both, and **skip the write entirely if the serialized result is unchanged**. Re-rated down — the old roadmap called this M by lumping it with the (non-existent) prefix bug. | +| 3 | **Detect Bun's text lockfile `bun.lock`** | 🔴 | S | 0.5d | Bun ≥ 1.2 writes a text `bun.lock`; the detector only lists `bun.lockb` ([package-manager-detector.ts:44](../../src/services/package-manager-detector.ts#L44),[:111](../../src/services/package-manager-detector.ts#L111)). A lockfile-only Bun repo falls through to the npm default. One array entry. *Narrow blast radius:* repos with `"packageManager": "bun@…"` are already caught by the field regex at [:92](../../src/services/package-manager-detector.ts#L92). | +| 4 | **Fix the `lib/` silent skip — make `SKIP_DIRS` overridable** | 🟡 | S | 0.5d | `SKIP_DIRS` hardcodes `lib`/`es`/`esm`/`cjs` ([scan.ts:9-19](../../src/utils/filesystem/scan.ts#L9-L19)), so a monorepo package living under `lib/` is **silently skipped, no warning**. The `.inuprc` reader already exists ([project-config.ts:8](../../src/config/project-config.ts#L8)) — add an `includeDirs`/`scanDirs` override there, and at minimum log when a `package.json`-bearing dir is pruned by the default list. *(Broader discovery work — `.gitignore` awareness, scanner de-dup — lives in [06-ecosystem.md](06-ecosystem.md).)* | + +## Why this ordering + +- **#1 before #2.** Write the lock test first, then make the formatting change against it — the test + is what lets you touch [upgrader.ts:160-162](../../src/core/upgrader.ts#L160-L162) without fear, + and it permanently documents the prefix-preservation contract so it can't quietly rot. +- **#3 and #4 are independent afternoon fixes.** Each kills one class of *silent* wrongness + (mis-detected PM, skipped package). They don't depend on #1/#2 and can land in any order. + +## What was removed from the old P0 (and why) + +- **"Preserve the range-prefix style" (was the 🔴 headline).** ❌ **Already implemented.** Deleted, + and its emotional framing with it. Its only legacy here is #1, which *locks* the behavior. +- **"Cross-platform `package.json` path handling."** Demoted to a footnote. The string + `packageJsonPath.replace('/package.json', '')` at + [upgrader.ts:122](../../src/core/upgrader.ts#L122) is POSIX-only, but it only feeds a **spinner + label** (`packageDir`); the actual file write at [:161](../../src/core/upgrader.ts#L161) uses the + real path, and install dir resolution at [:52](../../src/core/upgrader.ts#L52) already uses + `dirname()`. It's a cosmetic log glitch on Windows, not a correctness bug. *Fix it opportunistically + by copying the `dirname()` call from line 52 — don't schedule it.* + +## Relationship to later phases + +- The **`--save-exact`** opt-out (for users who *want* exact pins) lives in + [06-ecosystem.md](06-ecosystem.md). Because preservation is already the default, that flag is now + the *primary* prefix control rather than a follow-on — see its reframing there. +- **Routing detector warnings to stderr** (so they don't corrupt `--json`) is an output-hygiene + concern handled in [02-headless.md](02-headless.md), not here. +- The **broader write-path coverage** (beyond the #1 golden test) is tracked in + [05-infra.md](05-infra.md). diff --git a/docs/roadmap/02-headless.md b/docs/roadmap/02-headless.md new file mode 100644 index 0000000..218af8a --- /dev/null +++ b/docs/roadmap/02-headless.md @@ -0,0 +1,51 @@ +# P1 — Headless: become a program, not just a TUI + +> **The spine, and the single biggest *real* gap** (verified 2026-05-30). `runCli` +> ([cli.ts:25](../../src/cli.ts#L25)) parses only `-d/--dir`, `-e/--exclude`, `-i/--ignore`, +> `--max-depth`, `--package-manager`, `--debug` ([cli.ts:137-145](../../src/cli.ts#L137-L145)) and +> then always renders the full-screen TUI via `upgrader.run()` +> ([cli.ts:99](../../src/cli.ts#L99)). There is **no `--json`, no `--check`, no `-y`, no +> `process.stdout.isTTY` branch, and no `$CI` detection.** Exit codes exist only for *crashes* +> ([cli.ts:69](../../src/cli.ts#L69),[:151](../../src/cli.ts#L151)), never for "updates exist." So +> inup can't be scripted, can't gate CI, and will **hang in a pipeline** waiting on a TUI nothing can +> drive. This phase is the foundation the intelligence layer ([04](04-intelligence.md)) sits on — a +> verdict is only useful if something other than a human eye can read it. + +**Treat this as one epic, not nine tasks.** They share a single seam: a non-interactive path that +reuses the already-resolved outdated-package list *before* the TUI renders. Build that seam once +(#1); the rest are thin layers. + +See the [legend](README.md#legend) for rating definitions. + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 1 | **Non-interactive core** — branch on `!process.stdout.isTTY`, `--yes`, or `$CI` | 🔴 | M | 2–3d | The keystone. Before `upgrader.run()` renders the TUI, detect non-interactive and route the resolved package list through a headless code path instead. Auto-detect `process.env.CI`. Everything below reuses this branch. | +| 2 | **`--json` + output hygiene** — machine-readable report on clean stdout | 🔴 | S | 1d | Serialize the same package data the list shows. **Bundled with the stderr fix it requires:** detector warnings currently `console.log` to **stdout** ([package-manager-detector.ts:69](../../src/services/package-manager-detector.ts#L69),[:130](../../src/services/package-manager-detector.ts#L130)), which would corrupt JSON — move them to stderr first. The enabler for CI checks and piping. | +| 3 | **`--check` mode + a meaningful exit code** | 🔴 | S | 0.5d | Exit **non-zero when updates (or vulnerabilities) exist**, like `prettier --check`. inup sets a failing code only on crashes today, so it can't gate a build. Pairs with `--json`. | +| 4 | **`-y/--yes` + target presets** (`--target patch\|minor\|latest`; aliases `--patch/--minor/--latest`) | 🔴 | M | 2–3d | **Merges four scattered old items.** Auto-select by tier and apply without the TUI, reusing the bulk-select logic behind `m`/`l` ([input-handler.ts:273-285](../../src/ui/input-handler.ts#L273-L285)). One flag family, not new keys. | +| 5 | **`--dry-run`** — print the planned edits, skip write + install | 🔴 | M | 1–2d | Gate the write at [upgrader.ts:160](../../src/core/upgrader.ts#L160) and install at [:41](../../src/core/upgrader.ts#L41). Print a per-file version diff. Most valuable *after* [P0](01-correctness.md) so the preview reflects the preserved formatting/prefix. | +| 6 | **Plain non-TTY output + post-run summary** | 🔴 | S | 0.5d | When `!isTTY`, print a line-based report (not escape codes into a logfile), and close with a concise "upgraded N packages across M files" recap that surfaces the commit/revert command (git state is already known). Reuses the #1 branch. | +| 7 | **`NO_COLOR` / `FORCE_COLOR` + `--no-color`** | 🔴 | S | 0.5d | No handling today (grep-clean). Set chalk's level once at startup from env/flag before any render. Essential for clean CI logs. | +| 8 | **Safe apply: git revert + optional test hook** | 🟡 | M | 1–2d | inup already refuses to run on a dirty tree ([cli.ts:32-42](../../src/cli.ts#L32-L42)), so **git is the backup** — no file copying. On non-zero install exit ([upgrader.ts:83-90](../../src/core/upgrader.ts#L83-L90)), offer `git checkout -- `; optionally run a configured `--test ""` first and revert on its failure. **Note:** [utils/git.ts](../../src/utils/git.ts) currently exports only `getGitWorkingTreeState` — this **adds** a checkout helper. This is the first rung of the north-star Verify Loop ([00](00-north-star.md)). | +| 9 | **`--include` allowlist** (inverse of `--ignore`) | 🟡 | S | 0.5d | **De-duplicated** (appeared twice in the old file). `--ignore` already exists ([cli.ts:139-142](../../src/cli.ts#L139-L142)) with glob→regex matching ([project-config.ts:103-132](../../src/config/project-config.ts#L103-L132)); mirror it to target a subset by glob, in both modes. | + +## The CI story, end to end + +Once #1–#3, #6, #7 land, inup is a pipeline tool with no terminal required: + +``` +inup --check # exit 1 if anything is outdated → fails the build +inup --json | jq … # structured drift report for dashboards/bots +inup -y --target minor # auto-apply the safe tier in a scheduled job +``` + +That is the transformation: from *"a thing I run manually"* into *"a thing my pipeline runs."* + +## Sequencing within the phase + +1. **#1 first** — the seam everything attaches to. +2. **#2** carries its own stderr fix — do it as one change so the JSON is parseable from day one. +3. **#3, #6, #7** are cheap and independent — batch them with #1. +4. **#5 (`--dry-run`) and #8 (safe apply) want [P0](01-correctness.md) done first**, so previews and + reverts operate on correct, minimal-diff writes. +5. **#8's test-hook half** is the Verify Loop seed — keep it last and minimal. diff --git a/docs/roadmap/03-trust-and-ux.md b/docs/roadmap/03-trust-and-ux.md new file mode 100644 index 0000000..8d7c5c3 --- /dev/null +++ b/docs/roadmap/03-trust-and-ux.md @@ -0,0 +1,52 @@ +# P2 — Trust & UX: make the interactive product honest and discoverable + +> The cheapest credibility phase. The symptom: **the shipped README documents keys that don't +> exist.** Its table ([README.md:57-68](../../README.md#L57-L68)) promises `Space` to toggle (there +> is **no `case 'space'`** in the handler) and labels `←/→` as *"patch/minor/major"* when they +> actually cycle a single selection **current → range → latest** ([input-handler.ts:252-258](../../src/ui/input-handler.ts#L252-L258)). +> Meanwhile the real keys `d/p/o/s/!` are **absent** from that table. The instinct is to edit the +> README. The right fix is to remove the *possibility* of drift: make the keymap **data**, then let +> the input handler, the in-app help, and the docs all read from it. + +See the [legend](README.md#legend) for rating definitions. + +## The anchor: keymap-as-data (do this first — it dissolves four old tasks) + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 1 | **Extract the keymap into one declarative table** | 🔴 | M | 1–2d | Keys are hardcoded inline in the [input-handler.ts](../../src/ui/input-handler.ts) normal-mode switch ([:243-333](../../src/ui/input-handler.ts#L243-L333)): `m`/`l`/`u` bulk, `d`/`p`/`o` filters, `i`/`s`/`t`, plus `/` and `!`. Move them to a `{ key, action, label, context }[]` table that **(a)** drives the switch, **(b)** renders the `?` overlay (#2), and **(c)** generates the README key section (#4). One source of truth → drift becomes structurally impossible, and a new binding either slots into a free key or fails loudly instead of silently double-binding. | +| 2 | **`?` in-app help overlay** — rendered from the keymap table | 🔴 | S | 0.5d | Reuse the existing modal input pattern (theme/info/debug modals, [input-handler.ts:73-165](../../src/ui/input-handler.ts#L73-L165)). With #1 done, the overlay is just a render of the table — always correct, zero maintenance. | +| 3 | **`Space` toggles selection** | 🔴 | S | 0.5d | The README already promises this and it's a no-op today. Add it to the table; toggle between `none` and the last non-none option. The fastest single credibility fix. | +| 4 | **Generate the README keyboard section from the keymap** | 🔴 | S | 0.5d | Replace the hand-written (and wrong) table at [README.md:57-68](../../README.md#L57-L68) with generated output. Fixes the documented-but-missing (`Space`), the mislabeled (`←/→`), and the real-but-undocumented (`d/p/o/s/!`) in one move, permanently. *(This is the right home for the README keymap fix — not a one-off hand edit.)* | + +> **These four were four separate concerns in the old plan** ("fix README table", "help overlay", +> "configurable keybindings", broken `Space`). They share one root cause. Solve the root, and +> configurable keybindings becomes a trivial table extension rather than a project. + +## Discoverability & affordances + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 5 | **Use the real terminal width everywhere** | 🟡 | S | 0.5d | [renderer/index.ts:24](../../src/ui/renderer/index.ts#L24) hardcodes `80` for `renderPackageLine` even though `terminalWidth` is threaded through `renderInterface` right below it. **Also** the "update available" banner hardcodes `78` with manual column math ([cli.ts:105-126](../../src/cli.ts#L105-L126)). Both mis-render on wide/narrow terminals — fix together. | +| 6 | **Friendlier exit states** for "all up to date" / "nothing selected" | 🟡 | S | 0.5d | `return` silently does nothing when 0 packages are selected ([input-handler.ts:260-271](../../src/ui/input-handler.ts#L260-L271)) — show a hint instead. | +| 7 | **"Only vulnerable" filter** (e.g. `v`) | 🟡 | S | 0.5d | Audit data already exists ([vulnerability-checker.ts](../../src/services/vulnerability-checker.ts)); add a filter toggle in the #1 table alongside the `d`/`p`/`o` filters. **The cheap UI half of [04](04-intelligence.md) #1** (vuln-fix verdict) — build them together. | +| 8 | **Persist last filters / default target** | 🟡 | S | 0.5d | The `ConfigFile` interface persists only `theme` today ([config.ts:6-8](../../src/utils/config.ts#L6-L8)) — a clean `ConfigManager` singleton, easy to extend with `lastFilters`/`defaultTarget`. | +| 9 | **Vim navigation aliases** (`j`/`k`, `g`/`G`) | 🟢 | S | 0.25d | Trivial **once #1 exists** — just more rows in the table. Low-cost muscle-memory win. | + +## Deferred / re-scoped (don't let these masquerade as quick wins) + +- **Workspace-aware grouping** and **sort/group toggle** — 🟡 **M each, and net-new.** The old roadmap + pitched these as "extends the existing `scope-grouping.ts`." **That module does not exist in + `src/`** (only a stale `dist/` artifact; verified 2026-05-30). So these are *new* features with no + seam to lean on, and they're most useful only *after* the Risk Score ([04](04-intelligence.md)) + gives "severity"/"size" real meaning. Schedule after P3, if at all. +- **Interactive per-version picker** — 🟡 M. Selection is `none → range → latest` today; all versions + are already cached, so a picker modal is pure UI. Genuine nice-to-have, not a priority. +- **Mouse support** — 🟢 M. Last. Keyboard parity matters more. + +## Sequencing + +- **#1 → #2 → #3 → #4** is the core loop: extract the table, then the overlay, `Space`, and the + generated README are nearly free and permanently correct. +- **#5, #6** are independent quick wins — anytime. +- **#7** rides along with [04](04-intelligence.md) #1. diff --git a/docs/roadmap/04-intelligence.md b/docs/roadmap/04-intelligence.md new file mode 100644 index 0000000..acd2f69 --- /dev/null +++ b/docs/roadmap/04-intelligence.md @@ -0,0 +1,63 @@ +# P3 — Intelligence: turn data inup has into a verdict + +> The first step toward the [north star](00-north-star.md): reasoning over data inup already has, +> turning a wall of versions into **"safe now / review / hold."** But the old roadmap's premise — +> *"it's all free, the data is already in memory"* — is **only partly true** (verified 2026-05-30). +> inup fetches the **abbreviated** npm packument +> (`accept: application/vnd.npm.install-v1+json`, [npm-registry.ts:88](../../src/services/npm-registry.ts#L88)), +> which **includes `deprecated` and `engines`** but **omits per-version `time`**. So this phase splits +> cleanly into *genuinely-free* signals and ones that need a fetch decision — and we build the free +> ones first. +> +> **Prerequisite:** the headless engine ([02](02-headless.md)). A verdict is only worth computing if a +> `--json` consumer or CI gate can act on it. + +See the [legend](README.md#legend) for rating definitions. + +## Tier A — zero new fetches (build these first) + +Every input here reads a field inup *already* has in memory. The new code is *scoring*, not *fetching*. + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 1 | **Vuln-fix verdict — does the upgrade _clear_ the advisory?** | 🔴 | M | 2–3d | **The marquee feature.** [vulnerability-checker.ts](../../src/services/vulnerability-checker.ts) sends only the *current* version ([:62-65](../../src/services/vulnerability-checker.ts#L62-L65)) and captures each advisory's `vulnerable_versions` string ([:139](../../src/services/vulnerability-checker.ts#L139)) **but never compares it to the target.** Add `semver.satisfies(target, vulnerable_versions)` to mark **"✓ fixed by upgrade"** vs "still vulnerable." Turns the audit from information into guidance — and it's the highest-weight Risk Score input. *(Pairs with the only-vulnerable filter, [03](03-trust-and-ux.md) #7.)* | +| 2 | **Deprecation signal** | 🟡 | S | 0.5d | npm's `deprecated` field **is in the abbreviated response** but is dropped by the metadata mapper ([package-metadata.ts:4-40](../../src/features/changelog/parsers/package-metadata.ts#L4-L40)). Extract it; a deprecated target is a hard **"hold."** Cheap, high-signal. | +| 3 | **`engines.node` incompatibility warning** | 🟡 | S | 0.5–1d | `engines` is likewise present in the manifest and **read nowhere** (grep-clean). When a target raises `engines.node` above the local runtime, flag it: *"hold: needs Node ≥ 22, you're on 20."* | +| 4 | **Risk Score scaffold + semver-distance** | 🔴 | M | 2–3d | The composable score object that #1–#3 plug into, plus the cheapest own input: patch ≪ minor ≪ major (semver is already a dep; the diff is already computed for the UI). Render as a colour/badge **plus an explainable breakdown line — never a bare number.** The list then sorts/colours into "safe now / review / hold." | + +## Tier B — needs a fetch decision or new parsing + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 5 | **Decide the fetch strategy: abbreviated vs full packument** | 🔴 | S | 0.5d | **New, and the gate for #6.** Today's abbreviated fetch is fast but has no `time`. Decide once: (a) keep abbreviated + a targeted `time`-only fetch for the few packages being scored, or (b) switch hot paths to the full document. This is the honest reason release-age isn't free — surface the trade-off, don't bury it. | +| 6 | **Release-age signal** | 🟡 | M | 1d | Never silently recommend a version published 6 hours ago. *Depends on #5* to obtain `time`. Then surface "published 2 days ago" and weight very-new versions as riskier. | +| 7 | **Changelog breaking-change signal** | 🟡 | M | 1–2d | Detect a `BREAKING`/`Migration` section in release notes ([release-notes-service.ts](../../src/features/changelog/services/release-notes-service.ts)). Presence ⇒ "review." Feeds the major/breaking highlight in [03](03-trust-and-ux.md). | +| 8 | **Adoption signal** | 🟢 | S | 0.5d | Download counts are already fetched ([npm-registry-client.ts](../../src/features/changelog/clients/npm-registry-client.ts)) and stored as `weeklyDownloads`; a target few have adopted yet is weak maturity evidence. Lowest-weight input — wire the existing data in. | +| 9 | **Persist vulnerability results** (short-TTL cache) | 🟡 | S | 0.5d | Advisories refetch every session; reuse [persistent-cache.ts](../../src/services/persistent-cache.ts) with a short TTL. Keeps the security signal off the critical path (a north-star non-negotiable: *fast*). | + +## Later (L — graduate only after the score earns its place) + +- **Changelog diff across the version range** — 🟡 L. The viewer shows the *latest* notes; surface + *everything between current and target* for accumulated breaking changes. Feeds #7. +- **Full transitive (lockfile-tree) audit** — 🟡 L. Today's audit checks direct deps' current + versions; a true `npm audit`-style walk reads the lockfile and flags transitive advisories. This is + **north-star-adjacent** (lockfile-tree parsing is a "missing capability" in [00](00-north-star.md)) — + don't start before Tier A ships. + +## Why this is the right "first intelligence" + +- **Tier A is genuinely free** — `deprecated`, `engines`, and the vuln cross-ref all read fields + already in memory; only the scoring is new. +- **Explainable by construction.** Each signal is a separate contribution with its own reason string, + so the UI can always answer *"why is this 'hold'?"* — the entire trust argument from [00](00-north-star.md). +- **It composes with everything before it.** The score colours the rows ([03](03-trust-and-ux.md)), + can sort the list, and ships in `--json` ([02](02-headless.md)) so a CI gate can fail only on + "hold"-tier upgrades. + +## Sequencing + +1. **#1 (vuln-fix) first** — marquee demo value and the heaviest-weighted input. +2. **#2, #3** next — nearly-free reads that immediately enrich the verdict. +3. **#4** composes them into the score object. +4. **#5 → #6** only when you're ready to pay for `time`. +5. **#7, #8, #9** fill out the score; the **L** items wait. diff --git a/docs/roadmap/05-infra.md b/docs/roadmap/05-infra.md new file mode 100644 index 0000000..41cb09a --- /dev/null +++ b/docs/roadmap/05-infra.md @@ -0,0 +1,48 @@ +# ∞ — Infra: long-term health (runs in parallel with everything) + +> **No dependency on the critical path** — hygiene you batch in whenever there's slack. The +> supply-chain trio (#2–#4) is ~half a day each and improves posture immediately; the coverage work +> is best timed as a *guardrail just ahead of the change it protects*. + +See the [legend](README.md#legend) for rating definitions. + +## Test coverage & safety nets + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 1 | **Raise core unit coverage toward ~50%+** | 🔴 | M | 2–4d | ~33 test files vs ~91 source files; no threshold configured. Prioritize the modules the feature phases touch: [upgrader.ts](../../src/core/upgrader.ts) (writes), [package-detector.ts](../../src/core/package-detector.ts), [version.ts](../../src/utils/version.ts). **The write-path golden test is owned by [P0](01-correctness.md) #1** — this item is the broader fill-in around it. Do it *before* the P0 formatting change and the P2 keymap extraction ship. | +| 2 | **Coverage threshold gate** in vitest config | 🟡 | S | 0.25d | Add `coverage.thresholds` to [vitest.config.ts](../../vitest.config.ts) (no threshold today) to prevent regression once #1 lands. | +| 3 | **Snapshot/golden tests for the renderer** | 🟡 | M | 1–2d | Rendering is largely untested. Snapshot string output for representative states (themes, narrow width) **before** the [P2](03-trust-and-ux.md) width/keymap changes, so they lock behaviour rather than chase it. | + +## Supply-chain posture (the parallelizable trio) + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 4 | **Dependabot / Renovate config** | 🔴 | S | 0.5d | Add `.github/dependabot.yml`. Strong dogfooding — inup *is* a dependency upgrader; it should keep itself current. | +| 5 | **`pnpm audit` (or osv-scanner) step in CI** | 🔴 | S | 0.5d | [.github/workflows/ci.yml](../../.github/workflows/ci.yml) runs format-check → test → build, with **no audit step**. Fail (or warn) on known vulnerabilities before release. | +| 6 | **Add ESLint** (typescript-eslint) + a `lint` script + CI step | 🔴 | M | 1–2d | Only Prettier today — **no eslint dep, no `lint`/`typecheck` script** ([package.json:9-25](../../package.json#L9-L25)), no eslint config on disk. The TS config is already strict, so ESLint mainly adds *complexity/import-hygiene* rules. Start with the recommended config to avoid churn. | +| 7 | **Add a `typecheck` script + pin a clean Node CI matrix** | 🟡 | S | 0.5d | `tsc --noEmit` for fast CI type-checking without emitting `dist/`. **And** CI currently runs **Node 24 only** ([ci.yml:24](../../.github/workflows/ci.yml#L24)) while `engines` requires ≥ 20 — test the supported range (20/22/24). | + +## Code health (supports the feature phases) + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 8 | **Route scattered `console.*` through a leveled logger** | 🟡 | M | 1–2d | A [debug-logger.ts](../../src/utils/debug-logger.ts) exists but is underused; direct `console.*` is sprinkled across services. **Directly supports [P1](02-headless.md)** — clean leveling (debug/info/warn/error → stderr) is what makes `--json`/`--quiet` tractable. Pull forward if you start headless work. | +| 9 | **Tidy the renderer signatures** | 🟢 | S | 0.5d | `renderInterface` takes **15 positional args** ([renderer/index.ts:35-69](../../src/ui/renderer/index.ts#L35-L69)) and `renderPackagesTable`/`renderConfirmation` take `any[]` ([:71](../../src/ui/renderer/index.ts#L71),[:75](../../src/ui/renderer/index.ts#L75)). Move to an options object + real types — do it *alongside* the [P2](03-trust-and-ux.md) width fix that touches the same file. | + +## What was *not* scheduled (deliberate de-scoping) + +- **"Decompose the 400-line modules."** Files like `package-detector.ts` and `input-handler.ts` are + coherent at their current size. Splitting on line count, absent a concrete maintenance pain, is + exactly the over-thinking this revision removes. The [P2](03-trust-and-ux.md) keymap extraction will + shrink `input-handler.ts` naturally — let decomposition *fall out of* feature work, not precede it. +- **Standalone HTTP-timeout unification** folded into #8/normal maintenance rather than its own task — + worth fixing the registry `Pool`'s missing per-request timeout when you're next in that file, not as + scheduled work. + +## Sequencing notes + +- **#4–#5 are independent and parallelizable** — the natural first batch. +- **#1/#3 are guardrails** — install each just ahead of the change it protects (#1 before P0's write + change; #3 before P2's renderer changes). +- **#8 supports P1** — pull it forward if headless work starts. diff --git a/docs/roadmap/06-ecosystem.md b/docs/roadmap/06-ecosystem.md new file mode 100644 index 0000000..9a5b00d --- /dev/null +++ b/docs/roadmap/06-ecosystem.md @@ -0,0 +1,41 @@ +# deeper — Ecosystem: widen who can use inup + +> P0–P3 make inup *correct, scriptable, honest, and smart* for the mainstream case. This phase +> removes the walls that lock whole audiences out — private registries, prerelease channels, pnpm +> catalogs, non-standard layouts. None block the critical path, but **#1 is the difference between a +> personal tool and a team/enterprise one.** + +See the [legend](README.md#legend) for rating definitions. + +## Registry & resolution + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 1 | **Custom registry support** (`--registry ` + read `.npmrc`) | 🔴 | L | 3–4d | `NPM_REGISTRY_URL` is hardcoded at [constants.ts:2](../../src/config/constants.ts#L2) and **nothing reads `.npmrc`** (grep-clean across `src/`), so Artifactory/Verdaccio/GH-Packages users **can't use inup at all.** Read `registry` + scoped-registry keys + auth tokens from `.npmrc`; forward through the HTTP layer ([npm-registry.ts](../../src/services/npm-registry.ts)). The single biggest *audience* unlock in the roadmap — scope it even if you don't build it soon. | +| 2 | **Opt-in prerelease versions** (`--pre`) | 🟡 | M | 1–2d | `parseVersions` filters with `/^\d+\.\d+\.\d+$/` ([version.ts:24-26](../../src/utils/version.ts#L24-L26)), so `beta`/`rc`/`next` are **entirely invisible.** Add a flag/toggle to include them when explicitly requested. | +| 3 | **`--save-exact` flag** (pin without `^`) | 🟡 | S | 0.5d | **Now the _primary_ prefix control, not a follow-on.** Since range-prefix preservation is already the default (see [01](01-correctness.md)), the only missing knob is the *opt-out*: write bare versions on request for users who want exact pins. | +| 4 | **pnpm `catalog:` protocol support** | 🟡 | M | 1–2d | The detector skips `workspace:`/`file:`/`link:`/`github:`… ([package-detector.ts:427-433](../../src/core/package-detector.ts#L427-L433)) **but not `catalog:`**, a growing pnpm pattern — so a `catalog:` spec currently falls through and is mishandled. At minimum skip it cleanly; ideally resolve via `pnpm-workspace.yaml`. | +| 5 | **GitHub token for changelogs** (`GITHUB_TOKEN`/`GH_TOKEN`) | 🟡 | S | 0.5d | The changelog GitHub client ([features/changelog/clients/](../../src/features/changelog/clients/)) is unauthenticated → GitHub's 60 req/hr limit is hit fast when browsing many packages. Send the token if present. | + +## Caching & offline + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 6 | **Cache management** (`--no-cache`, `--clear-cache`, `--offline`) | 🟡 | S–M | 0.5–1d | A persistent disk cache exists ([persistent-cache.ts](../../src/services/persistent-cache.ts)) with `CACHE_TTL` ([constants.ts:5](../../src/config/constants.ts#L5)), but there's no user-facing way to bypass, clear, or pin to it. `--offline` (never hit the network, surface staleness) is cheap on top of the same infrastructure. | + +## Discovery (non-standard layouts) + +> The silent-skip *bug* (`lib/`) is fixed in [01](01-correctness.md). These are the broader +> improvements for repos that rely on `.gitignore` or unusual structure. + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 7 | **De-dup the sync/async scanners, then respect `.gitignore`** | 🟡 | M | 1–2d | `findAllPackageJsonFiles` (sync) and `…Async` in [scan.ts](../../src/utils/filesystem/scan.ts) duplicate exclude/skip logic. **Collapse them first** (one code path), *then* add `.gitignore` parsing to that single path so discovery stops descending into ignored build trees. Order matters — de-dup de-risks the `.gitignore` work. | + +## Sequencing notes + +- **#1 is the strategic item** despite its size — it defines whether inup can be adopted inside a + company, and its `.npmrc` reader overlaps the future Constraint Solver's need to understand + resolution config ([00](00-north-star.md)). +- **#3 is trivial and rides on [01](01-correctness.md)** being done (it already is). +- **#7's two halves are ordered** — collapse the scanners before adding `.gitignore`. diff --git a/docs/roadmap/07-distribution.md b/docs/roadmap/07-distribution.md new file mode 100644 index 0000000..0e3d0dd --- /dev/null +++ b/docs/roadmap/07-distribution.md @@ -0,0 +1,36 @@ +# reach — Distribution: onboarding & release automation + +> The npm side is already strong — provenance-signed publish, and the release step already +> auto-generates GitHub notes ([publish.yml:50](../../.github/workflows/publish.yml#L50)). So this +> phase is narrow: fill the *onboarding* gap (so others can contribute) and the *trigger* gap (so +> releases don't need a manual button press). It sits last on the critical path — reach matters most +> once the product underneath is worth reaching for. + +See the [legend](README.md#legend) for rating definitions. + +## Onboarding docs + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 1 | **`CONTRIBUTING.md` + architecture overview** | 🔴 | S | 0.5–1d | One onboarding pass. Document the existing scripts (`build`, `test`, `format`, `link`, `version:*`) and the `workflow_dispatch` publish flow, plus a short layer diagram: [cli.ts](../../src/cli.ts) → [core/](../../src/core/) → [ui/](../../src/ui/) → [services/](../../src/services/). The cheapest unblock for outside PRs. | +| 2 | **Document `.inuprc` + the `s` audit in the README** | 🟡 | S | 0.5d | The config keys `ignore` / `exclude` / `showPeerDependencyVulnerabilities` / `showOptionalDependencyVulnerabilities` live in [project-config.ts:8-30](../../src/config/project-config.ts#L8-L30) but aren't documented. *(The keyboard section is now **generated** from the keymap, [03](03-trust-and-ux.md) #4 — this covers the config/audit prose that isn't auto-generated.)* | + +## Release automation + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 3 | **Auto-publish on tag push** (replace manual `workflow_dispatch`) | 🟡 | M | 1d | [publish.yml](../../.github/workflows/publish.yml) triggers only via `workflow_dispatch` with a tag input; the `version:*` scripts already push tags, so make `v*` tags fire the publish directly, keeping provenance signing. **Note:** GitHub release notes are *already* auto-generated (`generate_release_notes: true`), so a separate conventional-commits changelog generator is **largely redundant** — don't schedule it as its own task. | + +## Reach (genuinely nice-to-have — weight last) + +| # | Task | Value | Cx | Effort | Notes / reuse | +|---|---|:--:|:--:|---|---| +| 4 | **Refresh the demo content** | 🟢 | S | 0.5–1d | Recording tooling already exists in [docs/demo](../../docs/demo). The best new clips are the things that don't exist yet — record the `?` overlay and Risk Score *after* [P2](03-trust-and-ux.md)/[P3](04-intelligence.md) ship them. | +| 5 | **Alt install channels** (self-update / Homebrew / Docker) | 🟢 | M | — | An update *check* already exists ([cli.ts:73](../../src/cli.ts#L73)); a guided `--upgrade-self`, a Homebrew tap, and a tiny `node:alpine` image broaden reach. **npm + npx already covers the large majority of users** — build any of these only if demand appears. | + +## Sequencing notes + +- **#1 first.** Onboarding docs are the cheapest contributor unblock. +- **#3** is the one automation worth doing; it pairs with nothing because the changelog half is + already solved. +- **#4–#5 are deliberately last** and demand-gated. diff --git a/docs/roadmap/README.md b/docs/roadmap/README.md new file mode 100644 index 0000000..dc97013 --- /dev/null +++ b/docs/roadmap/README.md @@ -0,0 +1,108 @@ +# inup — Improvement Roadmap + +A ranked, sequenced plan for `inup`. Every item is concrete, links to the source it touches, is rated +by **value** and **complexity**, and — new in this revision — was **verified against the working code +on 2026-05-30.** The files are **phases**; the phases build on each other. + +`inup` is already a polished, *correct* interactive TUI upgrader (npm/yarn/pnpm/bun, monorepo-aware, +themes, vulnerability audit, changelog viewer). The earlier roadmap framed the work as *rescue* +("it silently mangles your files"). **That framing was wrong** — see [provenance](#what-this-revision-corrected-provenance). +The real work, in order, is: **lock the correct behavior that already exists**, **become usable +outside a terminal**, **stop lying about its own keymap**, and **turn the data it already has into a +verdict.** + +## Read this first + +**[00-north-star.md](00-north-star.md)** — the destination: how inup goes from *"an interactive +upgrader"* to **the dependency-upgrade co-pilot** that tells you whether an upgrade is safe *for your +code* before you run it. The phases are the path; the north star is where it leads. + +## The critical path (read top to bottom) + +The single most important idea: **do these in order.** Each phase unlocks the next. + +| Phase | File | The job it finishes | Why it's here | +|:--:|---|---|---| +| **P0** | [01-correctness.md](01-correctness.md) | *Protect* the writes that already work | The feared "silent re-pin" bug **doesn't exist** — preservation already works. P0 now means: lock it with a test, then fix 3 small silent mis-detections. An afternoon, not a phase. | +| **P1** | [02-headless.md](02-headless.md) | Become a *program*, not just a TUI | `--json` / `--check` / `-y` / exit codes / non-TTY. **The single biggest real gap**, and the spine the intelligence layer stands on. | +| **P2** | [03-trust-and-ux.md](03-trust-and-ux.md) | Make the interactive product honest & discoverable | The README documents keys that don't exist. Fix the root cause (keymap-as-data), not the symptom. | +| **P3** | [04-intelligence.md](04-intelligence.md) | Turn data inup has into a *verdict* | The Risk Score. Its first three inputs need **zero new fetches**; the marquee is the vuln-fix verdict. | +| **∞** | [05-infra.md](05-infra.md) | Long-term health, runs in parallel | Coverage, ESLint, CI audit, Dependabot. Independent of the critical path — batch it whenever. | +| **deeper** | [06-ecosystem.md](06-ecosystem.md) | Widen *who* can use inup | Custom registry/`.npmrc`, prereleases, pnpm catalogs, `.gitignore`-aware discovery. | +| **reach** | [07-distribution.md](07-distribution.md) | Onboarding & release automation | CONTRIBUTING, architecture doc, tag-triggered publish. | + +**52 sequenced items** across the seven phase files — down from ~70, after deleting one already-done +headline task, demoting cosmetic items to footnotes, merging duplicates, and dropping work the repo +already does (auto-generated release notes). Every remaining item was checked against the code. + +## If you only do five things + +This is the irreducible core. + +1. **Lock the write path with a test.** Prefix + indent + newline preservation already works but is + **unguarded** — one golden test stops it from silently regressing into the bug the old roadmap + feared. → [01](01-correctness.md) +2. **Ship headless mode as one epic** — `--json`, `--check` + exit codes, `-y`, non-TTY fallback. + One spine, not nine tasks. The biggest capability gap. → [02](02-headless.md) +3. **Make the keymap data, not code** — one table drives input handling, the `?` overlay, *and* a + generated README section. Kills the doc-drift permanently. → [03](03-trust-and-ux.md) +4. **Ship the vuln-fix verdict** — cross-reference the upgrade target against each advisory's + `vulnerable_versions` to say "✓ fixed by upgrade." The marquee intelligence win, and it needs no + new data. → [04](04-intelligence.md) +5. **Detect `bun.lock`** and **stop silently skipping `lib/`** — two real silent mis-detections. → [01](01-correctness.md) + +## True quick wins (🔴/🟡 value × S complexity, ~½ day each) + +- **Detect `bun.lock`** (text lockfile) — one array entry. → [01](01-correctness.md) +- **`lib/` skip override** via `.inuprc` (+ warn on prune) — the reader already exists. → [01](01-correctness.md) +- **`Space` toggles selection** — make the README's existing promise real. → [03](03-trust-and-ux.md) +- **`?` help overlay** — reuse the modal pattern; surfaces the full keymap. → [03](03-trust-and-ux.md) +- **`NO_COLOR` / `--no-color`** — set chalk level once at startup. → [02](02-headless.md) +- **Deprecation signal** — read the `deprecated` field inup already fetches; a deprecated target is a + hard "hold." → [04](04-intelligence.md) +- **`--save-exact`** — the now-primary prefix knob (preservation is already the default). → [06](06-ecosystem.md) +- **Dependabot config** + **`pnpm audit` in CI** — dogfood: this *is* a dependency upgrader. → [05](05-infra.md) +- **`CONTRIBUTING.md` + architecture doc** — document the already-good setup/test/release flow. → [07](07-distribution.md) + +## Legend + +**Value** — user/maintainer impact: + +- 🔴 **High** — clear, broadly-felt win +- 🟡 **Medium** — solid improvement, narrower audience +- 🟢 **Nice-to-have** — polish or niche + +**Complexity** — implementation cost: + +- **S** — small, < 1 day, low risk, no architectural change +- **M** — medium, a few days +- **L** — large, architectural or cross-cutting + +## What this revision corrected (provenance) + +This roadmap was rebuilt after **verifying every load-bearing claim against the source** (2026-05-30). +The previous version was well-organized but rested on assertions that the code contradicts. The +corrections, because they reshaped the priorities: + +- **The #1 headline task was already done.** "inup strips `^`/`~` and silently pins on every upgrade" + is **false**: `applyVersionPrefix()` ([ui/utils/version.ts:6-10](../../src/ui/utils/version.ts#L6-L10)) + preserves the operator before the write. Deleted the task and its framing; replaced it with a + regression test that *locks* the behavior. +- **A cited pillar doesn't exist.** `src/ui/state/scope-grouping.ts` was referenced as a shipped + feature and a Constraint-Solver seed; it's **absent from `src/`** (only a stale `dist/` artifact). + Reframed the workspace-grouping items as net-new, and removed the false "extends existing grouping" + claim from the north star. +- **"Intelligence is free" was half-wrong.** inup fetches the *abbreviated* packument, which **omits + `time`** — so release-age is *not* free; it needs a fetch decision. (`deprecated` and `engines` + *are* in that response, so those signals genuinely are cheap.) +- **One automation is already solved.** The publish workflow already sets `generate_release_notes: + true`, so the "conventional-commits changelog generator" task was dropped as redundant. +- **One "bug" is cosmetic.** The Windows-unsafe `packageJsonPath.replace('/package.json','')` only + feeds a spinner label, not a file write — demoted from a task to a footnote. +- **Re-rated by evidence.** Format-preserving write dropped M→S (it was inflated by being lumped with + the non-existent prefix bug); the intelligence phase was reordered so the zero-new-fetch signals + ship first. + +The discipline going forward: **every claim is dated and verifiable.** This revision exists because a +polished plan drifted from the code it described — the cheapest insurance against that is to cite the +line and check it.