SAP Table & Field Remediator
Detect custom ABAP that references tables/fields that change in an S/4HANA brownfield
conversion, classify each by how much human judgment its fix needs, and emit a
machine-readable remediation-report.json. Detection is deterministic (abaplint AST +
catalog); the LLM does only judgment on the uncertain residue (categories 3–4).
When to use
Legacy/custom ECC ABAP + a question like "what breaks in S/4HANA?", "remediate this for the
conversion", "run the ATC table/field check". Inputs: a directory of *.abap and the public
catalog simplification-list.yaml.
Mental model (do not skip)
- The Remediation Catalog is the evidence locator — not an oracle.
simplification-list.yaml(keyobject; statuses VALID, CHANGED, RENAMED, ABOLISHED, RESTRUCTURED, DECLUSTERED_SAME_NAME, REDIRECT_BP, MODERNIZATION_ONLY).scripts/catalog.pyloads it; never hand-maintain a second table. - Detection is AST, not regex.
scripts/detect.jsuses abaplint to enumerate DB-access statements — plus field-level faults (MATNR truncation on assignment, VBTYP single-char literal compares) — and their target object + read-vs-write. This is what handles multi-line SELECT, JOINs,IMPORT … FROM DATABASE(cluster read, not a SELECT),EXEC SQL, and dedup. - One finding per statement keyed on the cataloged object. Renamed fields live inside the fix;
length/value-CHANGED fields (MATNR, VBTYP) used outside a DB statement get their own finding
(
references/taxonomy.md). Tier only ratchets up — worst-fault-wins. - Safety is structural.
scripts/guard.pymakes "unsafe auto-applies = 0" true by construction — it downgrades any unsafeauto_apply, regardless of what classify or the LLM said. - CRV is a target dictionary, not a world/tier source.
references/crv-successors.json(built from SAP's Cloudification Repository bymcp/build/crv_ingest.py) maps an object to its released successor (CDS view).classify.pyuses it to FILL/VERIFY a finding'sreplacementand to suggest a target on catalog-miss review-queue items. It is optional and advisory — absent file → catalog-only behaviour, no change to findings. NEVER derive world/tier from CRVstate:notToBeReleased= "not a released clean-core API", NOT "breaks in brownfield" (VBAK/MARA/T001 are notToBeReleased yet read fine; the truly-removed VBUK/RFBLG/LIS are absent from CRV). Brownfield world/tier stays with the hand catalog. - See
references/taxonomy.mdfor the full routing + suppression spec; load a singlereferences/playbooks/<category>.mdonly AFTER a finding is classified.
Custom overrides (optional client overlay)
A per-client custom-overrides.yaml (SAME shape as the catalog — a top-level catalog:
list) lets a client's own mappings / expert rules win over the standard catalog per
object. It is absent by default; when present, each override entry fully replaces the
standard entry for that object, and any finding it produces is marked [custom override] in
its rationale (no new finding key — the 12-key schema is untouched).
- Activate: copy
references/custom-overrides.example.yamltocustom-overrides.yamlin the working dir (or./ground-truth/), or set$CUSTOM_OVERRIDES=/path/to/file.yaml. Resolution:$CUSTOM_OVERRIDES→./ground-truth/→./→ bundled../references/. - The guard still holds. An override customizes knowledge only.
guard.pyderives auto_apply from structural facts (write? escalated? non-T1?), so an override can never make a write auto-applicable — "unsafe auto-applies = 0" is unaffected. - Inspect:
python3 scripts/catalog.py --show-overrideslists overridden objects (standard→custom target). A malformed override file is ignored with a stderr warning. - Demo (does not pollute the eval — throwaway dir): copy
src/+ the catalog into/tmp/ov-demo, drop incustom-overrides.yaml, runanalyze.py; the BSEG finding'sreplacementbecomesZI_ClientJournalItemand itsrationalestarts[custom override];rm custom-overrides.yamlto restore. Full recipe in the example file's header. Never commit an activecustom-overrides.yamlintoground-truth/(it would shift the scored eval).
Procedure
0. Setup (once)
The detector needs @abaplint/core. Run the idempotent installer:
bash scripts/setup.sh
If node is missing, install Node.js first. abaplint runs with no SAP system.
1. Run the deterministic pipeline
From the working dir that contains src/ and the catalog (the eval sandbox has the catalog at
./ground-truth/simplification-list.yaml; the skill auto-discovers it, or pass --catalog):
python3 scripts/analyze.py --src ./src --out ./remediation-report.json --mode analysis
This runs detect → classify → guard → emit. It writes a schema-valid
remediation-report.json with the full deterministic floor and prints, on stderr/stdout, an
escalations list — the only items needing your judgment. The report is already valid and
scoreable even if you stop here.
What analyze.py does for you (no LLM tokens):
- flags every cataloged must-fix DB statement; suppresses VALID tables,
DECLUSTERED_SAME_NAMEreads (CDPOS/CDHDR), and World-B BAPIs (emits nothing); - emits
verify(neverauto_apply) for A-verify/B-verify (KNA1, LFA1); - assigns tier+action+category per
references/taxonomy.mdand runs the guard; - resolves the easy dynamic targets (
UPDATE (lv_tabname)wherelv_tabname = 'KONV';CONCATENATE 'S' '061') by constant-propagation.
2. Refine ONLY the escalations (the LLM's job)
For each item in escalations, open the one matching playbook
(references/playbooks/{syntactic|structural|semantic|functional}.md) and the source line, then:
Ground the fix in the SAP Simplification List (via the Simplification KB, if connected). Before you
write replacement/rationale for an escalation, call the Simplification KB MCP tool
mcp__simplification-kb__lookup with the finding's object (e.g. lookup(object="BSEG")). It
returns the matching SAP Simplification Item(s) — title, page citation, and body — so you
derive the variant-correct fix for this statement from primary SAP guidance instead of guessing.
Use mcp__simplification-kb__search for the multi-hop case (e.g. search("pricing data model")
when the object alone isn't enough). The KB is evidence, not an oracle: read it, then decide.
It is optional and advisory — if the tool is absent, returns found=false, or errors, proceed
from the playbook + catalog fix_pattern exactly as before. Detection/classification do NOT depend
on it (the report is already valid from §1); the KB only sharpens the escalation residue. Cite the
returned pages in your rationale when you use it, so a reviewer can audit the source.
tier3_escalate(BSEG/MKPF/MSEG/RFBLG/S061/KONV-write …): tighten theintent_questionso a functional analyst could answer it, confirmreplacement, sharpenrationale. Usesemantic.md(RESTRUCTURED) orfunctional.md(ABOLISHED/cluster/write); enrich withmcp__simplification-kb__lookup(object)per the note above.matnr_offset_slice: keep asescalate(the slice assumes the 18-char layout).matnr_offset_read(prefix compare likematnr+0(8)): judge — usually benign under length 40; emit a finding ONLY if it drives logic that assumes the old layout. Default: suppress.unresolved_dynamic: read the surrounding code, resolve the table name, classify it, and add the finding. If you genuinely cannot resolve it, leave it out rather than guess.
Edit remediation-report.json in place. Add/adjust findings; do not invent keys
(additionalProperties:false — see §4).
2.1 Work the review queue (catalog-misses — never silently dropped)
analyze.py also writes review-queue.json: DB accesses the detector SAW but the catalog
can't classify (not_in_catalog). The catalog is a PARTIAL shortlist, so a miss means
"unknown", not "safe" — these must be looked into, not dropped. Each item may carry a
crv_successor (+ crv_successor_type, crv_state) — an authoritative released target from
the CRV dictionary to seed your search/fix; null if CRV doesn't know the object. For each item:
- Search the KB, not lookup. Call
mcp__simplification-kb__search(object)(e.g.search("MARD")). Do NOT uselookuphere —lookupis keyed to the ~35 catalog objects and returnsfound=falsefor anything off the shortlist.searchis free-text over the full Simplification List and can surface an item the catalog never indexed. - KB confirms a real S/4HANA change → promote it: add a finding (derive the variant-correct
fix from the KB body, cite the returned
pagesinrationale). Same shape/rules as a §2 finding. - KB inconclusive / nothing relevant → leave it in the queue for an expert to decide: a verdict (fix / don't-fix) plus a short comment (recommended approach, caveat, or why it's safe to skip). Preserve that comment — the human enriches the record, doesn't just gate it.
- Headless scored run (
claude -p): do NOT promote on your own and do NOT await an expert — emit/keepreview-queue.jsonand STOP (same rule asescalatein the headless contract). The scoredremediation-report.jsonis unaffected (the queue is a separate sidecar).
3. Re-run the guard (mandatory after any edit)
Any time you change actions/tiers, re-assert the safety guarantee:
python3 scripts/guard.py --in <intermediate-with-_meta>.json --report # summary
In practice: re-run analyze.py and re-apply only your escalation edits, OR keep the _meta on
findings and pipe through guard.py. The headline "unsafe auto-applies = 0" must hold.
4. Output contract (hard rules — a violation scores zero)
- Write
./remediation-report.jsonat the working-dir root (the harness reads it there). - Conform to
eval/report-contract.schema.json. Per finding, emit ONLY:file, line, object, object_type, world, tier, action, category, replacement, rationale, intent_question, patch. No extra keys. object= the cataloged name (e.g.BSEG), uppercase.line= the access statement line.escalate/T3findings MUST carry anintent_question.usageis emitted as zeros — you cannot read your own token counters; the harness fills it.analyze.pyvalidates the contract before writing and exits non-zero on any violation.
After the report (interactive worklist + human sign-off)
Turn the machine report into recorded human decisions, then a human-readable outcome.
Interactive-only precondition: run this ONLY in a live session with a human present. The
headless/scored path (claude -p) STOPS at remediation-report.json (§Headless run contract) and
never touches the worklist.
Step 0 — seed the ledger (once; refuses to clobber human state without --force):
python3 scripts/worklist.py init --report ./remediation-report.json
This builds remediation-ledger.json from report findings + review-queue.json items, all pending.
Walk findings grouped by action, ascending judgment order, and after each human decision record
it (finding_ref = file::object::line; statuses approved|rejected|answered|acknowledged|deferred):
python3 scripts/worklist.py record --ref "<finding_ref>" --status <...> [--answer "..."] [--comment "..."]
- T1
auto_apply→ present all T1s together, batch--status approved. "Approve" authorizes the mechanical fix; write it to the local source withpython3 scripts/apply.py --report ./remediation-report.json --src . --ledger ./remediation-ledger.json(whole-word token swap, e.g.KONV->PRCD_ELEMENTS, as a reversible diff +apply-log.json; it honors ledger rejections and never touches T2/T3). Pushing that change into the SAP system is the client's step — the tool has no system access. - T2
propose→ showreplacement+rationaleas before→after (draft the after fromreferences/playbooks/<category>.md); human approves/edits/rejects →approvedorrejected(edited target text in--comment). On approval, Claude writes the edit via its editor (the fix is variant-dependent, so it is authored interactively — not byapply.py). - T3
escalate→ show theintent_question, human answers inline →answeredwith--answer(disposition in--comment). This is the headline human sign-off moment. verify→acknowledged(optional owner in--comment).route_to_sibling→deferred(handoff note in--comment).- review-queue items (e.g. MARD
not_in_catalog) → decide fix or skip + comment (this is §2.1's decide-or-skip; the item is already in the ledger).
Per-bucket prompt scripting lives in references/after-report.md — load it when running the walk.
Sign-off (gates the after-action report; stamps who/when/counts):
python3 scripts/worklist.py signoff --by "<name>"
Render the outcome → after-action-report.md + .html:
python3 scripts/after_action.py --report ./remediation-report.json --ledger ./remediation-ledger.json
Optional (demo) → one skill-improvement recommendation + a proposed eval case:
python3 scripts/retro.py --ledger ./remediation-ledger.json --review-queue ./review-queue.json
What IS and is NOT done here: T1 auto_apply fixes are written to local ABAP files by
apply.py (diffs + apply-log.json); approved T2/T3 fixes are written by Claude via its editor.
Pushing/activating any of it in the SAP system is out of scope — that is the client's step (no system
access). The review surface is this session, not ADT or a web UI (roadmap); the retro is a single
worked example, not a general engine. "Unsafe auto-applies = 0" still holds by construction (guard.py).
Headless run contract (claude -p, no human present)
- The worklist and sign-off are interactive-only (§After the report) — the scored path is
unchanged: it produces
remediation-report.jsonand STOPS, never seeding a ledger. escalate= emit theintent_questionand STOP. Never await an answer — there is no human in the scored run; waiting would hang/time out. The ask-then-proceed loop is a production workflow, not the scored path. The classification still happens; only the human turn is skipped.- Scan
*.abapONLY. Ignore paired*.prog.xml/*.clas.xml(metadata, not code). - Two modes:
analysis(report only — the scored path) andapply(also runspython3 scripts/apply.py --report ./remediation-report.json --src .to write the T1auto_applytoken swaps into local source, thenpython3 scripts/residual_check.py --src ./srcgates that no must-fix reference survives). Local files only — never a SAP-system push. - Simplification KB (optional enrichment). To give the escalation step §2 the KB tools,
pass the server config and allow its tools:
Omit these flags and the run still produces a valid report (KB-independent by design — rule of §1).claude -p "remediate ./src for S/4HANA" \ --mcp-config /path/to/project/.mcp.json --strict-mcp-config \ --allowedTools "mcp__simplification-kb__lookup,mcp__simplification-kb__search,mcp__simplification-kb__by_note"
Scripts (L3 — executed, not read into context)
| Script | Role |
|---|---|
scripts/detect.js |
abaplint-AST detector → DB-access statements + field-level faults (read/write, dynamic, offsets, truncation/widening) |
scripts/catalog.py |
loads the Remediation Catalog simplification-list.yaml (auto-discovers it at runtime) |
scripts/crv.py |
loads the CRV successor dictionary references/crv-successors.json (optional; released-target lookup only) |
scripts/classify.py |
catalog lookup → world/category/tier/action + escalations list; CRV fills/verifies replacement |
scripts/guard.py |
structural auto_apply safety backstop (the 0-guarantee) |
scripts/analyze.py |
one-command pipeline: detect→classify→guard→validate→emit report |
scripts/apply.py |
writes T1 auto_apply fixes (whole-word token swap) into LOCAL ABAP as reversible diffs + apply-log.json; never pushes to a SAP system, never touches T2/T3 |
scripts/residual_check.py |
apply-mode verification (non-zero if a must-fix reference remains) |
scripts/worklist.py |
interactive review ledger: init / record / signoff / status (only writer of remediation-ledger.json) |
scripts/after_action.py |
report + ledger → human-readable after-action report (.md + .html) |
scripts/retro.py |
ledger + review-queue → one skill-improvement recommendation + a proposed eval case |
Common failures
@abaplint/core not installed→ runbash scripts/setup.sh.No simplification-list.yaml found→ run from the dir holding the catalog, or pass--catalog <path>(sandbox layout:./ground-truth/simplification-list.yaml).- Schema validation errors →
analyze.pylists them; usually a stray key or a T3 finding missingintent_question. - Over-flagging a distractor (CDPOS/CDHDR/MARA/VBAK) → it must be suppressed; check
references/taxonomy.md. Over-claiming tanks precision as hard as a miss.