Legacy App (Read-Only)
The existing system being modernized. Never modified by the SDK. Analyzed via static analysis, runtime profiling, and database schema inspection. The Modernizer reads from it but never writes to it.
The ModernizeSpec SDK does not live in your legacy app or your new app. It lives in a separate helper project — the Modernizer App — that orchestrates analysis, parity testing, and progress tracking between the two systems.
Every ModernizeSpec migration involves three distinct codebases:
Legacy App (Read-Only)
The existing system being modernized. Never modified by the SDK. Analyzed via static analysis, runtime profiling, and database schema inspection. The Modernizer reads from it but never writes to it.
Modernizer App (Orchestrator)
A separate project/repo that contains @modernizespec/* packages. Reads the legacy codebase, produces spec JSON files, runs parity tests against both systems, tracks migration progress, and collects runtime profiling data. This is where the SDK lives.
New App (Clean)
The target system being built. Has no ModernizeSpec dependencies. Gets validated against the legacy system via parity tests run by the Modernizer. Stays clean and focused on business logic.
No. Adding the SDK to the legacy system means modifying the codebase you are trying to replace. This introduces risk of side effects, dependency conflicts with old package managers, and muddies the line between “understanding the system” and “changing the system.” The legacy app should remain untouched — a stable reference for parity verification.
No. The new app should have zero awareness of the legacy system. Its job is to implement clean business logic using modern patterns. If the new app imports @modernizespec/sdk, it becomes coupled to the migration tooling — tooling that should be discarded once migration is complete. Keep the new app clean.
Yes. The Modernizer App is a tool, not part of either system. It reads the legacy codebase, validates the new app via parity tests, and tracks progress. When migration is complete, the Modernizer App is archived or deleted. Neither the legacy nor the new app is affected.
The Modernizer App acts as the intermediary between the legacy and new systems. Here is the sequence of operations:
The Modernizer reads the legacy source code and produces spec files:
npx modernizespec analyze --legacy-path ../legacy-appThis generates domains.json, complexity.json, and other spec files based on static analysis of the legacy codebase.
Using the spec files as a guide, the team builds the equivalent modules in the new app. The Modernizer does not generate code — it provides the map. The new app is built independently.
The Modernizer records how the legacy system responds to requests:
import { captureEndpoint } from '@modernizespec/sdk';
// Record legacy behavior as baselineconst baseline = await captureEndpoint("GET /api/invoices/123", { target: "legacy", outputDir: "./baselines/invoices/"});Baselines are stored in the baselines/ directory as JSON snapshots.
The Modernizer sends the same requests to both systems and compares:
import { compareBehavior } from '@modernizespec/sdk';
const diff = compareBehavior(baseline, modern, { ignoreFields: ["timestamp", "request_id"], tolerance: { amount: 0.01 }});Results update parity-tests.json automatically.
The Modernizer ingests runtime data (traces, metrics) from the legacy system and produces runtime-profile.json. This reveals hot paths, dead code, and database query patterns.
As contexts are extracted and parity tests pass, the Modernizer updates migration-state.json with current progress, velocity metrics, and blockers.
Parity checking is the mechanism that proves the new system behaves identically to the legacy system. The Modernizer App orchestrates this in five phases.
Run requests against the legacy system and record responses:
import { captureEndpoint } from '@modernizespec/sdk';
const baseline = await captureEndpoint("GET /api/invoices/123", { target: "legacy", outputDir: "./baselines/invoices/"});
// baseline contains: status, headers, body, timingImplement the same endpoint in the new app. The new app has no awareness of baselines — it just implements the business logic.
Run the same request against the new system and diff the responses:
import { captureEndpoint, compareBehavior } from '@modernizespec/sdk';
// Capture from new systemconst modern = await captureEndpoint("GET /api/v2/invoices/123", { target: "new"});
// Diff field-by-fieldconst diff = compareBehavior(baseline, modern, { ignoreFields: ["timestamp", "request_id", "server_version"], tolerance: { amount: 0.01 }});Table-driven tests verify field-level parity:
import { assertParity } from '@modernizespec/sdk';
// Fails if any field diverges beyond toleranceassertParity(diff, { strictFields: ["invoice_number", "line_items", "tax_amount"], toleranceFields: { total: 0.01, discount: 0.005 }, ignoredFields: ["created_at", "updated_at"]});Results flow back into the spec files:
import { updateParityResults } from '@modernizespec/sdk';
await updateParityResults("./agents/modernization/parity-tests.json", { context: "Invoices", passed: 42, failed: 3, skipped: 0, timestamp: new Date().toISOString()});Runtime profiling adds evidence from the live legacy system. Instead of guessing which code paths matter, you measure them.
Legacy App (instrumented) → APM / OpenTelemetry → Export → Modernizer → runtime-profile.jsonprofiling/ directoryruntime-profile.jsonimport { processTraces } from '@modernizespec/sdk';
const profile = await processTraces({ tracesDir: "./profiling/traces/", outputFile: "./.agents/modernization/runtime-profile.json", period: { start: "2026-01-01", end: "2026-01-31" }});| Data Point | Impact on Migration |
|---|---|
| Hot paths (high call count) | Prioritize these for parity testing |
| Dead code (zero calls in production) | Skip extraction — migrate last or never |
| N+1 query patterns | Fix during extraction, not before |
| Slow endpoints | Opportunity for improvement in new system |
| Resource usage spikes | Capacity planning for the new system |
Runtime profile data influences other spec files:
See runtime-profile.json for the full file specification.
Create a Modernizer App alongside your existing project:
mkdir my-modernizer && cd my-modernizernpm init -ynpm install @modernizespec/sdk @modernizespec/clinpx modernizespec init --legacy-path ../legacy-appThis scaffolds the directory structure, analyzes the legacy codebase, and produces initial spec files. From here, iterate: capture baselines, build new modules, run parity tests, track progress.
.agents/modernization/ directory structure