Skip to content
Draft
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
100 changes: 100 additions & 0 deletions .agents/skills/framework-modernizer/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
---
name: framework-modernizer
description: >-
Use this skill when the user asks to migrate, upgrade, or modernize a
Node.js codebase from Express 4 to Express 5 — including phrasings like
"bump express to v5", "upgrade express", "migrate to express 5",
"express deprecation warnings", "we're stuck on express 4", or when
preparing a major-version dependency PR involving express ^4.x.
Also fires when the user mentions removed Express APIs (app.del, res.send(status),
the deprecated body-parser bundling), discontinued path-to-regexp 0.x
patterns (e.g. unnamed wildcards `*` or optional segments `:id?`), or
asks for a migration plan / breaking changes audit on an Express 4 repo.
This skill audits, classifies (SAFE / AUTOFIX / MANUAL), applies safe
autofixes, and emits a phased migration plan grounded in the official
Express 5 migration guide. It does NOT run the consumer's tests; it
emits a plan the team executes.
license: MIT
allowed-tools: Read, Grep, Glob, Edit
---

# framework-modernizer

> **Reference skill — Express 4 → Express 5 migration.**
> Built with [Genesis](https://github.com/DevExpGbb/genesis) as a worked example for the workshop. Read it, fork the pattern, build your own (Next 13→14, React 17→18, Angular 16→17…).

## When to use this skill

- The repo's `package.json` has a top-level `express` dependency on `^4.x` and the user wants to move to `^5.x`.
- The user reports deprecation warnings from Express 4 they want resolved.
- A major-version Dependabot/Renovate PR is open and a human asks "is this safe to merge?"

**Do NOT use this skill when:**
- The repo is not Express (Fastify, Koa, Hapi → wrong tool).
- The Express version is already `^5.x` (no work).
- The user wants you to also write/run tests post-migration (out of scope — emit the plan, the team validates).

## What this skill does

1. **Discover.** `Glob` for `package.json` files. `Read` each, confirm `express` is a direct dependency on `^4.x`. Stop early if not found.
2. **Scan.** For each Express 4 app rooted in a discovered `package.json`:
- `Grep -n` the breaking-change patterns from [`references/express-4-to-5-breaking-changes.md`](references/express-4-to-5-breaking-changes.md) across `**/*.{js,mjs,cjs,ts}`.
- Collect each hit with its file path, line number, and matched pattern ID (e.g. `BC-001`).
3. **Classify** every finding via [`references/classifier-rubric.md`](references/classifier-rubric.md):
- **SAFE** — no behavior change in v5; informational only.
- **AUTOFIX** — mechanical replacement; this skill applies it via `Edit`.
- **MANUAL** — semantics changed; emit a TODO comment + reference link, do **not** edit.
4. **Apply autofixes.** For each AUTOFIX finding, perform the exact `Edit` specified in the catalog. Print a one-line diff summary per edit.
5. **Emit migration plan.** Write `MIGRATION-PLAN.md` at the repo root using [`references/phased-plan-template.md`](references/phased-plan-template.md). Include three phases: **Phase 1 — Autofixed (this skill)**, **Phase 2 — Manual edits required**, **Phase 3 — Validation checklist**. Cross-reference every MANUAL item back to the official Express 5 migration guide.

## Outputs

| Artifact | Where | When |
|---|---|---|
| Per-file `Edit`s for AUTOFIX class | In-place | Step 4 |
| `MIGRATION-PLAN.md` at repo root | New file | Step 5 |
| Console summary: `N safe, M autofixed, K manual` | stdout | End |

## Constraints

- **Source-grounded only.** Every finding must trace to a pattern in [`references/express-4-to-5-breaking-changes.md`](references/express-4-to-5-breaking-changes.md). Do not invent breaking changes from memory — Express 5 is the reference, not your training data.
- **No package.json bump in this skill.** The migration plan instructs the team to bump `express` to `^5.0.0`; the skill does not rewrite it. (Reason: bumping invalidates the lockfile and triggers a npm install side-effect; that's a deliberate human gate.)
- **No test runs.** Emitting the plan is the deliverable. The team's CI is the oracle.
- **Idempotent.** Re-running the skill on an already-migrated repo emits `0 findings` and no edits.

## Examples

### Invocation

> "Migrate `services/api/` from Express 4 to Express 5."

### Expected end-state

```
Discovered: services/api/package.json (express ^4.18.2)
Scanned: 14 files
Findings:
SAFE × 2 (informational)
AUTOFIX × 3 → applied
MANUAL × 4 → see MIGRATION-PLAN.md

Wrote: services/api/MIGRATION-PLAN.md
```

## How this was designed

This skill went through the full [Genesis](https://github.com/DevExpGbb/genesis) 8-step process. The handoff packet is in [`references/DESIGN.md`](references/DESIGN.md). Reproducing it for your own framework migration (Next 13→14, React 17→18, etc.):

1. **Step 1 — intent.** Single capability, single framework pair. Don't try to migrate 5 frameworks in one skill.
2. **Step 2 — components.** PIPELINE pattern: scan → classify → autofix → plan. No fan-out, no panel.
3. **Step 5 — Architecture artifacts.** Three files do the heavy lifting: the **catalog** (cited breaking changes) is loaded as context; the **rubric** (SAFE/AUTOFIX/MANUAL classifier) is the decision boundary; the **skill** orchestrates them. The eval runner is the regression harness — change a regex, watch CI.

## Evals

Run against the fixture:

```bash
node .apm/skills/framework-modernizer/evals/run.js
```

The fixture is a deliberate Express-4 mini-app at `.apm/skills/framework-modernizer/evals/fixtures/express4-app/` with **8 known breaking patterns** (3 SAFE, 3 AUTOFIX, 2 MANUAL). The expected findings are checked-in at `evals/expected/findings.txt`. The runner diffs actual vs expected and exits non-zero on mismatch. See [`evals/README.md`](evals/README.md).
38 changes: 38 additions & 0 deletions .agents/skills/framework-modernizer/evals/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Evals — framework-modernizer

Lightweight regression eval that locks the catalog regexes against a deliberate fixture.

## Run

```bash
node .apm/skills/framework-modernizer/evals/run.js
```

Exit `0` = all expected findings matched; exit `1` = drift (catalog regex changed, fixture changed, or expected file out of date). CI-friendly.

## What's here

| Path | Purpose |
|---|---|
| `run.js` | Pure-Node runner. Re-implements catalog regexes line-by-line and diffs vs `expected/findings.txt`. |
| `fixtures/express4-app/server.js` | Deliberate Express 4 mini-app exercising 8 of the 12 catalog patterns (BC-001, BC-002, BC-006, BC-007, BC-101, BC-102, BC-201, BC-202). |
| `fixtures/express4-app/package.json` | Pins `express ^4.18.2` so the fixture is unambiguously v4. |
| `expected/findings.txt` | Ground truth — `BC-ID<TAB>file<TAB>line` rows the runner must reproduce exactly. |

## Why it works this way

- **The catalog is the contract.** Every detection regex in [`../references/express-4-to-5-breaking-changes.md`](../references/express-4-to-5-breaking-changes.md) must have a corresponding entry in `run.js` and (for any pattern exercised by the fixture) a row in `expected/findings.txt`.
- **The fixture is intentionally broken.** Don't "fix" the v4 patterns — the file's whole purpose is to fail v5 detection so the eval has something to assert on.
- **Annotations use `EXPECT-NNN` form**, not the literal pattern, so comments don't trigger false positives in the regex pass.

## Extending — add a new BC-NNN pattern

1. Add the pattern to [`../references/express-4-to-5-breaking-changes.md`](../references/express-4-to-5-breaking-changes.md) with: ID, classification, source citation, detect regex, fix.
2. Add `['BC-NNN', /your-regex/]` to the `PATTERNS` array in `run.js`.
3. Add a triggering example to `fixtures/express4-app/server.js` (annotated `// EXPECT-NNN ...`).
4. Add the expected `BC-NNN<TAB>server.js<TAB><line>` row to `expected/findings.txt`.
5. Run `node .apm/skills/framework-modernizer/evals/run.js` and adjust line numbers if needed.

## Forking the pattern (Next 13→14, React 17→18, etc.)

Same structure, swap the catalog and fixture. See [`../references/DESIGN.md`](../references/DESIGN.md) for the Genesis handoff packet.
10 changes: 10 additions & 0 deletions .agents/skills/framework-modernizer/evals/expected/findings.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Expected findings on the fixture
# Format: BC-ID<TAB>file<TAB>line
BC-201 server.js 8
BC-202 server.js 11
BC-001 server.js 14
BC-006 server.js 20
BC-007 server.js 25
BC-002 server.js 30
BC-101 server.js 34
BC-102 server.js 39
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "express4-app-fixture",
"version": "0.0.1",
"private": true,
"description": "Deliberate Express 4 fixture with 8 known breaking patterns (3 SAFE, 3 AUTOFIX, 2 MANUAL). Used by framework-modernizer eval runner.",
"main": "server.js",
"dependencies": {
"express": "^4.18.2"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Deliberate Express 4 fixture for the framework-modernizer eval suite.
// Comment annotations use the EXPECT-* form to avoid accidental regex hits.

const express = require('express');
const app = express();

// EXPECT-201 SAFE: urlencoded extended default flipped in v5
app.use(express.urlencoded());

// EXPECT-202 SAFE: static dotfiles default flipped in v5
app.use(express.static('public'));

// EXPECT-001 AUTOFIX: app.del removed
app.del('/user/:id', (req, res) => {
res.send(`DELETE /user/${req.params.id}`);
});

// EXPECT-006 AUTOFIX: magic redirect string removed
app.get('/back', (req, res) => {
res.redirect('back');
});

// EXPECT-007 AUTOFIX: lowercase method renamed
app.get('/file', (req, res) => {
res.sendfile(__dirname + '/public/index.html');
});

// EXPECT-002 AUTOFIX: numeric-only send removed
app.get('/notfound', (req, res) => {
res.send(404);
});

// EXPECT-101 MANUAL: unnamed wildcard
app.get('/*', (req, res) => {
res.send('catch-all');
});

// EXPECT-102 MANUAL: optional segment
app.get('/file/:name.:ext?', (req, res) => {
res.send(`name=${req.params.name} ext=${req.params.ext}`);
});

module.exports = app;
98 changes: 98 additions & 0 deletions .agents/skills/framework-modernizer/evals/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#!/usr/bin/env node
/* eslint-disable */
/**
* Eval runner for framework-modernizer.
*
* Validates the catalog regexes against the deliberate fixture by:
* 1. Running each BC-NNN regex from the catalog over every JS file in the fixture
* 2. Emitting actual findings as: BC-ID<TAB>file<TAB>line
* 3. Diffing against evals/expected/findings.txt
*
* Exit 0 on match, 1 on mismatch. CI-friendly. Pure Node, no deps.
*/

const fs = require('node:fs');
const path = require('node:path');

const SKILL_DIR = path.resolve(__dirname, '..');
const FIXTURE_DIR = path.join(SKILL_DIR, 'evals/fixtures/express4-app');
const EXPECTED_FILE = path.join(SKILL_DIR, 'evals/expected/findings.txt');

// Catalog patterns. Must stay in sync with
// references/express-4-to-5-breaking-changes.md.
// Detection regexes are line-by-line (the skill scans similarly).
const PATTERNS = [
['BC-001', /\bapp\.del\s*\(/],
['BC-002', /\bres\.send\s*\(\s*\d{3}\s*\)/],
['BC-006', /\bres\.redirect\s*\(\s*['"]back['"]\s*\)/],
['BC-007', /\bres\.sendfile\s*\(/],
['BC-101', /\.(?:get|post|put|patch|delete|all|use)\s*\(\s*['"][^'"]*\*(?![a-zA-Z_])[^'"]*['"]/],
['BC-102', /\.(?:get|post|put|patch|delete|all|use)\s*\(\s*['"][^'"]*:[a-zA-Z_]\w*\?[^'"]*['"]/],
['BC-201', /express\.urlencoded\s*\(\s*\)/],
['BC-202', /express\.static\s*\(/],
];

const SOURCE_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts']);

function walk(dir) {
const out = [];
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
if (entry.name === 'node_modules') continue;
const full = path.join(dir, entry.name);
if (entry.isDirectory()) out.push(...walk(full));
else if (SOURCE_EXTS.has(path.extname(entry.name))) out.push(full);
}
return out;
}

function scan() {
const findings = [];
for (const file of walk(FIXTURE_DIR)) {
const rel = path.relative(FIXTURE_DIR, file);
const lines = fs.readFileSync(file, 'utf8').split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
for (const [id, regex] of PATTERNS) {
if (regex.test(line)) findings.push({ id, file: rel, line: i + 1 });
}
}
}
return findings;
}

function serialize(findings) {
return findings
.slice()
.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.id.localeCompare(b.id))
.map((f) => `${f.id}\t${f.file}\t${f.line}`)
.join('\n');
}

function loadExpected() {
return fs
.readFileSync(EXPECTED_FILE, 'utf8')
.split(/\r?\n/)
.filter((l) => l && !l.startsWith('#'))
.sort((a, b) => {
const [, fa, la] = a.split('\t');
const [, fb, lb] = b.split('\t');
return fa.localeCompare(fb) || Number(la) - Number(lb);
})
.join('\n');
}

const actual = serialize(scan());
const expected = loadExpected();

if (actual === expected) {
const count = actual.split('\n').filter(Boolean).length;
console.log(`✅ framework-modernizer eval PASSED (${count} findings match expected)`);
process.exit(0);
}

console.log('❌ framework-modernizer eval FAILED');
console.log('--- expected ---');
console.log(expected);
console.log('--- actual ---');
console.log(actual);
process.exit(1);
Loading
Loading