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
143 changes: 143 additions & 0 deletions docs/roadmap/00-north-star.md
Original file line number Diff line number Diff line change
@@ -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.
54 changes: 54 additions & 0 deletions docs/roadmap/01-correctness.md
Original file line number Diff line number Diff line change
@@ -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).
Loading
Loading