Express to fastify
Skill Siddharth00/agent-revamp-skills/skills/04-migrate/backend/express-to-fastify
Production-grade migration skills for AI coding agents. Revamp any product, module, or feature from one stack to another.
npx -y skills add Siddharth00/agent-revamp-skills --skill express-to-fastifyAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Migrates one Express 4.x route at a time to Fastify 4.x via an in-process strangler-fig proxy, with contract tests verifying identical status codes, response shapes, and headers before each proxy cutover.
SKILL.md
30.4 KB, as published. Nobody here has run it
1. Purpose
This skill migrates an Express 4.x API server to Fastify 4.x one route at a time, using a
strangler-fig approach where an in-process proxy (or nginx upstream block) forwards requests
to either the Express instance or the Fastify instance based on path prefix. Both servers run
in the same Node.js process during the transition — Fastify registers fastify.all('*', expressApp) as a catch-all fallback, so unmitigated routes are silently handled by Express
without a deploy change. The migration is gated per route by contract tests: a supertest
assertion suite runs against Express to capture the behavioral baseline, and a matching
inject() suite runs against Fastify — both must produce identical status codes, response
body shapes, and headers before the proxy is updated for that path. The key risks are not
syntactic but semantic: Express middleware executes in a global ordered stack while Fastify
hooks are scoped per plugin, so a route that relies on a globally-registered Express middleware
(auth, rate-limiting, request-id injection) must have an equivalent hook registered in the
correct Fastify plugin scope. Session handling carries additional risk — @fastify/session
requires explicit cookie and store configuration that Express's express-session defaults
assume implicitly; a mismatch silently issues new sessions instead of resuming existing ones.
2. Trigger Conditions
Use when:
- A Phase 1 audit has produced a complete route inventory for the Express application — every route with its method, path, ordered middleware chain, and whether it reads or writes
req.session. - A Phase 2 migration manifest exists and specifies the order in which routes are migrated (low-risk, stateless, session-free routes first; session-bearing and auth routes last).
- The in-process proxy or nginx upstream is configured and smoke-tested — requests can be shifted per path prefix without a deploy.
- The Fastify instance boots alongside the Express instance in the development environment and responds to
GET /healthzon its port. - The pre-migration
supertestcontract test suite is green on the current commit fortarget_route. @fastify/session,@fastify/cors, and@fastify/helmetare installed and configured — Phase 3 plugin scaffolding is complete.
Do NOT use when:
- The route inventory from Phase 1 is incomplete — migrating routes without a full inventory risks silently dropping Express routes that have no Fastify counterpart.
- Any Express middleware in the global stack has no Fastify equivalent identified — migrate or replicate the middleware in Phase 3 before migrating routes that depend on it.
req.sessionis used bytarget_routeand@fastify/sessionhas not been configured with a matching store, secret, and cookie options — silent session invalidation is worse than a 500.- The Express application uses
app.router(Express 3.x pattern) or custom monkey-patchedresmethods — this skill targets stock Express 4.x only. - The Fastify instance is receiving production traffic — run this skill only in dev and staging until all routes have passed contract tests and a load test.
- The Phase 1 audit flagged
target_routeas having untested error paths — write the missing tests before migrating; the contract tests cannot establish a baseline for behavior that is not tested.
3. Inputs
Required:
| Input | Type | Description |
|---|---|---|
target_route | string | The route to migrate in this invocation, in METHOD /path/pattern format (e.g., POST /api/users). Must match an entry in route_manifest_path. |
repo_root | file-path | Absolute path to the repository root. All commands run from here. |
route_manifest_path | file-path | Path to the route manifest JSON produced by Phase 2 (e.g., output/revamp-spec-routes.json). Contains middleware chains, body shapes, session usage, and error paths per route. |
express_entry | file-path | Repo-root-relative path to the file that creates and exports the Express app instance (e.g., src/app.ts). Used to read global middleware registration order. |
fastify_entry | file-path | Repo-root-relative path to the Fastify server factory (e.g., src/fastify/app.ts). The new route plugin is registered here. |
proxy_config | file-path | Path to the nginx location config or in-process proxy route table. Updated in Step 10 to shift traffic for target_route to Fastify. |
fastify_port | integer | Port the Fastify instance listens on during parallel running (e.g., 3001). Must differ from the Express port. |
Optional:
| Input | Type | Default | Description |
|---|---|---|---|
schema_format | enum(zod, json-schema) | json-schema | How to write request/response schemas. zod requires zod-to-json-schema; json-schema uses native Fastify schema objects. |
dry_run | boolean | false | Analyze and write the proposed route file and contract tests, but do not update the proxy config or register the route in fastify_entry. |
test_timeout_ms | integer | 5000 | Timeout per contract test assertion (both supertest and inject). Increase for routes that call slow external services. |
k6_vus | integer | 50 | Number of virtual users for the p99 latency load test in Step 11. |
k6_duration | string | 30s | Duration of the k6 load test. |
4. Steps
-
Read
route_manifest_path. Locate the entry fortarget_route. Extract and record:middleware_chain: ordered list of middleware names applied globally and per-route (e.g.,[requestId, rateLimit, requireAuth, validateBody]).body_shape: JSON schema or TypeScript type of the expected request body (null for GET/DELETE).response_shape: JSON schema or TypeScript type of the success response body.session_usage: boolean; which session fields are read (request.session.userId) or written.error_paths: list of conditions that trigger non-200 responses and their expected status codes.custom_headers_set: any response headers the handler sets explicitly beyondContent-Type.- If any field is marked
unknownor missing in the manifest: STOP. Ask the user to fill in the manifest entry before proceeding — do not infer.
-
Read
express_entry. Extract the ordered list of globally-registered Express middleware (the sequence ofapp.use(...)calls). Cross-reference withmiddleware_chainfrom Step 1 to identify which global middleware the route implicitly depends on. For each global middleware, confirm a Fastify equivalent is registered infastify_entry(either as a globalfastify.addHookor as a plugin-scoped hook). Write any gap as a blocking item in the migration log — do not proceed past Step 2 if a gap exists. -
Read the Express handler file(s) for
target_route. Identify every use of:next(err)— must becomethrow errin Fastify (orreply.send(err)for explicit control).res.status(N).json(body)— must becomereply.status(N).send(body).req.body.*— body is already parsed by Fastify ifContent-Type: application/json; add a note if the Express handler has a guard for missingContent-Type.req.session.*— maps torequest.session.*under@fastify/session; flag if the session key names differ.res.set(header, value)— maps toreply.header(header, value). Write the translation map to the migration log.
-
Write the Fastify route plugin file. Apply the translation map from Step 3.
Before (Express) — illustrative example for
POST /api/users:// src/routes/users.ts import { Router } from 'express'; import { body, validationResult } from 'express-validator'; import { db } from '../db'; import { requireAuth } from '../middleware/auth'; const router = Router(); router.post( '/api/users', requireAuth, // route-level middleware body('email').isEmail(), body('name').notEmpty().trim(), async (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(422).json({ errors: errors.array() }); } try { const user = await db.user.create({ data: { email: req.body.email, name: req.body.name }, }); res.status(201).json({ id: user.id, email: user.email, name: user.name }); } catch (err) { next(err); // reaches the global Express error handler } } ); export default router;After (Fastify) — translated equivalent:
// src/fastify/routes/users.ts import { FastifyPluginAsync } from 'fastify'; import { z } from 'zod'; import { zodToJsonSchema } from 'zod-to-json-schema'; // omit if schema_format === 'json-schema' import { db } from '../../db'; // Schema replaces the express-validator chain. // Fastify rejects invalid bodies with 400 before the handler runs — no manual validationResult() check. const CreateUserBody = z.object({ email: z.string().email(), name: z.string().min(1).trim(), }); // Response schema enables fast-json-stringify serialization and strips undeclared fields. const UserCreatedReply = z.object({ id: z.string(), email: z.string(), name: z.string(), }); const usersRoutes: FastifyPluginAsync = async (fastify) => { fastify.post<{ Body: z.infer<typeof CreateUserBody>; Reply: z.infer<typeof UserCreatedReply>; }>( '/api/users', { // preHandler runs after global hooks; replaces route-level middleware arrays. // fastify.authenticate must be decorated on the instance — registered in fastify_entry. preHandler: [fastify.authenticate], schema: { body: zodToJsonSchema(CreateUserBody), response: { 201: zodToJsonSchema(UserCreatedReply) }, // Fastify returns 400 (not 422) for schema violations by default. // If Express returned 422, override the error handler or set a custom 400 → 422 mapping. }, }, async (request, reply) => { // request.body is validated — access fields directly. const { email, name } = request.body; // Errors thrown here reach setErrorHandler, not Express's next(err) stack. // Ensure fastify_entry registers a setErrorHandler that returns the same // shape as the Express global error handler for DB errors. const user = await db.user.create({ data: { email, name } }); return reply.status(201).send({ id: user.id, email: user.email, name: user.name }); } ); }; export default usersRoutes;Translation decisions applied above — adapt these for
target_route:Express pattern Fastify equivalent Risk note Route-level middleware array preHandlerarray in route optionsOrder is preserved; verify global hooks don't double-apply express-validatorchainSchema object ( bodykey)Fastify returns 400; Express-validator can return 422 — check contract next(err)throw errFastify passes thrown errors to setErrorHandler; no equivalent to Express's 4-arg error middlewareres.status(N).json(body)reply.status(N).send(body)sendwith an object setsContent-Type: application/jsonautomaticallyreq.body.*request.body.*Fastify requires Content-Type: application/json; Express with body-parser is more lenient — addaddContentTypeParserif clients omit the headerreq.session.*request.session.*Field names must match; cookie options (maxAge, secure) must be re-verified in @fastify/sessionconfigres.set(k, v)reply.header(k, v)Direct rename; no behavior difference - If this fails to compile (
npx tsc --noEmit): check that the Fastify generic type parameters match the schema, and thatfastify.authenticateis declared as a decoration in a.d.tsfile.
- If this fails to compile (
-
Register the new plugin in
fastify_entry:// src/fastify/app.ts (add this line alongside other route registrations) fastify.register(usersRoutes); // or: fastify.register(import('./routes/users'))Run
npx tsc --noEmitfromrepo_root. Confirm 0 errors before proceeding. -
Write a
supertestcontract test fortarget_routeagainst the Express server. The test must assert every item in theerror_pathslist and the success case. Save tosrc/__tests__/contracts/<route-slug>-express.test.ts.Run:
npx jest src/__tests__/contracts/<route-slug>-express.test.ts --testTimeout=<test_timeout_ms>. Must exit 0.- If it fails: STOP. The Express behavior is not fully known. Fix or extend the test until it passes — the Fastify contract test is meaningless without a green Express baseline.
-
Write a matching
inject()contract test fortarget_routeagainst the Fastify server. Use the same inputs and assert the same status codes, body shapes, and headers as the Express test. Save tosrc/__tests__/contracts/<route-slug>-fastify.test.ts.Example structure:
// src/__tests__/contracts/post-api-users-fastify.test.ts import { buildFastify } from '../../fastify/app'; describe('POST /api/users (Fastify)', () => { const app = buildFastify({ logger: false }); beforeAll(() => app.ready()); afterAll(() => app.close()); it('returns 201 with {id, email, name} for valid input', async () => { const res = await app.inject({ method: 'POST', url: '/api/users', payload: { email: '[email protected]', name: 'Alice' }, headers: { 'content-type': 'application/json', authorization: 'Bearer <test-token>' }, }); expect(res.statusCode).toBe(201); expect(res.json()).toMatchObject({ email: '[email protected]', name: 'Alice' }); expect(res.headers['content-type']).toMatch(/application\/json/); }); it('returns 400 for invalid email', async () => { const res = await app.inject({ method: 'POST', url: '/api/users', payload: { email: 'not-an-email', name: 'Alice' }, headers: { 'content-type': 'application/json', authorization: 'Bearer <test-token>' }, }); // NOTE: Fastify schema validation returns 400; Express-validator returned 422. // If the contract test requires 422, add a custom Fastify error handler that maps // FST_ERR_VALIDATION to 422 — document this decision in the migration log. expect(res.statusCode).toBe(400); }); it('returns 401 when authorization header is absent', async () => { const res = await app.inject({ method: 'POST', url: '/api/users', payload: { email: '[email protected]', name: 'Alice' }, headers: { 'content-type': 'application/json' }, }); expect(res.statusCode).toBe(401); }); });Run:
npx jest src/__tests__/contracts/<route-slug>-fastify.test.ts --testTimeout=<test_timeout_ms>. Must exit 0.- If status code differs from Express: check the translation table in Step 4. If the divergence is intentional (e.g., 400 vs 422 for validation), document it explicitly in the migration log and get sign-off before proceeding. Do not silently accept a status code change.
- If body shape differs: re-read the response schema.
fast-json-stringifystrips fields not declared in the schema — if a field is missing from the Fastify response, add it toUserCreatedReply. - If headers differ: check for headers set by global Express middleware (e.g.,
X-Request-Id,X-RateLimit-*) that may not be replicated in the Fastify hook chain.
-
→ Hand off to
equivalence-validator(see Section 5. Agent Handoffs) to diff both contract test outputs side by side and produce a formal divergence report. Wait foroutput/express-to-fastify-divergence-<route-slug>-<timestamp>.mdbefore continuing. If the divergence report contains any item markedBREAKING, resolve it before Step 9. -
If
dry_runistrue: write the migration log with the route file, contract tests, and divergence report, then STOP. Do not update the proxy config. -
Update
proxy_configto route requests matchingtarget_route's path to the Fastify port.nginx example (add inside the
serverblock, before the Express fallback location):# Migrated by skills/04-migrate/backend/express-to-fastify — <timestamp> # Route: POST /api/users location = /api/users { limit_except POST { deny all; } proxy_pass http://127.0.0.1:<fastify_port>; proxy_http_version 1.1; proxy_set_header Connection ""; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Migrated-By fastify; # marker for log filtering }In-process proxy example (update the route table before the Express catch-all):
// src/proxy/routeTable.ts // Prepend; order matters — first match wins. { method: 'POST', path: '/api/users', target: 'fastify' },Reload the proxy config (
nginx -s reloador restart the Node process). Send a test request and confirm the response containsX-Migrated-By: fastify(or equivalent marker). If the marker is absent, the proxy is still routing to Express — check the location block order. -
Deploy to staging. Monitor the error log for 5 minutes:
# Tail the access log and filter for 5xx on the migrated path tail -f <access_log> | grep '<target_path>' | grep -E ' 5[0-9]{2} '- If any 5xx is observed on
target_route: this is the hard rollback trigger. Remove the nginx location block (or revert the route table entry), reload the proxy, confirm Express handles the route again, write the incident to the migration log, STOP: "Rolled back<target_route>— 5xx observed in staging. See migration log for request details." - If no 5xx after 5 minutes: run the p99 latency test (Section 6
p99-latencytest) and record the result.
- If any 5xx is observed on
-
Write all outputs declared in Section 7. Run every Equivalence Test in Section 6 and record results in
output/express-to-fastify-equiv-<timestamp>.md. Evaluate every item in Section 9 Done Criteria; report pass/fail inline, then print the final verdict.
5. Agent Handoffs
equivalence-validator
- File:
agents/equivalence-validator.md - Triggered by: Step 8
- Prompt template:
ORIGINAL: Express handler for <target_route> (src/routes/<route-file>.ts) MIGRATED: Fastify handler for <target_route> (src/fastify/routes/<route-file>.ts) REPO_ROOT: <repo_root> TEST_SUITE: src/__tests__/contracts/<route-slug>-*.test.ts OUTPUT_FILE: output/express-to-fastify-divergence-<route-slug>-<timestamp>.md ENV: staging TASK: Diff the contract test outputs for both suites. For each test case, report whether status codes, response body fields, and response headers match exactly. Mark any divergence as BREAKING (different status code or missing required field) or NON-BREAKING (additional field in Fastify response, cosmetic header difference). Include a VERDICT: PASS | FAIL | INDETERMINATE.
code-archaeologist
- File:
agents/code-archaeologist.md - Triggered by: Step 2 (invoked inline if the middleware gap analysis in Step 2 cannot be completed by reading
express_entryalone — e.g., middleware is registered across multiple files viaapp.use(require('./middleware/...'))) - Prompt template:
TASK: Trace the complete ordered middleware chain for <target_route> in the Express application. Start from <express_entry> and follow every app.use(), router.use(), and route-level middleware reference. For each middleware, report: name, file path, line number, and whether it reads or writes req.session, req.user, req.body, or sets response headers. REPO_ROOT: <repo_root> SCOPE: <repo_root>/src OUTPUT_FILE: output/express-to-fastify-middleware-chain-<route-slug>-<timestamp>.md FORMAT: markdown
6. Equivalence Tests
<!-- All tests are run in Step 12. Results written to output/express-to-fastify-equiv-<timestamp>.md. <route-slug> is the kebab-case version of target_route (e.g., "post-api-users"). -->| Test Name | Input | Expected Output | Tool |
|---|---|---|---|
express-contract | npx jest src/__tests__/contracts/<route-slug>-express.test.ts (Step 6 result) | Exit code 0. All test cases pass. This is the behavioral baseline — if it fails, there is no valid target to match. | Bash — captured in Step 6. |
fastify-contract | npx jest src/__tests__/contracts/<route-slug>-fastify.test.ts (Step 7 result) | Exit code 0. All test cases pass with status codes, body shapes, and headers identical to the Express contract results. | Bash — captured in Step 7. |
contract-divergence-clean | Divergence report at output/express-to-fastify-divergence-<route-slug>-<timestamp>.md (Step 8 result) | Report verdict is PASS. Zero items marked BREAKING. Non-breaking divergences are logged but do not block. | Read — check for VERDICT: PASS and absence of BREAKING. |
no-5xx-in-staging | tail -f <access_log> | grep '<target_path>' | grep -E ' 5[0-9]{2} ' monitored for 5 minutes after Step 11 proxy cutover | Zero matching lines — grep finds no 5xx responses for the migrated path. | Bash — output captured in migration log during Step 11. |
session-parity | Only if session_usage: true in manifest: send authenticated request with a pre-seeded session cookie to both Express (supertest) and Fastify (inject) | Session data read and written identically; Set-Cookie response has same httpOnly, secure, sameSite, and maxAge attributes on both. | Bash: supertest + inject with --cookie flag; diff Set-Cookie headers. |
error-handler-parity | Send a request to target_route that triggers the error handler (e.g., payload causing a DB constraint violation via a test fixture) | Status code and {error, message} body shape are identical between Express and Fastify responses. Fastify setErrorHandler must mirror Express global error handler output. | Bash: supertest + inject with error-triggering payload. |
p99-latency | k6 run --vus <k6_vus> --duration <k6_duration> output/express-to-fastify-k6-<route-slug>.js against Fastify in staging | p99 latency ≤ Express p99 baseline recorded in the migration log. A regression of ≤5ms is acceptable; >5ms warrants investigation before proceeding to the next route. | Bash: k6 output parsed for http_req_duration{p(99)} value. |
7. Outputs
| Artifact | Path Pattern | Format | Description |
|---|---|---|---|
| Middleware gap analysis | output/express-to-fastify-middleware-chain-<route-slug>-<timestamp>.md | markdown | Produced by code-archaeologist if invoked in Step 2. Complete ordered middleware chain for target_route; consumed in Steps 3–4 to verify every middleware has a Fastify hook equivalent. |
| Fastify route file | src/fastify/routes/<route-slug>.ts | TypeScript | The migrated Fastify route plugin. Committed to the repo as a permanent artifact. |
| Express contract test | src/__tests__/contracts/<route-slug>-express.test.ts | TypeScript | Supertest behavioral baseline for target_route. Committed; runs in CI against Express until Express is fully removed. |
| Fastify contract test | src/__tests__/contracts/<route-slug>-fastify.test.ts | TypeScript | inject() assertions mirroring the Express contract. Committed; becomes the permanent test for this route after Express is removed. |
| Divergence report | output/express-to-fastify-divergence-<route-slug>-<timestamp>.md | markdown | Produced by equivalence-validator in Step 8. Side-by-side diff of contract test outputs with BREAKING/NON-BREAKING classification and a VERDICT. |
| k6 load test script | output/express-to-fastify-k6-<route-slug>.js | javascript | k6 script targeting target_route on Fastify in staging. Generated in Step 12 for the p99-latency equivalence test. |
| Migration log | output/express-to-fastify-log-<timestamp>.md | markdown | Translation decisions, middleware gap items, status code divergences, p99 baseline, rollback events (if any), confidence level, and numbered assumptions list. Consumed by /validate and the PR reviewer. |
| Equivalence test results | output/express-to-fastify-equiv-<timestamp>.md | markdown | Pass/fail verdict for every row in Section 6, with raw command output attached. Required by Section 9 Done Criteria. |
8. References
references/strangler-fig-pattern.md— the in-process proxy with Express catch-all is the "Facade / Proxy" mechanism described here; thefastify.all('*', expressApp)pattern is the traffic-shifting seam.references/migration-anti-patterns.md— "Migrating Shared Infrastructure Last" (§6) applies to the auth and session middleware: migrate those in Phase 3, not here. "Confidence Without Evidence" (§7) is why every translation decision in Step 3 must cite a line number.references/stack-compatibility-matrix.md— "Framework Compatibility: Express 4 → Fastify 4" row; check before assuming middleware API parity.skills/03-prepare/— plugin scaffolding (@fastify/session,@fastify/cors,@fastify/helmet), the in-process proxy setup, and the Fastify instance bootstrap must be complete before this skill runs.skills/05-validate/— run/validateafter all routes are migrated and Express is removed; the full contract test suite and a production load test are the final gates.agents/equivalence-validator.md— primary agent in Step 8; review its VERDICT semantics before interpreting the divergence report.agents/code-archaeologist.md— optional agent in Step 2; invoke when middleware is registered across multiple files.https://fastify.dev/docs/latest/Guides/Migration-Guide-V4/— Fastify v4 migration guide; consult for plugin API changes if upgrading from Fastify v3.https://fastify.dev/docs/latest/Reference/Hooks/— hook lifecycle order; essential for correctly scopingpreHandler,onRequest, andonSendequivalents of Express middleware.
9. Done Criteria
<!-- Claude evaluates each item and reports pass/fail before declaring this skill complete. Any unchecked item means the skill is NOT complete for this route invocation. The first four gates (1–4) apply to the final state of the full migration (all routes done). Gates 5–10 are per-invocation and must pass for every individual route before the next route begins. Mandatory universal gates are 11–15. -->- [Final state]
expressis absent frompackage.jsondependencies—grep '"express"' <repo_root>/package.jsonreturns no matches. This gate is only checked after all routes in the manifest have been migrated. Skip and note if this is not the final route. - [Final state] All routes in
route_manifest_pathhave a corresponding entry insrc/fastify/routes/and are registered infastify_entry— globsrc/fastify/routes/*.tsand cross-reference with the manifest; no manifest entry is unmapped. - [Final state] The full contract test suite (
npx jest src/__tests__/contracts/) exits 0 — all Express and Fastify contract tests pass simultaneously in CI. - [Final state] p99 latency for all migrated routes is equal to or better than the Express baseline — all
p99-latencyresults in the migration log show no regression >5ms. -
express-contractequivalence test recorded as pass — the behavioral baseline fortarget_routeis established and green. -
fastify-contractequivalence test recorded as pass — Fastify handler produces identical status codes, body shapes, and headers. -
contract-divergence-cleanequivalence test recorded as pass — divergence report verdict isPASS; zeroBREAKINGitems. -
no-5xx-in-stagingequivalence test recorded as pass — no 5xx observed fortarget_routeduring the 5-minute post-cutover monitoring window. -
error-handler-parityequivalence test recorded as pass — error response shape is identical between Express and Fastify for the error-triggering payload. - If
session_usage: truein manifest:session-parityequivalence test recorded as pass. Ifsession_usage: false, mark this gate N/A and note. - All output files listed in Section 7 exist at their declared paths — verify each with a file read. (The middleware gap analysis is only required if
code-archaeologistwas invoked in Step 2.) - Every equivalence test in Section 6 has a recorded result in
output/express-to-fastify-equiv-<timestamp>.md— no test name is missing from the results file. (Session-parity is marked N/A if not applicable, not absent.) - No equivalence test in Section 6 is recorded as fail — grep the results file for
fail; zero matches required. N/A entries do not count as fail. - The migration log includes a confidence level (High / Medium / Low) — grep
output/express-to-fastify-log-<timestamp>.mdforConfidence:. - The migration log includes a numbered assumptions list — grep
output/express-to-fastify-log-<timestamp>.mdforAssumptions:.