|
|
@@ -11,11 +11,24 @@ runtime. You **preserve structure and make the smallest diffs that compile
|
|
|
and behave identically on the target**, driven by the *known* breaking
|
|
|
changes between $2 and $3 — not by re-deriving the business logic.
|
|
|
|
|
|
-The defining advantage of a same-stack uplift: **the same test suite can run
|
|
|
-on both the old and new runtime.** That makes your equivalence proof a real
|
|
|
-differential test (run on both, diff the results), not a golden-master
|
|
|
-recording. The whole command is built around establishing that dual-run
|
|
|
-harness early and leaning on it.
|
|
|
+The potential advantage of a same-stack uplift: **if both runtimes execute in
|
|
|
+this environment, the same test suite can run on both** and your equivalence
|
|
|
+proof becomes a real differential test (run on both, diff the results). That
|
|
|
+is the strong case — but it is **not always available**, and the command is
|
|
|
+explicit about when it is:
|
|
|
+
|
|
|
+- It depends on the stack. .NET can multi-target one test project to both
|
|
|
+ framework monikers (`<TargetFrameworks>net48;net8.0</TargetFrameworks>`),
|
|
|
+ **but `net48` only executes on Windows/Mono** — on a Linux/macOS box or most
|
|
|
+ CI sandboxes the old leg cannot run. Java 8→17 is not one suite over two
|
|
|
+ targets at all — it is the whole build run twice under two JDK toolchains.
|
|
|
+ Python 2→3 cannot import the same un-rewritten module under both
|
|
|
+ interpreters. So "true dual-run" is the *best* case, common only for
|
|
|
+ .NET-on-Windows.
|
|
|
+- When both runtimes are **not** runnable here, equivalence degrades — exactly
|
|
|
+ like `/modernize-transform` — to characterization tests pinned to
|
|
|
+ recorded/expected outputs on the target only. That is fine; it just must be
|
|
|
+ labelled honestly (Step 0.3, Step 7).
|
|
|
|
|
|
Optional 4th arg `$4` scopes to projects/modules matching a pattern.
|
|
|
|
|
|
@@ -35,33 +48,75 @@ Optional 4th arg `$4` scopes to projects/modules matching a pattern.
|
|
|
recorded/expected outputs (as in `/modernize-transform`). Note this in the
|
|
|
plan and UPLIFT_NOTES — reviewers must know whether the proof was a true
|
|
|
dual-run or target-only.
|
|
|
-4. **Detect the ecosystem migration tool** (see the agent's list): .NET
|
|
|
- `upgrade-assistant` / Portability Analyzer / `try-convert`; Java
|
|
|
- **OpenRewrite**; Python `pyupgrade`/`2to3`; Angular `ng update`. Report which
|
|
|
- are present. These do the mechanical bulk; this command orchestrates them
|
|
|
- and owns the residue.
|
|
|
+4. **Detect the ecosystem migration tool** — and distinguish **present /
|
|
|
+ runnable-here / actually-ran**. Most of these tools need a working
|
|
|
+ restore + build (and often network), which a read-only sandbox does not
|
|
|
+ have, so "installed" ≠ "produced findings". Report all three states and
|
|
|
+ **never fold a tool's findings into the catalog unless it actually ran** —
|
|
|
+ say "coverage lost: <tool> needs restore+network, unavailable here" instead.
|
|
|
+ - .NET: **`dotnet upgrade-assistant`** (loads + restores the project; also
|
|
|
+ *applies* changes in place — see Step 5). The legacy **Portability
|
|
|
+ Analyzer** (`apiport`) analyzes *compiled assemblies*, not source, and is
|
|
|
+ Windows-centric/archived — treat as optional, not primary.
|
|
|
+ - Java/Spring: **OpenRewrite** (`mvn rewrite:dryRun` is genuinely headless
|
|
|
+ and emits a patch — the most reliable of these; lean on it).
|
|
|
+ - Python: **`pyupgrade`** (source-level, runnable). Note `2to3` is deprecated
|
|
|
+ and removed in Python 3.13; `python-modernize` is abandoned — don't rely
|
|
|
+ on them.
|
|
|
+ - JS/Angular: `ng update` (edits in place, needs a clean git tree +
|
|
|
+ `node_modules`; no real report-only mode).
|
|
|
|
|
|
Run `/modernize-preflight $1 $3` for the full readiness report.
|
|
|
|
|
|
-## Step 1 — Project graph & ordering
|
|
|
-
|
|
|
-Same-stack uplifts move through a **build dependency graph**, not a strangler
|
|
|
-fig. Reuse `/modernize-map $1` if `analysis/$1/topology.json` exists, else
|
|
|
-build a quick project/module graph (`.csproj`/`.sln` references, Maven
|
|
|
-modules, package imports). Order **leaf-first**: uplift the libraries with no
|
|
|
-internal dependents before the apps that depend on them. Scope to `$4` if
|
|
|
-given. Present the order.
|
|
|
+## Step 1 — Working copy, project graph & ordering
|
|
|
+
|
|
|
+**Working copy (do this first).** An uplift edits an existing solution *in
|
|
|
+place* — it bumps target frameworks and fixes APIs while keeping the `.sln`,
|
|
|
+the relative `<ProjectReference>`/module paths, and a reviewable `git diff`.
|
|
|
+That is fundamentally different from `transform`/`reimagine`, which write a
|
|
|
+new tree. So: **copy the whole system once** — `cp -r legacy/$1 modernized/$1`
|
|
|
+(the entire solution, not project-by-project) — and do all editing in place
|
|
|
+under `modernized/$1/`, git-tracked. `legacy/$1` stays the untouched baseline
|
|
|
+oracle. Copying the *whole* solution (not incrementally) is what keeps
|
|
|
+relative project references intact and makes the final artifact a real
|
|
|
+`git diff` between the seeded copy and the end state — which is exactly what a
|
|
|
+reviewer of an uplift wants.
|
|
|
+
|
|
|
+**Graph & ordering.** Reuse `/modernize-map $1` if `analysis/$1/topology.json`
|
|
|
+exists, else build a quick project/module graph (`.csproj`/`.sln` references,
|
|
|
+Maven modules, package imports). Default order is **leaf-first** (libraries
|
|
|
+before the apps that depend on them), but three things override pure
|
|
|
+leaf-first — call them out in the plan:
|
|
|
+- **Spanning nodes go first, not last.** The dual-run test project and any
|
|
|
+ shared test utilities reference SUTs across the whole graph — they are not
|
|
|
+ leaves. Stand up / multi-target them up front so the harness exists before
|
|
|
+ you migrate anything.
|
|
|
+- **Dependency deltas force a coordinated cut.** A major-version bump consumed
|
|
|
+ mid-graph (EF6→EF Core, `javax`→`jakarta`) cannot be done leaf-first
|
|
|
+ incrementally — every consumer changes together. Sequence these as their own
|
|
|
+ cross-cutting step.
|
|
|
+- **Multi-target shared libraries during transition.** Set
|
|
|
+ `<TargetFrameworks>$2-moniker;$3-moniker</TargetFrameworks>` on shared leaf
|
|
|
+ libs so old and new consumers can both reference them while the migration is
|
|
|
+ in flight (the standard .NET technique). Note cycles in the project graph
|
|
|
+ need a manual cut point.
|
|
|
+
|
|
|
+Scope to `$4` if given. Present the working-copy plan and the order.
|
|
|
|
|
|
## Step 2 — Plan (HITL gate)
|
|
|
|
|
|
Present and **stop — change nothing until the user approves** (use plan mode
|
|
|
if available):
|
|
|
-- The exact version pair and the ecosystem tool you'll drive
|
|
|
-- The project order (leaf-first)
|
|
|
-- The dual-run harness plan (which test framework multi-targets both $2 and
|
|
|
- $3 — e.g. NUnit/xUnit/MSTest all can via multi-targeting) and whether a true
|
|
|
- dual-run is possible here or it's target-only (Step 0.3)
|
|
|
+- The exact version pair, the working-copy plan (Step 1), and which ecosystem
|
|
|
+ tool you'll drive (and whether it can actually run here)
|
|
|
+- The project order (leaf-first, with the spanning-node / dependency-cut /
|
|
|
+ multi-target overrides from Step 1)
|
|
|
+- The harness plan and **whether a true dual-run is possible here or it's
|
|
|
+ target-only** (Step 0.3): for .NET, multi-target one test project to both
|
|
|
+ monikers (the `net48` leg needs Windows); for Java, a double JDK build; for
|
|
|
+ Python, separate interpreter envs (the suite itself diverges post-`2to3`)
|
|
|
- How equivalence is proven: **baseline on $2 = oracle; $3 must reproduce it**
|
|
|
+ — or, target-only, characterization vs recorded outputs
|
|
|
- Anything ambiguous needing a decision now
|
|
|
|
|
|
## Step 3 — Delta catalog (the driver artifact)
|
|
|
@@ -81,10 +136,14 @@ Workflow({
|
|
|
```
|
|
|
|
|
|
It runs one finder per delta category (API-removed, behavioral-silent,
|
|
|
-project-system, dependency) in parallel, folds in the ecosystem tool's report,
|
|
|
-verifies each delta against the cited code, and returns structured delta
|
|
|
-cards. The finders are read-only; **you** write `DELTA_CATALOG.md` from the
|
|
|
-result. Surface `injectionFlags` if non-empty.
|
|
|
+project-system, dependency — the finders also probe reflection/encapsulation,
|
|
|
+globalization/locale, and hosting/runtime-config, the highest-blast-radius
|
|
|
+classes) in parallel, folds in the ecosystem tool's report **only if it
|
|
|
+actually ran**, verifies each delta against the cited code, and returns
|
|
|
+structured delta cards. Tell the user the finder count (one per category)
|
|
|
+before launching. The finders are read-only; **you** write `DELTA_CATALOG.md`
|
|
|
+from the result. Surface `injectionFlags` if non-empty, and read the
|
|
|
+`upliftVsRewriteSignal` (Step "When NOT to use").
|
|
|
|
|
|
**Fallback** (no Workflow tool): spawn the **version-delta-analyst** agent:
|
|
|
"Build the delta catalog for uplifting legacy/$1 from $2 to $3. Detect and run
|
|
|
@@ -102,10 +161,15 @@ Either way the catalog must rank by blast radius and mark each delta
|
|
|
The harness is the safety net the rest of the command leans on. Build it in
|
|
|
this order so you de-risk the oracle before depending on it:
|
|
|
|
|
|
-1. **Prove the harness shape first.** Stand up a test project that
|
|
|
- **multi-targets both $2 and $3** with a single trivial/dummy test, and run
|
|
|
- it on *both* targets. If that won't go green on both, fix the harness now —
|
|
|
- not mid-migration. (This is the structure `test-engineer` then fills.)
|
|
|
+1. **Prove the harness shape first — against a real (tiny) type, not a free
|
|
|
+ dummy.** A dummy test with no reference to the system-under-test only proves
|
|
|
+ the *test framework* multi-targets; it does not prove the hard part, which
|
|
|
+ is one test binding to **two SUT builds** (the $2 build and the $3 build)
|
|
|
+ via target-conditional references. So pick one trivial real type from the
|
|
|
+ system and assert on it under both targets. If that won't go green on both,
|
|
|
+ fix the harness now — not mid-migration. (This is the structure
|
|
|
+ `test-engineer` then fills.) If the $2 leg can't run here (Step 0.3), prove
|
|
|
+ the $3 leg only and mark the proof target-only.
|
|
|
2. **Baseline = the oracle.** Run the existing suite on the **$2** target and
|
|
|
record pass/fail per test. This is the equivalence target — including any
|
|
|
tests that legacy fails. You are proving *no behavior changed*, not *all
|
|
|
@@ -121,9 +185,15 @@ gap-fill tests to expected/recorded outputs and label the proof target-only.
|
|
|
|
|
|
## Step 5 — Migrate, leaf-first, minimal-diff
|
|
|
|
|
|
-For each project in dependency order:
|
|
|
-1. **Run the ecosystem codemod** for the Mechanical deltas (upgrade-assistant /
|
|
|
- OpenRewrite recipe / pyupgrade / ng update). Let the tool do what it does.
|
|
|
+All editing happens **in place inside the working copy `modernized/$1/`** from
|
|
|
+Step 1 (so relative project references resolve and the result is a clean
|
|
|
+`git diff` against the seeded copy). `legacy/$1` is never touched. Apply-mode
|
|
|
+tools (`upgrade-assistant`, `ng update`) mutate the tree in place — that is
|
|
|
+fine *here* because they run against the `modernized/$1/` copy, not `legacy/`.
|
|
|
+
|
|
|
+For each project in dependency order (respecting the Step 1 overrides):
|
|
|
+1. **Run the ecosystem codemod** for the Mechanical deltas (`upgrade-assistant`
|
|
|
+ apply / OpenRewrite recipe / `pyupgrade` / `ng update`) against the copy.
|
|
|
2. **Apply the Judgment deltas** by hand from the catalog.
|
|
|
3. **Smallest diff that builds.** Preserve structure, names, and layout. Adopt
|
|
|
a new idiom *only* where the old one was removed and there's no choice.
|
|
|
@@ -133,8 +203,7 @@ For each project in dependency order:
|
|
|
here (the inverse of its usual job): any change beyond the minimal uplift is
|
|
|
a finding.
|
|
|
|
|
|
-Write migrated code to `modernized/$1/` (never edit `legacy/` — it stays the
|
|
|
-read-only baseline oracle). Keep going until the project **builds on $3**.
|
|
|
+Keep going until the project **builds on $3**.
|
|
|
|
|
|
## Step 6 — Dual-run diff (the proof)
|
|
|
|