Working with legacy research code
Research software outlives its authors' contracts. The typical legacy
situation is a working but untested codebase whose author has left,
whose behavior IS the specification, and whose results current papers
still depend on. The prime directive: preserve today's behavior first,
improve structure second, change behavior only deliberately and
visibly. In research code a silent behavior change is worse than a
crash - it can corrupt published results downstream.
First contact with an inherited codebase
Before changing anything:
- Get it running. Capture the exact environment that works (versions,
OS, data paths) in a lockfile or container while it still runs
anywhere (rseng-reproducible-environments) - the running environment
is itself endangered knowledge.
- Snapshot everything: commit the code as-is to version control,
including generated files and local tweaks, before any cleanup
(rseng-version-control-review). Tag it as the reference state.
- Recover intent from what exists: papers that used the code, commit
messages, variable names, comments in any language, old emails or
READMEs. Write down what you learn as documentation NOW
(rseng-documentation) - you are the next person who will forget.
- Map the danger zones: which outputs feed publications, which parts
are dead code, which parts everyone fears. Effort goes where
published results depend on correctness.
Characterization tests: the safety net
Legacy code has no tests, so the first tests do not check that the
code is RIGHT - they pin down what it currently DOES:
- Run the code on representative inputs and capture the outputs.
- Turn each captured run into an automated test asserting today's
output (golden-master / snapshot testing). For floating-point
results, assert within tolerances, and record the tolerance
decision.
- Only then start changing code, keeping the characterization tests
green after every step.
When a characterization test later fails on purpose (a bug fix changes
results), update it explicitly and record the scientific justification
in the commit and changelog - that changed number may need to reach
users of previous results (rseng-publishing-releasing). The rseng-testing
skill covers the mechanics; the discipline here is: no refactoring
without a pinned baseline.
Incremental modernization
Never big-bang rewrite what you cannot yet test. Work in small,
reversible steps, each one commit-sized (rseng-version-control-review):
- Strangler pattern: wrap the legacy core behind a clean interface,
route new work through the interface, and replace the inside
piece by piece while the outside stays stable.
- Seams first: to test an untestable function, introduce the smallest
change that lets you inject inputs or observe outputs (extract a
function, parameterize a hard-coded path) - then test, then
refactor.
- One concern per pass: formatting-only commits, then dead-code
removal, then restructuring. Never mix behavior changes into
cleanup commits - reviewers must be able to trust that a "cleanup"
diff changes nothing.
- Add modern hygiene as you touch things, not wholesale: linting and
CI on the files you changed (rseng-code-quality, rseng-ci-cd) rather
than a repository-wide reformat that destroys git blame.
Language and stack migrations
For Fortran-to-Python, MATLAB-to-Python, Python 2-to-3, IDL and
similar migrations:
- Migrate by module with the characterization tests as the contract:
old and new implementations must agree on the pinned outputs within
stated tolerances before the old one is deleted.
- Consider wrapping instead of translating: mature Fortran/C numeric
cores are often best kept and called from a modern interface
(f2py, ctypes, Rcpp) - translation risk is highest exactly where
the code is most optimized.
- Expect floating-point differences across languages, compilers and
BLAS implementations; decide tolerances scientifically, with the
domain expert, not by loosening until tests pass.
- Record the migration in the project history and cite the original
authors - the old code is a research contribution
(rseng-citation-metadata).
Refactor, rewrite or retire
Make the decision explicit and write it down
(rseng-management-planning):
- Refactor when results still matter and the code mostly works -
the default choice.
- Rewrite only with characterization tests as the acceptance
criterion, and keep the legacy version runnable until parity is
proven.
- Retire when nothing current depends on it - but archive properly
rather than delete: a tagged release plus a public archive keeps
the record citable (rseng-publishing-releasing; Software Heritage
archives source code for exactly this purpose).
Working with this skill
This skill is source-independent: it encodes established legacy-code
practice (characterization testing, seams, strangler migration)
applied to research software.
Learn more (verified):
Related skills
Check whether any of these applies before moving on:
- rseng-archiving - retire path ends in archive
- rseng-documentation - recording recovered intent
- rseng-open-source-migration - commercial-platform exits build on this
- rseng-reproducible-environments - capture the working environment first
- rseng-testing - characterization test mechanics
- rseng-version-control-review - small reversible cleanup commits
1---2name: rseng-legacy-code3description: Covers working safely with inherited research code: characterization tests before any change, incremental modernization of untested scripts, recovering intent from code without documentation, and deciding between refactor, rewrite and retire. Use PROACTIVELY when asked to change code that has no tests, and when the user inherits a codebase from a departed researcher, mentions legacy or untested code they are afraid to touch, or wants to change code that has no tests. (Migrating off commercial platforms like MATLAB, IDL or SAS to open alternatives is rseng-open-source-migration; this skill supplies the characterization-test safety net it builds on.)4license: CC-BY-4.05---67# Working with legacy research code89Research software outlives its authors' contracts. The typical legacy10situation is a working but untested codebase whose author has left,11whose behavior IS the specification, and whose results current papers12still depend on. The prime directive: preserve today's behavior first,13improve structure second, change behavior only deliberately and14visibly. In research code a silent behavior change is worse than a15crash - it can corrupt published results downstream.1617## First contact with an inherited codebase1819Before changing anything:20211. Get it running. Capture the exact environment that works (versions,22 OS, data paths) in a lockfile or container while it still runs23 anywhere (rseng-reproducible-environments) - the running environment24 is itself endangered knowledge.252. Snapshot everything: commit the code as-is to version control,26 including generated files and local tweaks, before any cleanup27 (rseng-version-control-review). Tag it as the reference state.283. Recover intent from what exists: papers that used the code, commit29 messages, variable names, comments in any language, old emails or30 READMEs. Write down what you learn as documentation NOW31 (rseng-documentation) - you are the next person who will forget.324. Map the danger zones: which outputs feed publications, which parts33 are dead code, which parts everyone fears. Effort goes where34 published results depend on correctness.3536## Characterization tests: the safety net3738Legacy code has no tests, so the first tests do not check that the39code is RIGHT - they pin down what it currently DOES:40411. Run the code on representative inputs and capture the outputs.422. Turn each captured run into an automated test asserting today's43 output (golden-master / snapshot testing). For floating-point44 results, assert within tolerances, and record the tolerance45 decision.463. Only then start changing code, keeping the characterization tests47 green after every step.4849When a characterization test later fails on purpose (a bug fix changes50results), update it explicitly and record the scientific justification51in the commit and changelog - that changed number may need to reach52users of previous results (rseng-publishing-releasing). The rseng-testing53skill covers the mechanics; the discipline here is: no refactoring54without a pinned baseline.5556## Incremental modernization5758Never big-bang rewrite what you cannot yet test. Work in small,59reversible steps, each one commit-sized (rseng-version-control-review):6061- Strangler pattern: wrap the legacy core behind a clean interface,62 route new work through the interface, and replace the inside63 piece by piece while the outside stays stable.64- Seams first: to test an untestable function, introduce the smallest65 change that lets you inject inputs or observe outputs (extract a66 function, parameterize a hard-coded path) - then test, then67 refactor.68- One concern per pass: formatting-only commits, then dead-code69 removal, then restructuring. Never mix behavior changes into70 cleanup commits - reviewers must be able to trust that a "cleanup"71 diff changes nothing.72- Add modern hygiene as you touch things, not wholesale: linting and73 CI on the files you changed (rseng-code-quality, rseng-ci-cd) rather74 than a repository-wide reformat that destroys git blame.7576## Language and stack migrations7778For Fortran-to-Python, MATLAB-to-Python, Python 2-to-3, IDL and79similar migrations:8081- Migrate by module with the characterization tests as the contract:82 old and new implementations must agree on the pinned outputs within83 stated tolerances before the old one is deleted.84- Consider wrapping instead of translating: mature Fortran/C numeric85 cores are often best kept and called from a modern interface86 (f2py, ctypes, Rcpp) - translation risk is highest exactly where87 the code is most optimized.88- Expect floating-point differences across languages, compilers and89 BLAS implementations; decide tolerances scientifically, with the90 domain expert, not by loosening until tests pass.91- Record the migration in the project history and cite the original92 authors - the old code is a research contribution93 (rseng-citation-metadata).9495## Refactor, rewrite or retire9697Make the decision explicit and write it down98(rseng-management-planning):99100- Refactor when results still matter and the code mostly works -101 the default choice.102- Rewrite only with characterization tests as the acceptance103 criterion, and keep the legacy version runnable until parity is104 proven.105- Retire when nothing current depends on it - but archive properly106 rather than delete: a tagged release plus a public archive keeps107 the record citable (rseng-publishing-releasing; Software Heritage108 archives source code for exactly this purpose).109110## Working with this skill111112This skill is source-independent: it encodes established legacy-code113practice (characterization testing, seams, strangler migration)114applied to research software.115116Learn more (verified):117 - https://refactoring.com - Fowler's refactoring catalog and book118 - https://archive.softwareheritage.org - universal source code119 archive, for preserving and citing retired research code120121<!-- related-skills:begin -->122123## Related skills124125Check whether any of these applies before moving on:126127- rseng-archiving - retire path ends in archive128- rseng-documentation - recording recovered intent129- rseng-open-source-migration - commercial-platform exits build on this130- rseng-reproducible-environments - capture the working environment first131- rseng-testing - characterization test mechanics132- rseng-version-control-review - small reversible cleanup commits133134<!-- related-skills:end -->