mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-06-13 22:26:03 -03:00
code-modernization: interactive topology map, preflight command, persona flows
modernize-map previously rendered the call graph and data lineage as static Mermaid diagrams, which become unreadable once a node has ~10+ edges — exactly the shape of real legacy systems. It now builds an interactive viewer from a shipped template (assets/topology-viewer.html): a zoomable circle-pack of domains/modules sized by LOC, rendered to canvas with level-of-detail reveal, dependency edges with per-kind toggles, search with fly-to, a per-node detail sidebar, and a flow walkthrough mode. Small domain-level .mmd exports remain for docs. - topology.json now has a documented schema (hierarchy + edges + entry points + observations + flows) consumed by the viewer - map traces 2-4 business flows anchored to personas (claimant, operator, auditor), each step in plain business language mapped to the modules that implement it; the viewer plays them as numbered paths - brief gains a Business Walkthroughs section connecting each persona flow to the phase that replaces it - new modernize-preflight command: detects the stack, checks analysis tooling, smoke-compiles a real source file with the legacy toolchain, inventories missing copybooks/descriptors/binary-only artifacts, and writes a per-command readiness verdict - transform now verifies legacy + target toolchains before its plan gate instead of failing at test time - README: commands updated, optional-tooling section reframed as 'what to give Claude'
This commit is contained in:
parent
bbbff6ab54
commit
1c4a5cfded
@ -7,7 +7,7 @@ A structured workflow and set of specialist agents for modernizing legacy codeba
|
||||
Legacy modernization fails most often not because the target technology is wrong, but because teams skip steps: they transform code before understanding it, reimagine architecture before extracting business rules, or ship without a harness that would catch behavior drift. This plugin enforces a sequence:
|
||||
|
||||
```
|
||||
assess → map → extract-rules → brief → reimagine | transform → harden
|
||||
preflight → assess → map → extract-rules → brief → reimagine | transform → harden
|
||||
```
|
||||
|
||||
The discovery commands (`assess`, `map`, `extract-rules`) build artifacts under `analysis/<system>/`. The `brief` command synthesizes them into an approval gate. The build commands (`reimagine`, `transform`) write new code under `modernized/`. The `harden` command audits the legacy system and produces a reviewable remediation patch. Each step has a dedicated slash command, and specialist agents (legacy analyst, business rules extractor, architecture critic, security auditor, test engineer) are invoked from within those commands — or directly — to keep the work honest.
|
||||
@ -20,25 +20,33 @@ Commands take a `<system-dir>` argument and assume the system being modernized l
|
||||
mkdir -p legacy && ln -s /path/to/your/legacy/codebase legacy/billing
|
||||
```
|
||||
|
||||
## Optional tooling
|
||||
## What to give Claude
|
||||
|
||||
`/modernize-assess` works best with [`scc`](https://github.com/boyter/scc) (LOC + complexity + COCOMO) or [`cloc`](https://github.com/AlDanial/cloc), and falls back to `find`/`wc` if neither is installed. Portfolio mode also benefits from [`lizard`](https://github.com/terryyin/lizard) (cyclomatic complexity). The commands degrade gracefully without them, but the metrics will be coarser.
|
||||
The commands degrade gracefully, but each of these makes the output meaningfully better — run `/modernize-preflight <system-dir>` to check all of them at once and get a readiness report:
|
||||
|
||||
- **Analysis tools**: [`scc`](https://github.com/boyter/scc) (LOC + complexity + COCOMO) or [`cloc`](https://github.com/AlDanial/cloc); [`lizard`](https://github.com/terryyin/lizard) for portfolio mode. Without them, metrics fall back to `find`/`wc` and get coarser.
|
||||
- **A working build toolchain** for the legacy stack (e.g. GnuCOBOL for COBOL) — required before `/modernize-transform` can prove behavioral equivalence, and verified by preflight with a real smoke compile against your code.
|
||||
- **The whole system in the tree**: deployment descriptors (JCL, CICS definitions, route configs), copybooks/includes, and DDL/schemas. Entry-point detection and data lineage in `/modernize-map` are guesswork without them.
|
||||
- **Production telemetry** (optional): an observability MCP server or batch job logs enable the runtime overlay in `/modernize-assess` and timing annotations on critical paths.
|
||||
|
||||
## Commands
|
||||
|
||||
The commands are designed to be run in order, but each produces a standalone artifact so you can stop, review, and resume.
|
||||
|
||||
### `/modernize-preflight <system-dir> [target-stack]`
|
||||
Environment readiness check, meant to run first: detects the legacy stack, checks analysis tooling, **smoke-compiles a real source file** with the legacy toolchain (the errors this surfaces — missing copybooks, wrong dialect flags — are the ones that otherwise appear mid-transform), inventories missing includes / deployment descriptors / binary-only artifacts, and probes for telemetry. Produces `analysis/<system>/PREFLIGHT.md` with a per-command Ready / Ready-with-gaps / Not-ready verdict.
|
||||
|
||||
### `/modernize-assess <system-dir>` — or — `/modernize-assess --portfolio <parent-dir>`
|
||||
Inventory the legacy codebase: languages, line counts, complexity, build system, integrations, technical debt, security posture, documentation gaps, and a COCOMO-derived effort estimate. Produces `analysis/<system>/ASSESSMENT.md` and `analysis/<system>/ARCHITECTURE.mmd`. Spawns `legacy-analyst` (×2) and `security-auditor` in parallel for deep reads. With `--portfolio`, sweeps every subdirectory of a parent directory and writes a sequencing heat-map to `analysis/portfolio.html`.
|
||||
|
||||
### `/modernize-map <system-dir>`
|
||||
Build a dependency and topology map of the **legacy** system: program/module call graph, data lineage (programs ↔ data stores), entry points, dead-end candidates, and one traced critical-path business flow. Writes a re-runnable extraction script and produces `analysis/<system>/topology.json` (machine-readable), `analysis/<system>/TOPOLOGY.html` (rendered Mermaid + architect observations), and standalone `call-graph.mmd`, `data-lineage.mmd`, and `critical-path.mmd`.
|
||||
Build a dependency and topology map of the **legacy** system: program/module call graph, data lineage (programs ↔ data stores), entry points, dead-end candidates, and 2–4 traced business flows each anchored to a persona (the claimant, the operator, the auditor — not the maintainer). Writes a re-runnable extraction script and produces `analysis/<system>/topology.json` plus `analysis/<system>/TOPOLOGY.html` — an **interactive zoomable map** (circle-pack of domains/modules sized by LOC, dependency edges with per-kind toggles, search, click-for-details sidebar, and a walkthrough mode that plays each persona flow as a numbered path with a plain-language narrative). Built from a template shipped with the plugin, so it works on systems far too dense for a static diagram. Small domain-level `call-graph.mmd`, `data-lineage.mmd`, and `critical-path.mmd` are still exported for docs and PRs.
|
||||
|
||||
### `/modernize-extract-rules <system-dir> [module-pattern]`
|
||||
Mine the business rules embedded in the legacy code — calculations, validations, eligibility, state transitions, policies — into Given/When/Then "Rule Cards" with `file:line` citations and confidence ratings. Spawns three `business-rules-extractor` agents in parallel (calculations, validations, lifecycle). Produces `analysis/<system>/BUSINESS_RULES.md` and `analysis/<system>/DATA_OBJECTS.md`.
|
||||
|
||||
### `/modernize-brief <system-dir> [target-stack]`
|
||||
Synthesize the discovery artifacts into a phased **Modernization Brief** — the single document a steering committee approves and engineering executes: target architecture, strangler-fig phase plan with entry/exit criteria, behavior contract, validation strategy, open questions, and an approval block. Reads `ASSESSMENT.md`, `TOPOLOGY.html`, and `BUSINESS_RULES.md` and **stops if any are missing** — run the discovery commands first. Produces `analysis/<system>/MODERNIZATION_BRIEF.md` and enters plan mode as a human-in-the-loop gate.
|
||||
Synthesize the discovery artifacts into a phased **Modernization Brief** — the single document a steering committee approves and engineering executes: target architecture, strangler-fig phase plan with entry/exit criteria, persona-based business walkthroughs (the section non-technical approvers actually read), behavior contract, validation strategy, open questions, and an approval block. Reads `ASSESSMENT.md`, `TOPOLOGY.html`, and `BUSINESS_RULES.md` and **stops if any are missing** — run the discovery commands first. Produces `analysis/<system>/MODERNIZATION_BRIEF.md` and enters plan mode as a human-in-the-loop gate.
|
||||
|
||||
### `/modernize-reimagine <system-dir> <target-vision>`
|
||||
Greenfield rebuild from extracted intent rather than a structural port. Mines a spec (`analysis/<system>/AI_NATIVE_SPEC.md`), designs a target architecture and has it adversarially reviewed (`analysis/<system>/REIMAGINED_ARCHITECTURE.md`), then **scaffolds services with executable acceptance tests** under `modernized/<system>-reimagined/` and writes a `CLAUDE.md` knowledge handoff for the new system. Two human-in-the-loop checkpoints. Spawns `business-rules-extractor`, `legacy-analyst` (×2), `architecture-critic`, and general-purpose scaffolding agents.
|
||||
|
||||
454
plugins/code-modernization/assets/topology-viewer.html
Normal file
454
plugins/code-modernization/assets/topology-viewer.html
Normal file
@ -0,0 +1,454 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>System topology</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #1e1e1e; --panel: #252526; --panel-2: #2d2d2d;
|
||||
--text: #d4d4d4; --muted: #8a8a8a; --accent: #cc785c;
|
||||
--border: #3a3a3a; --blue: #5ba0e0; --green: #6bb85a;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; }
|
||||
html, body { height: 100%; overflow: hidden; background: var(--bg);
|
||||
color: var(--text); font: 14px/1.45 system-ui, sans-serif; }
|
||||
#map { position: absolute; inset: 0; cursor: grab; }
|
||||
#map.dragging { cursor: grabbing; }
|
||||
|
||||
#hud { position: absolute; top: 12px; left: 12px; width: 280px;
|
||||
display: flex; flex-direction: column; gap: 8px; z-index: 2; }
|
||||
.panel { background: var(--panel); border: 1px solid var(--border);
|
||||
border-radius: 8px; padding: 10px 12px; }
|
||||
.panel h1 { font-size: 15px; color: var(--accent); margin-bottom: 2px; }
|
||||
.panel .sub { color: var(--muted); font-size: 12px; }
|
||||
#search { width: 100%; padding: 6px 8px; border-radius: 6px;
|
||||
border: 1px solid var(--border); background: var(--panel-2);
|
||||
color: var(--text); font: inherit; outline: none; }
|
||||
#search:focus { border-color: var(--accent); }
|
||||
#results { max-height: 180px; overflow-y: auto; margin-top: 4px; }
|
||||
#results div { padding: 4px 6px; border-radius: 4px; cursor: pointer;
|
||||
font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
#results div:hover { background: var(--panel-2); }
|
||||
#results .kind { color: var(--muted); font-size: 11px; margin-left: 6px; }
|
||||
|
||||
.toggles label { display: flex; align-items: center; gap: 7px;
|
||||
font-size: 13px; padding: 2px 0; cursor: pointer; }
|
||||
.swatch { width: 14px; height: 3px; border-radius: 2px; display: inline-block; }
|
||||
#flows select { width: 100%; margin-top: 4px; padding: 5px;
|
||||
background: var(--panel-2); color: var(--text);
|
||||
border: 1px solid var(--border); border-radius: 6px; font: inherit; }
|
||||
|
||||
#sidebar { position: absolute; top: 12px; right: 12px; bottom: 12px;
|
||||
width: 320px; overflow-y: auto; z-index: 2; display: none; }
|
||||
#sidebar.open { display: block; }
|
||||
#sidebar h2 { font-size: 16px; word-break: break-word; }
|
||||
#sidebar .meta { color: var(--muted); font-size: 12px; margin: 2px 0 10px; }
|
||||
#sidebar h3 { font-size: 12px; text-transform: uppercase; letter-spacing: .05em;
|
||||
color: var(--muted); margin: 12px 0 4px; }
|
||||
#sidebar .link { color: var(--blue); cursor: pointer; display: block;
|
||||
padding: 2px 0; font-size: 13px; white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; }
|
||||
#sidebar .link:hover { color: var(--accent); }
|
||||
#sidebar ol { padding-left: 20px; } #sidebar ol li { margin: 6px 0; font-size: 13px; }
|
||||
#sidebar .closebtn { float: right; cursor: pointer; color: var(--muted);
|
||||
font-size: 18px; line-height: 1; } #sidebar .closebtn:hover { color: var(--text); }
|
||||
.badge { display: inline-block; padding: 1px 7px; border-radius: 9px;
|
||||
font-size: 11px; border: 1px solid var(--border); color: var(--muted); margin-right: 4px; }
|
||||
.badge.entry { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
#hint { position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
|
||||
color: var(--muted); font-size: 12px; z-index: 2; background: var(--panel);
|
||||
border: 1px solid var(--border); border-radius: 6px; padding: 4px 12px; }
|
||||
#err { position: absolute; inset: 0; display: none; place-items: center; z-index: 9; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="map"></canvas>
|
||||
|
||||
<div id="hud">
|
||||
<div class="panel">
|
||||
<h1 id="title">System topology</h1>
|
||||
<div class="sub" id="stats"></div>
|
||||
</div>
|
||||
<div class="panel">
|
||||
<input id="search" type="search" placeholder="Search modules, stores, jobs…" autocomplete="off">
|
||||
<div id="results"></div>
|
||||
</div>
|
||||
<div class="panel toggles" id="toggles"></div>
|
||||
<div class="panel" id="flows" style="display:none">
|
||||
<div class="sub">Business flow walkthrough</div>
|
||||
<select id="flowSel"><option value="">— none —</option></select>
|
||||
</div>
|
||||
<details class="panel" id="obs" style="display:none">
|
||||
<summary style="cursor:pointer;color:var(--muted);font-size:12px">Architect observations</summary>
|
||||
<ul id="obsList" style="padding-left:18px;margin-top:6px;font-size:12.5px"></ul>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
<aside id="sidebar" class="panel"></aside>
|
||||
<div id="hint">scroll to zoom · drag to pan · click a node · double-click to zoom in · Esc to reset</div>
|
||||
<div id="err" class="panel"><p>No topology data found in this file.<br>
|
||||
Re-run <code>/modernize-map</code> to regenerate it.</p></div>
|
||||
|
||||
<script type="module">
|
||||
import * as d3 from "https://cdn.jsdelivr.net/npm/d3@7/+esm";
|
||||
|
||||
// Injected by /modernize-map: the contents of topology.json replace the
|
||||
// null after the marker comment. Schema documented in the command file.
|
||||
const DATA = /*__TOPOLOGY_DATA__*/ null;
|
||||
|
||||
if (!DATA || !DATA.root) {
|
||||
document.getElementById("err").style.display = "grid";
|
||||
throw new Error("no data");
|
||||
}
|
||||
|
||||
// ── Layout: circle-pack in a fixed world space ─────────────────────────
|
||||
// d3.pack gives every node (x, y, r) once; zooming is purely a canvas
|
||||
// transform, so the layout never recomputes and pan/zoom stays at 60fps.
|
||||
const WORLD = 8000;
|
||||
// Leaves without a meaningful LOC (data stores, screens, jobs) get a floor
|
||||
// based on the median module size so they stay visible next to code.
|
||||
const locs = [];
|
||||
(function walk(d) { if (d.children && d.children.length) d.children.forEach(walk);
|
||||
else if (d.loc > 1) locs.push(d.loc); })(DATA.root);
|
||||
locs.sort((a, b) => a - b);
|
||||
const FLOOR = Math.max(1, Math.round((locs[Math.floor(locs.length / 2)] || 100) * 0.4));
|
||||
const root = d3.hierarchy(DATA.root, d => d.children)
|
||||
.sum(d => (d.children && d.children.length ? 0 : Math.max(d.loc || 0, d.kind === "module" ? 1 : FLOOR)))
|
||||
.sort((a, b) => b.value - a.value);
|
||||
d3.pack().size([WORLD, WORLD]).padding(d => Math.max(3, 24 / (d.depth + 1)))(root);
|
||||
|
||||
const nodes = [];
|
||||
const byId = new Map();
|
||||
root.each(d => {
|
||||
const n = { id: d.data.id, name: d.data.name, kind: d.data.kind || "module",
|
||||
language: d.data.language || null, file: d.data.file || null,
|
||||
loc: d.data.loc || 0, x: d.x, y: d.y, r: d.r, depth: d.depth,
|
||||
parent: d.parent ? d.parent.data.id : null,
|
||||
container: !!(d.children && d.children.length) };
|
||||
nodes.push(n); byId.set(n.id, n);
|
||||
});
|
||||
nodes.sort((a, b) => b.r - a.r); // painters: parents under children
|
||||
|
||||
const entrySet = new Set(DATA.entryPoints || []);
|
||||
const edges = (DATA.edges || []).filter(e => byId.has(e.source) && byId.has(e.target));
|
||||
const fanIn = new Map(), fanOut = new Map();
|
||||
for (const e of edges) {
|
||||
fanOut.set(e.source, (fanOut.get(e.source) || 0) + 1);
|
||||
fanIn.set(e.target, (fanIn.get(e.target) || 0) + 1);
|
||||
}
|
||||
|
||||
// ── Colors ──────────────────────────────────────────────────────────────
|
||||
const KIND_FILL = { system: "#1e1e1e", domain: null, module: "#4a6580",
|
||||
datastore: "#3d6a5a", job: "#7a5d8a", screen: "#8a6d4a" };
|
||||
const LANG_FILL = { cobol: "#5e7a9e", java: "#9a6848", c: "#6e6a58",
|
||||
"c++": "#6e6a58", python: "#5a7850", javascript: "#9a8348",
|
||||
typescript: "#4a6580", csharp: "#5f7a5f", rpg: "#7d6580", pl1: "#806858",
|
||||
natural: "#587a78", sql: "#3d6a5a" };
|
||||
const EDGE_STYLE = {
|
||||
call: { color: "rgba(201,182,144,0.30)", label: "calls" },
|
||||
dispatch: { color: "rgba(160,130,200,0.35)", label: "dynamic dispatch" },
|
||||
read: { color: "rgba(91,160,224,0.30)", label: "reads" },
|
||||
write: { color: "rgba(224,122,82,0.40)", label: "writes" },
|
||||
};
|
||||
function fillFor(n) {
|
||||
if (n.kind === "domain" || (n.container && n.kind !== "system"))
|
||||
return n.depth % 2 ? "#2d2d2d" : "#262626";
|
||||
if (n.kind !== "module" && KIND_FILL[n.kind]) return KIND_FILL[n.kind];
|
||||
return (n.language && LANG_FILL[n.language.toLowerCase()]) || "#4a6580";
|
||||
}
|
||||
|
||||
// ── State ───────────────────────────────────────────────────────────────
|
||||
const canvas = document.getElementById("map");
|
||||
const ctx = canvas.getContext("2d");
|
||||
let transform = d3.zoomIdentity;
|
||||
let hovered = null, selected = null, activeFlow = null;
|
||||
const kindOn = Object.fromEntries(Object.keys(EDGE_STYLE).map(k => [k, true]));
|
||||
|
||||
// ── Render ──────────────────────────────────────────────────────────────
|
||||
const MIN_R = 1.5, LABEL_R = 22, REVEAL_R = 55;
|
||||
let raf = null;
|
||||
const requestDraw = () => { if (!raf) raf = requestAnimationFrame(() => { raf = null; draw(); }); };
|
||||
|
||||
function visible(n, k, tx, ty, vw, vh) {
|
||||
const sx = n.x * k + tx, sy = n.y * k + ty, sr = n.r * k;
|
||||
return sx + sr > -60 && sx - sr < vw + 60 && sy + sr > -60 && sy - sr < vh + 60;
|
||||
}
|
||||
|
||||
function flowNodeSet() {
|
||||
if (!activeFlow) return null;
|
||||
const s = new Set();
|
||||
for (const step of activeFlow.steps) for (const id of step.nodes || []) s.add(id);
|
||||
return s;
|
||||
}
|
||||
|
||||
function draw() {
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const vw = innerWidth, vh = innerHeight;
|
||||
if (canvas.width !== vw * dpr) { canvas.width = vw * dpr; canvas.style.width = vw + "px"; }
|
||||
if (canvas.height !== vh * dpr) { canvas.height = vh * dpr; canvas.style.height = vh + "px"; }
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.fillStyle = "#1e1e1e"; ctx.fillRect(0, 0, vw, vh);
|
||||
|
||||
const { x: tx, y: ty, k } = transform;
|
||||
const flowSet = flowNodeSet();
|
||||
ctx.textAlign = "center"; ctx.textBaseline = "middle";
|
||||
|
||||
// Nodes
|
||||
for (const n of nodes) {
|
||||
const sr = n.r * k;
|
||||
if (sr < MIN_R) continue;
|
||||
if (n.parent) {
|
||||
const p = byId.get(n.parent);
|
||||
if (p && p.r * k < REVEAL_R) continue; // LOD: reveal children gradually
|
||||
}
|
||||
if (!visible(n, k, tx, ty, vw, vh)) continue;
|
||||
const sx = n.x * k + tx, sy = n.y * k + ty;
|
||||
const inFlow = flowSet ? flowSet.has(n.id) : true;
|
||||
const isSel = selected === n.id, isHov = hovered === n.id;
|
||||
|
||||
ctx.beginPath(); ctx.arc(sx, sy, sr, 0, Math.PI * 2);
|
||||
ctx.globalAlpha = (n.container ? 0.55 : 0.85) * (inFlow ? 1 : 0.18);
|
||||
ctx.fillStyle = isSel ? "rgba(204,120,92,0.25)" : fillFor(n);
|
||||
ctx.fill(); ctx.globalAlpha = 1;
|
||||
ctx.lineWidth = isSel ? 2.5 : isHov ? 1.8 : n.container ? 1 : 0.8;
|
||||
ctx.strokeStyle = isSel || isHov ? "#cc785c"
|
||||
: entrySet.has(n.id) ? "rgba(204,120,92,0.9)"
|
||||
: n.container ? "#3a3a3a" : "rgba(212,212,212,0.18)";
|
||||
if (entrySet.has(n.id) && !isSel && !isHov) ctx.lineWidth = 1.6;
|
||||
ctx.globalAlpha = inFlow ? 1 : 0.25; ctx.stroke(); ctx.globalAlpha = 1;
|
||||
|
||||
if (sr > LABEL_R) {
|
||||
const fs = Math.min(17, Math.max(9, sr / 6));
|
||||
ctx.font = `${n.container ? "600" : "400"} ${fs}px system-ui, sans-serif`;
|
||||
ctx.fillStyle = n.container ? "#8a8a8a" : "#e8e8e8";
|
||||
ctx.globalAlpha = inFlow ? 1 : 0.3;
|
||||
const ly = n.container && sr > 55 ? sy - sr + fs + 5 : sy;
|
||||
const label = n.name.length > 28 ? n.name.slice(0, 26) + "…" : n.name;
|
||||
ctx.fillText(label, sx, ly);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Edges: batched per kind, endpoints must both be on screen
|
||||
const vis = id => { const n = byId.get(id); return n && n.r * k > MIN_R * 2 && visible(n, k, tx, ty, vw, vh); };
|
||||
const curve = (a, b) => {
|
||||
const ax = a.x * k + tx, ay = a.y * k + ty, bx = b.x * k + tx, by = b.y * k + ty;
|
||||
if ((ax - bx) ** 2 + (ay - by) ** 2 < 120) return;
|
||||
ctx.moveTo(ax, ay);
|
||||
ctx.quadraticCurveTo((ax + bx) / 2 + (by - ay) * 0.12, (ay + by) / 2 + (ax - bx) * 0.12, bx, by);
|
||||
};
|
||||
if (!flowSet) {
|
||||
for (const [kind, style] of Object.entries(EDGE_STYLE)) {
|
||||
if (!kindOn[kind]) continue;
|
||||
ctx.strokeStyle = style.color; ctx.lineWidth = 0.8; ctx.beginPath();
|
||||
let drawn = 0;
|
||||
for (const e of edges) {
|
||||
if (e.kind !== kind || !vis(e.source) || !vis(e.target)) continue;
|
||||
curve(byId.get(e.source), byId.get(e.target));
|
||||
if (++drawn > 1200) break;
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
// Focus edges (hover/selection) in accent, on top
|
||||
const focus = hovered || selected;
|
||||
if (focus && !flowSet) {
|
||||
ctx.strokeStyle = "#cc785c"; ctx.lineWidth = 1.5; ctx.beginPath();
|
||||
for (const e of edges) {
|
||||
if (e.source !== focus && e.target !== focus) continue;
|
||||
const a = byId.get(e.source), b = byId.get(e.target);
|
||||
if (a && b) curve(a, b);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
// Active flow: numbered step path
|
||||
if (flowSet) {
|
||||
ctx.strokeStyle = "#cc785c"; ctx.lineWidth = 2;
|
||||
const seq = activeFlow.steps.flatMap(s => s.nodes || []);
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i + 1 < seq.length; i++) {
|
||||
const a = byId.get(seq[i]), b = byId.get(seq[i + 1]);
|
||||
if (a && b) curve(a, b);
|
||||
}
|
||||
ctx.stroke();
|
||||
seq.forEach((id, i) => {
|
||||
const n = byId.get(id); if (!n) return;
|
||||
const sx = n.x * k + tx, sy = n.y * k + ty - n.r * k - 10;
|
||||
ctx.beginPath(); ctx.arc(sx, sy, 9, 0, Math.PI * 2);
|
||||
ctx.fillStyle = "#cc785c"; ctx.fill();
|
||||
ctx.fillStyle = "#1e1e1e"; ctx.font = "700 11px system-ui, sans-serif";
|
||||
ctx.fillText(String(i + 1), sx, sy + 0.5);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Zoom / pan / hit-testing ────────────────────────────────────────────
|
||||
const zoomB = d3.zoom().scaleExtent([0.05, 600])
|
||||
.on("zoom", ev => { transform = ev.transform; requestDraw(); })
|
||||
.on("start", () => canvas.classList.add("dragging"))
|
||||
.on("end", () => canvas.classList.remove("dragging"));
|
||||
const sel = d3.select(canvas).call(zoomB).on("dblclick.zoom", null);
|
||||
|
||||
function hit(mx, my) {
|
||||
const wx = (mx - transform.x) / transform.k, wy = (my - transform.y) / transform.k;
|
||||
let best = null;
|
||||
for (const n of nodes) {
|
||||
if (n.r * transform.k < MIN_R) continue;
|
||||
const dx = wx - n.x, dy = wy - n.y;
|
||||
if (dx * dx + dy * dy <= n.r * n.r && (!best || n.r < best.r)) best = n;
|
||||
}
|
||||
return best;
|
||||
}
|
||||
function flyTo(n) {
|
||||
const vw = innerWidth, vh = innerHeight, cur = transform;
|
||||
const from = [(vw / 2 - cur.x) / cur.k, (vh / 2 - cur.y) / cur.k, Math.min(vw, vh) / cur.k];
|
||||
const interp = d3.interpolateZoom(from, [n.x, n.y, n.r * 2.6]);
|
||||
const dur = Math.max(400, interp.duration * 0.55), t0 = performance.now();
|
||||
(function step() {
|
||||
const t = Math.min(1, (performance.now() - t0) / dur);
|
||||
const [cx, cy, w] = interp(d3.easeCubicInOut(t));
|
||||
const k = Math.min(vw, vh) / w;
|
||||
sel.call(zoomB.transform, d3.zoomIdentity.translate(vw / 2 - cx * k, vh / 2 - cy * k).scale(k));
|
||||
if (t < 1) requestAnimationFrame(step);
|
||||
})();
|
||||
}
|
||||
canvas.addEventListener("mousemove", e => {
|
||||
const h = hit(e.offsetX, e.offsetY);
|
||||
const id = h ? h.id : null;
|
||||
if (id !== hovered) { hovered = id; canvas.style.cursor = h ? "pointer" : "grab"; requestDraw(); }
|
||||
});
|
||||
canvas.addEventListener("click", e => {
|
||||
const h = hit(e.offsetX, e.offsetY);
|
||||
selected = h ? h.id : null;
|
||||
renderSidebar(); requestDraw();
|
||||
});
|
||||
canvas.addEventListener("dblclick", e => { const h = hit(e.offsetX, e.offsetY); if (h) flyTo(h); });
|
||||
addEventListener("keydown", e => {
|
||||
if (e.key === "Escape") { selected = null; setFlow(""); flyTo(byId.get(DATA.root.id)); renderSidebar(); }
|
||||
});
|
||||
addEventListener("resize", requestDraw);
|
||||
|
||||
// ── Sidebar ─────────────────────────────────────────────────────────────
|
||||
const sidebar = document.getElementById("sidebar");
|
||||
const esc = s => String(s).replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
function renderSidebar() {
|
||||
if (activeFlow) return renderFlowSidebar();
|
||||
if (!selected) { sidebar.classList.remove("open"); return; }
|
||||
const n = byId.get(selected);
|
||||
const neighbors = [];
|
||||
for (const e of edges) {
|
||||
if (e.source === n.id) neighbors.push({ id: e.target, rel: EDGE_STYLE[e.kind]?.label || e.kind, dir: "→" });
|
||||
else if (e.target === n.id) neighbors.push({ id: e.source, rel: EDGE_STYLE[e.kind]?.label || e.kind, dir: "←" });
|
||||
}
|
||||
sidebar.innerHTML = `
|
||||
<span class="closebtn" id="close">×</span>
|
||||
<h2>${esc(n.name)}</h2>
|
||||
<div class="meta">${esc(n.kind)}${n.language ? " · " + esc(n.language) : ""}${n.loc ? " · " + n.loc.toLocaleString() + " LOC" : ""}</div>
|
||||
<div>${entrySet.has(n.id) ? '<span class="badge entry">entry point</span>' : ""}
|
||||
<span class="badge">fan-in ${fanIn.get(n.id) || 0}</span>
|
||||
<span class="badge">fan-out ${fanOut.get(n.id) || 0}</span></div>
|
||||
${n.file ? `<h3>Source</h3><div class="meta">${esc(n.file)}</div>` : ""}
|
||||
${neighbors.length ? `<h3>Connections (${neighbors.length})</h3>` +
|
||||
neighbors.slice(0, 40).map(x => `<span class="link" data-id="${esc(x.id)}">${x.dir} ${esc(byId.get(x.id)?.name || x.id)} <span class="kind">${esc(x.rel)}</span></span>`).join("") +
|
||||
(neighbors.length > 40 ? `<div class="meta">…and ${neighbors.length - 40} more</div>` : "") : ""}`;
|
||||
sidebar.classList.add("open");
|
||||
sidebar.querySelector("#close").onclick = () => { selected = null; renderSidebar(); requestDraw(); };
|
||||
sidebar.querySelectorAll(".link").forEach(el => el.onclick = () => {
|
||||
const t = byId.get(el.dataset.id);
|
||||
if (t) { selected = t.id; flyTo(t); renderSidebar(); requestDraw(); }
|
||||
});
|
||||
}
|
||||
function renderFlowSidebar() {
|
||||
const f = activeFlow;
|
||||
sidebar.innerHTML = `
|
||||
<span class="closebtn" id="close">×</span>
|
||||
<h2>${esc(f.name)}</h2>
|
||||
<div class="meta">${f.persona ? "Persona: " + esc(f.persona) : ""}</div>
|
||||
${f.description ? `<p style="font-size:13px">${esc(f.description)}</p>` : ""}
|
||||
<h3>Steps</h3>
|
||||
<ol>${f.steps.map(s => `<li>${esc(s.label)}${(s.nodes || []).length ?
|
||||
`<div class="meta">${s.nodes.map(id => esc(byId.get(id)?.name || id)).join(" → ")}</div>` : ""}</li>`).join("")}</ol>`;
|
||||
sidebar.classList.add("open");
|
||||
sidebar.querySelector("#close").onclick = () => setFlow("");
|
||||
}
|
||||
|
||||
// ── Search ──────────────────────────────────────────────────────────────
|
||||
const searchEl = document.getElementById("search"), resultsEl = document.getElementById("results");
|
||||
searchEl.addEventListener("input", () => {
|
||||
const q = searchEl.value.trim().toLowerCase();
|
||||
resultsEl.innerHTML = "";
|
||||
if (q.length < 2) return;
|
||||
nodes.filter(n => n.name.toLowerCase().includes(q)).slice(0, 12).forEach(n => {
|
||||
const d = document.createElement("div");
|
||||
d.innerHTML = `${esc(n.name)}<span class="kind">${esc(n.kind)}</span>`;
|
||||
d.onclick = () => { selected = n.id; flyTo(n); renderSidebar(); requestDraw(); };
|
||||
resultsEl.appendChild(d);
|
||||
});
|
||||
});
|
||||
|
||||
// ── Edge-kind toggles & legend ──────────────────────────────────────────
|
||||
const togglesEl = document.getElementById("toggles");
|
||||
for (const [kind, style] of Object.entries(EDGE_STYLE)) {
|
||||
if (!edges.some(e => e.kind === kind)) continue;
|
||||
const solid = style.color.replace(/[\d.]+\)$/, "0.9)");
|
||||
const l = document.createElement("label");
|
||||
l.innerHTML = `<input type="checkbox" checked><span class="swatch" style="background:${solid}"></span>${style.label}`;
|
||||
l.querySelector("input").onchange = ev => { kindOn[kind] = ev.target.checked; requestDraw(); };
|
||||
togglesEl.appendChild(l);
|
||||
}
|
||||
if (!togglesEl.children.length) togglesEl.style.display = "none";
|
||||
|
||||
// ── Flows ───────────────────────────────────────────────────────────────
|
||||
const flowsPanel = document.getElementById("flows"), flowSel = document.getElementById("flowSel");
|
||||
const flows = DATA.flows || [];
|
||||
if (flows.length) {
|
||||
flowsPanel.style.display = "";
|
||||
flows.forEach((f, i) => {
|
||||
const o = document.createElement("option");
|
||||
o.value = i; o.textContent = f.persona ? `${f.name} (${f.persona})` : f.name;
|
||||
flowSel.appendChild(o);
|
||||
});
|
||||
flowSel.onchange = () => setFlow(flowSel.value);
|
||||
}
|
||||
function setFlow(v) {
|
||||
flowSel.value = v;
|
||||
activeFlow = v === "" ? null : flows[+v];
|
||||
selected = null;
|
||||
if (activeFlow) {
|
||||
const ids = activeFlow.steps.flatMap(s => s.nodes || []);
|
||||
const pts = ids.map(id => byId.get(id)).filter(Boolean);
|
||||
if (pts.length) {
|
||||
const minX = Math.min(...pts.map(n => n.x - n.r)), maxX = Math.max(...pts.map(n => n.x + n.r));
|
||||
const minY = Math.min(...pts.map(n => n.y - n.r)), maxY = Math.max(...pts.map(n => n.y + n.r));
|
||||
flyTo({ x: (minX + maxX) / 2, y: (minY + maxY) / 2, r: Math.max(maxX - minX, maxY - minY) / 2 || 100 });
|
||||
}
|
||||
}
|
||||
renderSidebar(); requestDraw();
|
||||
if (!activeFlow) sidebar.classList.remove("open");
|
||||
}
|
||||
|
||||
// ── Observations ────────────────────────────────────────────────────────
|
||||
if ((DATA.observations || []).length) {
|
||||
document.getElementById("obs").style.display = "";
|
||||
document.getElementById("obsList").innerHTML =
|
||||
DATA.observations.map(o => `<li style="margin:4px 0">${esc(o)}</li>`).join("");
|
||||
}
|
||||
|
||||
// ── Boot ────────────────────────────────────────────────────────────────
|
||||
document.getElementById("title").textContent = DATA.system || DATA.root.name || "System topology";
|
||||
document.getElementById("stats").textContent =
|
||||
`${nodes.filter(n => !n.container).length} modules · ${edges.length} edges` +
|
||||
(entrySet.size ? ` · ${entrySet.size} entry points` : "");
|
||||
{
|
||||
const vw = innerWidth, vh = innerHeight;
|
||||
const k = Math.min(vw, vh) / (WORLD * 1.05);
|
||||
sel.call(zoomB.transform, d3.zoomIdentity.translate(vw / 2 - WORLD / 2 * k, vh / 2 - WORLD / 2 * k).scale(k));
|
||||
}
|
||||
requestDraw();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -36,23 +36,32 @@ fewest-dependencies first. For each phase:
|
||||
|
||||
Render the phases as a Mermaid `gantt` chart.
|
||||
|
||||
### 4. Behavior Contract
|
||||
### 4. Business Walkthroughs
|
||||
For each persona flow in `analysis/$1/topology.json` (`flows` — produced
|
||||
by `/modernize-map`), a short narrative table: persona, what happens in
|
||||
business language, which legacy modules implement it today, and which
|
||||
phase from §3 replaces each. This is the section non-technical approvers
|
||||
actually read — it connects "Phase 2" to "what happens when a customer
|
||||
files a claim". If topology.json has no flows, derive 2–3 walkthroughs
|
||||
from the entry points and say they need SME confirmation.
|
||||
|
||||
### 5. Behavior Contract
|
||||
List the **P0 rules** from BUSINESS_RULES.md (the ones tagged `Priority: P0` —
|
||||
money, regulatory, data integrity) that MUST be proven equivalent before any
|
||||
phase ships. These become the regression suite. Flag any P0 rule with
|
||||
Confidence < High as a blocker requiring SME confirmation before its phase
|
||||
starts.
|
||||
|
||||
### 5. Validation Strategy
|
||||
### 6. Validation Strategy
|
||||
State which combination applies: characterization tests, contract tests,
|
||||
parallel-run / dual-execution diff, property-based tests, manual UAT.
|
||||
Justify per phase.
|
||||
|
||||
### 6. Open Questions
|
||||
### 7. Open Questions
|
||||
Anything requiring human/SME decision before Phase 1 starts. Each as a
|
||||
checkbox the approver must tick.
|
||||
|
||||
### 7. Approval Block
|
||||
### 8. Approval Block
|
||||
```
|
||||
Approved by: ________________ Date: __________
|
||||
Approval covers: Phase 1 only | Full plan
|
||||
|
||||
@ -55,50 +55,108 @@ re-run and audited. Have it write a machine-readable
|
||||
`analysis/$1/topology.json` and print a human summary. Run it; show the
|
||||
summary (cap at ~200 lines for very large estates).
|
||||
|
||||
## Render
|
||||
`topology.json` must follow this schema — it feeds the interactive viewer:
|
||||
|
||||
From the extracted data, generate **three Mermaid diagrams** and write them
|
||||
to `analysis/$1/TOPOLOGY.html` as a self-contained page that renders in any
|
||||
browser.
|
||||
|
||||
The HTML page must use: dark `#1e1e1e` background, `#d4d4d4` text,
|
||||
`#cc785c` for `<h2>`/accents, `system-ui` font, all CSS **inline** (no
|
||||
external stylesheets). Load Mermaid from a CDN in `<head>`:
|
||||
|
||||
```html
|
||||
<script type="module">
|
||||
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs';
|
||||
mermaid.initialize({ startOnLoad: true, theme: 'dark' });
|
||||
</script>
|
||||
```json
|
||||
{
|
||||
"system": "<display name>",
|
||||
"root": {
|
||||
"id": "sys", "name": "<system>", "kind": "system",
|
||||
"children": [
|
||||
{ "id": "dom:<domain>", "name": "<Domain>", "kind": "domain",
|
||||
"children": [
|
||||
{ "id": "<MODULE>", "name": "<MODULE>", "kind": "module",
|
||||
"language": "cobol", "loc": 1234, "file": "src/MODULE.cbl" }
|
||||
] },
|
||||
{ "id": "dom:data", "name": "Data stores", "kind": "domain",
|
||||
"children": [
|
||||
{ "id": "ds:<NAME>", "name": "<NAME>", "kind": "datastore" }
|
||||
] }
|
||||
]
|
||||
},
|
||||
"edges": [
|
||||
{ "source": "<id>", "target": "<id>", "kind": "call" }
|
||||
],
|
||||
"entryPoints": ["<id>", "..."],
|
||||
"observations": ["<architect observation>", "..."],
|
||||
"flows": [
|
||||
{ "name": "<business flow>", "persona": "<who experiences it>",
|
||||
"description": "<one sentence, plain language>",
|
||||
"steps": [
|
||||
{ "label": "<business-language step>", "nodes": ["<id>", "<id>"] }
|
||||
] }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Each diagram goes in a `<pre class="mermaid">...</pre>` block. Do **not**
|
||||
wrap diagrams in markdown ` ``` ` fences inside the HTML.
|
||||
- Group leaf modules under `domain` containers (use the domains from
|
||||
`/modernize-assess` if available). Leaf kinds: `module`, `datastore`,
|
||||
`job`, `screen`. `loc` drives circle size — include it for modules.
|
||||
- Edge kinds: `call` (direct), `dispatch` (dynamic/router), `read`,
|
||||
`write`. Every edge endpoint must be a leaf id that exists in the tree.
|
||||
- `observations`: 3–7 architect observations — tight coupling clusters,
|
||||
single points of failure, service-extraction candidates, data stores
|
||||
with too many writers.
|
||||
- `flows` is the **persona walkthrough** section — see below.
|
||||
|
||||
1. **`graph TD` — Module call graph.** Cluster by domain (use `subgraph`).
|
||||
Highlight entry points in a distinct style. Cap at ~40 nodes — if larger,
|
||||
show domain-level with one expanded domain.
|
||||
## Persona flows
|
||||
|
||||
2. **`graph LR` — Data lineage.** Programs → data stores.
|
||||
Mark read vs write edges.
|
||||
Trace **2–4 end-to-end business flows**, each anchored to a persona —
|
||||
the people who experience the system, not the people who maintain it
|
||||
(e.g. for a benefits system: the claimant, the caseworker, the auditor;
|
||||
for billing: the customer, the billing operator). For each flow:
|
||||
|
||||
3. **`flowchart TD` — Critical path.** Trace ONE end-to-end business flow
|
||||
(e.g., "monthly billing run" or "process payment") through every program
|
||||
and data store it touches, in execution order. If production telemetry is
|
||||
available (see `/modernize-assess` Step 4), annotate each step with its
|
||||
p50/p99 wall-clock.
|
||||
- `name` + one-sentence `description` in plain business language —
|
||||
something a steering committee member relates to ("a claimant files a
|
||||
weekly claim"), not a data-flow label ("CLM batch ingest").
|
||||
- `steps`: 3–8 steps, each with a business-language `label` and the
|
||||
`nodes` (programs + data stores) that implement that step, in
|
||||
execution order.
|
||||
|
||||
Also export the three diagrams as standalone `.mmd` files for re-use:
|
||||
`analysis/$1/call-graph.mmd`, `analysis/$1/data-lineage.mmd`,
|
||||
`analysis/$1/critical-path.mmd`.
|
||||
This is the bridge between the technical map and non-technical
|
||||
stakeholders: the same diagram answers "which program does X" for
|
||||
engineers and "what happens when someone files a claim" for everyone else.
|
||||
|
||||
## Annotate
|
||||
## Render
|
||||
|
||||
Below each `<pre class="mermaid">` block in TOPOLOGY.html, add a `<ul>`
|
||||
with 3-5 **architect observations**: tight coupling clusters, single
|
||||
points of failure, candidates for service extraction, data stores
|
||||
touched by too many writers.
|
||||
`analysis/$1/TOPOLOGY.html` is an **interactive map**: a zoomable
|
||||
circle-pack of the whole system (domains as containers, modules sized by
|
||||
LOC) with dependency edges, search, per-node detail sidebar, edge-kind
|
||||
toggles, and a flow-walkthrough mode that plays each persona flow as a
|
||||
numbered path. Build it from the template that ships with this plugin —
|
||||
do not hand-write the viewer:
|
||||
|
||||
```bash
|
||||
python3 - "$CLAUDE_PLUGIN_ROOT/assets/topology-viewer.html" analysis/$1 <<'EOF'
|
||||
import json, sys
|
||||
tpl_path, out_dir = sys.argv[1], sys.argv[2]
|
||||
tpl = open(tpl_path).read()
|
||||
data = json.dumps(json.load(open(f"{out_dir}/topology.json")))
|
||||
html = tpl.replace("/*__TOPOLOGY_DATA__*/ null", "/*__TOPOLOGY_DATA__*/ " + data)
|
||||
open(f"{out_dir}/TOPOLOGY.html", "w").write(html)
|
||||
print(f"wrote {out_dir}/TOPOLOGY.html ({len(html):,} bytes)")
|
||||
EOF
|
||||
```
|
||||
|
||||
The viewer loads d3 from a CDN, so opening it needs network access; the
|
||||
rest is self-contained. If the data injection marker is missing from the
|
||||
output, the template was not found — check `$CLAUDE_PLUGIN_ROOT`.
|
||||
|
||||
Mermaid stays for **small, exportable** diagrams. Generate standalone
|
||||
`.mmd` files for reuse in docs and PRs — but keep each under ~40 edges;
|
||||
collapse to domain level if the full graph is bigger (dense Mermaid
|
||||
becomes unreadable, which is exactly what the interactive map is for):
|
||||
|
||||
- `analysis/$1/call-graph.mmd` — domain-level `graph TD`, entry points
|
||||
highlighted
|
||||
- `analysis/$1/data-lineage.mmd` — `graph LR`, programs → data stores,
|
||||
read vs write marked
|
||||
- `analysis/$1/critical-path.mmd` — `flowchart TD` of the primary flow
|
||||
from `flows`, annotated with p50/p99 wall-clock if telemetry is
|
||||
available (see `/modernize-assess` Step 4)
|
||||
|
||||
## Present
|
||||
|
||||
Tell the user to open `analysis/$1/TOPOLOGY.html` in a browser.
|
||||
Tell the user to open `analysis/$1/TOPOLOGY.html` in a browser, and to
|
||||
try: search for a module, click it to see its connections, and pick a
|
||||
persona flow from the walkthrough dropdown.
|
||||
|
||||
93
plugins/code-modernization/commands/modernize-preflight.md
Normal file
93
plugins/code-modernization/commands/modernize-preflight.md
Normal file
@ -0,0 +1,93 @@
|
||||
---
|
||||
description: Environment readiness check — analysis tools, build toolchain, source completeness, telemetry access
|
||||
argument-hint: <system-dir> [target-stack]
|
||||
---
|
||||
|
||||
Check whether this environment is ready to analyze — and eventually
|
||||
transform — `legacy/$1`, and tell the user exactly what to fix before the
|
||||
other commands run into it. Modernization sessions fail late and
|
||||
confusingly when this isn't done: assessment metrics silently degrade
|
||||
without analysis tools, characterization tests can't run without a build
|
||||
toolchain, and dependency maps come out wrong when half the source isn't
|
||||
in the tree.
|
||||
|
||||
Run every check even when an early one fails — the point is one complete
|
||||
readiness report, not the first error.
|
||||
|
||||
## Check 1 — Detect the stack
|
||||
|
||||
Fingerprint `legacy/$1` from file extensions and manifests: languages,
|
||||
build system, deployment/config descriptors. This drives which checks
|
||||
below apply. Report what was detected and the rough file split.
|
||||
|
||||
## Check 2 — Analysis tooling
|
||||
|
||||
For each, check availability (`command -v`) and report version, what it's
|
||||
used for, and what degrades without it:
|
||||
|
||||
| Tool | Used by | Without it |
|
||||
|---|---|---|
|
||||
| `scc` (or `cloc`) | assess | LOC/complexity fall back to `find`+`wc`; COCOMO estimate gets coarser |
|
||||
| `lizard` | assess --portfolio | complexity estimated from decision-keyword counts |
|
||||
| `glow` | all | markdown artifacts render as plain text |
|
||||
|
||||
Include the platform's install one-liner for anything missing
|
||||
(`brew install scc`, `apt install cloc`, `pip install lizard`, …).
|
||||
|
||||
## Check 3 — Build toolchain (smoke test, not just presence)
|
||||
|
||||
Identify the compiler/interpreter for the detected legacy stack — e.g.
|
||||
GnuCOBOL (`cobc`) for COBOL, JDK + Maven/Gradle for Java, `cc`/`make` for
|
||||
C, `dotnet` for .NET. Then **prove it works on this codebase**: pick one
|
||||
representative source file and run a syntax-only compile
|
||||
(`cobc -fsyntax-only`, `javac`, `gcc -fsyntax-only`, …).
|
||||
|
||||
A failed smoke test is the most valuable output of this command — report
|
||||
the actual error and diagnose it: missing copybook/include path, missing
|
||||
dialect flag (`-std=ibm` etc.), fixed vs free format, missing dependency
|
||||
jar. These are the errors that otherwise surface mid-`/modernize-transform`
|
||||
with much less context.
|
||||
|
||||
If the user passed a `[target-stack]`, do the same for it: runtime,
|
||||
package manager, test framework (`mvn -v`, `npm -v`, `pytest --version`, …).
|
||||
|
||||
## Check 4 — Source completeness
|
||||
|
||||
The dependency map is only as good as what's in the tree. Check for the
|
||||
detected stack's equivalents of:
|
||||
|
||||
- **Referenced-but-missing includes** — copybooks (`COPY X` with no
|
||||
`X.cpy`), headers, imports that resolve nowhere. Count and list the top
|
||||
missing names.
|
||||
- **Deployment/config descriptors** — JCL for batch COBOL, CICS CSD
|
||||
definitions, `web.xml`/route configs, cron/scheduler definitions.
|
||||
Without these, entry-point detection and the code↔storage join in
|
||||
`/modernize-map` are guesswork.
|
||||
- **Data definitions** — DDL, schemas, copybook record layouts, ORM
|
||||
mappings.
|
||||
- **Binary-only artifacts** — load modules, jars, DLLs with no matching
|
||||
source. These become unmappable black boxes; flag them now.
|
||||
|
||||
## Check 5 — Optional context
|
||||
|
||||
- **Production telemetry** — is an observability/APM MCP server connected,
|
||||
or are batch job logs / runtime exports available? (Enables the runtime
|
||||
overlay in `/modernize-assess` Step 4 and timing annotations in
|
||||
`/modernize-map`.)
|
||||
- **Version control history** — is `legacy/$1` under git with meaningful
|
||||
history? (Change-frequency data sharpens risk ranking.)
|
||||
|
||||
## Report
|
||||
|
||||
Write `analysis/$1/PREFLIGHT.md`: a status table — one row per check,
|
||||
status ✅ / ⚠️ / ❌, what was found, and the fix for anything not green —
|
||||
followed by a **Ready / Ready-with-gaps / Not ready** verdict per command:
|
||||
|
||||
- `assess` + `map` + `extract-rules` — need Checks 1–2 green-ish and
|
||||
Check 4's missing-include count low
|
||||
- `transform` + `reimagine` — additionally need Check 3 green for both
|
||||
legacy and target stacks
|
||||
- `harden` — needs Check 2 plus any stack-specific SAST tooling found
|
||||
|
||||
Print the table in the session too, and end with the single most
|
||||
important fix if anything is red.
|
||||
@ -9,7 +9,24 @@ equivalence.
|
||||
This is a surgical, single-module transformation — one vertical slice of the
|
||||
strangler fig. Output goes to `modernized/$1/$2/`.
|
||||
|
||||
## Step 0 — Plan (HITL gate)
|
||||
## Step 0a — Toolchain check (fail fast)
|
||||
|
||||
Verify the build environment **before** planning, not when the tests
|
||||
first run:
|
||||
|
||||
- **Target stack ($3):** runtime, package manager, and test framework all
|
||||
respond (`java -version` + `mvn -v`, `node -v` + `npm -v`,
|
||||
`python3 -V` + `pytest --version`, …).
|
||||
- **Legacy stack (if equivalence tests will execute legacy code):** the
|
||||
compiler/interpreter works on this codebase — run a syntax-only compile
|
||||
of the module being transformed (e.g. `cobc -fsyntax-only`).
|
||||
|
||||
If anything is missing or the smoke compile fails, stop and report what
|
||||
to install or fix — suggest `/modernize-preflight $1 $3` for the full
|
||||
readiness report. Don't enter plan mode on a machine that can't run the
|
||||
proof.
|
||||
|
||||
## Step 0b — Plan (HITL gate)
|
||||
|
||||
Read the source module and any business rules in `analysis/$1/BUSINESS_RULES.md`
|
||||
that reference it. Then **enter plan mode** and present:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user