# Modernization on SteelFrame — standing instructions for Claude Code

This repository follows the code-modernization playbook (understand first, rank the risk, tests before code,
one component at a time with continuous validation) on a SteelFrame estate reached over MCP. SteelFrame is a
z/OS-compatible reference engine: it **returns facts** (source, decoded records beside their bytes, joblogs,
screens, scheduler cycles) and **never judges equivalence or converts code**. You compare on this side; the
engineer signs.

## The two MCP servers and what each is for

| server | door | use it for |
|---|---|---|
| `steelframe` | `/mcp` (persistent lane) | **reading** the estate: `inventory_scan`, `find_program`, `search_source`, `read_member`, `decode_records`, `analysis_*`, `export_bundle`, `list_jobs`, `read_spool`, `whoami`. The project settings deny every tool on this server that changes the estate, submits work or runs a session. Do not try to work around that. |
| `steelframe-private` | `/mcp?lane=ephemeral` (RAM-only lane) | **everything that runs**: `create_workspace` (a RAM-only copy of the estate), then `workspace:<id>` on every call — `load_dataset`, `run_batch`, `cics_*`, `sched_*`, `export_state`, `sql`. Nothing on this lane reaches the live estate; nothing survives `destroy_workspace` or the TTL. |

Rules that are not negotiable:

1. **Never run anything against the live estate.** Every job, seed, CICS session or SQL statement goes into a
   workspace on `steelframe-private`. A stateful tool without `workspace:<id>` is refused there (403) — that is
   the design, not an error to fix.
2. **Name every workspace** (`name` is the idempotency key). Poll `get_workspace` while `status` is `CREATING`;
   never call `create_workspace` again meanwhile. On 429 (per-user or plane cap) wait and poll; do not loop.
   Destroy a workspace when the task is done.
3. **Pin what can be pinned**: `clock:{fixed:'YYYY-MM-DD HH:MM:SS'}` on `run_batch` / `cics_open_session` /
   `cics_run_script`, `jobNumber` on the workspace. JES timestamps in JESMSGLG/JESYSMSG are real — never
   compare them.
4. **Facts are not verdicts.** When the oracle's output and the port's output differ, report the difference with
   the bytes/rows on both sides and stop; do not "fix" the fixture to make a test pass.
5. **Credit**: reads, lists and status are free; a write/compile is $0.03; a run is $0.03 + $0.99 per core-hour
   after 10 s; workspaces are unmetered. `CREDIT_REQUIRED` means stop and tell the user (do not retry). Every
   billable result ends with its cost — mention totals in your summary.
6. **Secrets**: the API key is `SF_TOKEN` in the environment, expanded by `.mcp.json`. Never print it, never
   write it into a file, never put it in a commit. Never ask for a password.
7. **Numbers**: decoded rows carry the engine's native numbers (a JSON number when a double carries the exact
   decimal, else the exact decimal string). Compare as decimals (see `tools/fixture.mjs` `num()`), never as
   floats.
8. **Charsets**: PS/PDS/GDG datasets are EBCDIC at rest; KSDS/ESDS hex is the program's record image. The
   `charset` field says which; byte comparisons must respect it.

## The phases, as skills

| phase | skill | writes |
|---|---|---|
| 1 Discovery | `/sf-discover <PROGRAM or JOB>` | `discovery/<NAME>.md` — what the estate knows: source, copybooks, JCL steps, datasets, CSD slice, call graph, field-level impact |
| 2 Risk | `/sf-risk-rank` | `risk-register.md` — components ranked by coupling, dead code, third-party dependencies; "isolated candidates first" |
| 3 Tests first | `/sf-characterize <spec.json>` | `fixtures/<name>/fixture.json` — the legacy behaviour captured on a workspace with a pinned clock (`tools/characterize.mjs`) |
| 4a Port | `/sf-port <PROGRAM> <language>` | the port + tests that load the fixture, written **after** the fixture exists |
| 4b Compare | `/sf-compare <a> <b>` | `tools/fixture-diff.mjs` verdicts: oracle vs re-capture, oracle vs the port's output |

Worked example: `carddemo-posttran/` (CardDemo's POSTTRAN / CBTRN02C, end to end). Read its README before
starting a new component; copy its shape.

## Conventions

- Discovery notes and risk rows cite the tool and arguments they came from (`find_program {"name":"CBTRN02C"}`),
  so a reviewer can re-run them.
- A port lives in `<component>/<language>/`; its tests load `fixtures/<name>/fixture.json` through `FIXTURE=`
  and never embed expected values by hand; they address files by DD name (the program's own view), never by DSN.
- Files a job writes or loads live under your own HLQ (`{{USER}}` in a characterization spec); the estate's
  masters are read and updated in place inside the workspace, never re-defined (ordinary users may not, and
  the workspace copy carries the estate's RACF).
- Keep deviations explicit: when the port cannot reproduce a COBOL behaviour (truncation on size error, a file
  status path), it raises or logs; it never silently differs.
