scientific-method-engine
Bounded instruction-derived x86 evidence reports for segmented 16-bit MZ/FBOV code and
PE32/i386 code. The engine decodes instructions with Capstone, takes their values, flags and
branch conditions from Ghidra's SLEIGH specification through pypcode, follows bounded paths and
emits bounded-x86-v1 JSON. It never runs the original program. It needs Python 3.12 or later.
uv add --group research scientific-method-engine # or: pip install scientific-method-engine
For original MZ/FBOV executables, run reports through
@scientific-method/executable-reader. The
reader derives relocation, fixup and trampoline data from the hash-checked source and pipes a
prepared config to this engine. Running the engine directly trusts whatever relocation data the
config supplies, so use it directly only for synthetic inputs, PE32 sources and checked mappings:
scientific-method-engine trace analysis/query.json
python -m scientific_method_engine trace analysis/query.json
The package also carries the shared Ghidra headless scripts. scientific-method-engine ghidra-scripts prints their directory, for Ghidra's -scriptPath:
& "$env:GHIDRA_HOME/support/analyzeHeadless.bat" $project $name -process GAME.EXE -noanalysis `
-scriptPath (scientific-method-engine ghidra-scripts) -postScript ReportReferences.java 0x1234
Addresses are Ghidra addresses (0x00401000, or 1028:d820 for segmented programs). Report scripts
print to the analyzer log and cap their output. ReportScalarConstants, ReportCallsToRange,
ReportConstantFirstArgumentCalls and ReportCallSitesWithScalars say when a cap stopped their scan;
for the other report scripts, a result count equal to the cap means the same.
analyzeHeadless can exit with code 0 after a script failed to load, so check the log for the
script's own result lines (the Ghidra workflow
says what a run must show). The scripts compile against Ghidra 12.1.
Reading code and data:
| Script | Arguments | Prints |
|---|---|---|
ReportInstructionContext |
one or more instruction addresses | a bounded instruction window around each |
ReportInstructionWindow |
address, instruction count | instructions from the address onward |
ReportDataBytes |
address, byte count (1..256) | the bytes at the address |
ReportFunctionSummary |
one or more addresses | focused decompiler output of each containing function, its body ranges and each call without a fall-through (a sign of a wrong no-return flag), then the addresses with no function or a failed decompile |
ReportDecompileWindow |
address, first line (1-based), line count (a count above 160 is cut to 160) | a window of one function's decompilation, its total line count and where the next window starts |
ReportDecompileMatches |
address, one or more literal text patterns | decompilation lines around each match |
ReportMemoryBlocks |
nothing, page <start> <count>, or name <exact-name> |
memory block indexes, names, ranges and sizes, never bytes |
ReportFilePatternInMemory |
file offset (hex), optional pattern length (default 8) | where the bytes at that file offset occur in loaded memory |
ReportMemoryBlockForFileOffset |
one or more file offsets (hex) | the memory block and instructions where each offset's bytes are loaded |
Finding references and calls:
| Script | Arguments | Prints |
|---|---|---|
ReportReferences |
one or more addresses | references to each, with the referring instruction and function |
ReportStringReferences |
one or more literal string fragments | strings containing a fragment and their references |
ReportSymbolReferences |
one or more symbol-name fragments, matched as case-insensitive substrings of the full name | matching symbols, default labels included, and their references. Default labels end in their address, so an address fragment such as 0089d4a4 finds the PTR_<name>_0089d4a4 import slot there |
ReportScalarConstants |
optional operand kind (immediate or memory), one or more scalar values |
instructions using any of them, unsigned or signed, as an immediate or inside a memory operand (a displacement such as [ECX + 0x44], an absolute address such as [0x41c000], or an index scale), with the kind on each line |
ReportFunctionScalarConstants |
function address, one or more scalar values | instructions inside one function using any of them, compared unsigned |
ReportCallArguments |
callee address | the three nearest pushed arguments at every call Ghidra references to the callee |
ReportCallSitesWithScalars |
callee address, one or more scalar values | calls whose argument setup contains a requested value as an immediate, never as a memory-operand displacement or address. The setup is up to 12 instructions that fall through to the call, ending after a function entry or a jump or call target, and before an earlier call. Then the counts or the cap |
ReportConstantFirstArgumentCalls |
callee address, constant | cdecl calls whose first argument is the constant: the nearest PUSH before the call, past instructions that fall through, are no function entry or jump or call target, and write neither the stack pointer nor memory addressed through it. Only calls Ghidra references to the callee are read |
ReportFirstArgumentCallSummary |
callee address | the literal first argument of every call, read as in ReportConstantFirstArgumentCalls, and each call without one with the reason |
ReportCallsToRange |
start address, end address (inclusive), optional kind (all, calls or jumps; default all) |
calls and jumps whose target lies in the range, each labelled [call] or [jump], then the counts or the cap |
ReportCallPaths |
start function, target function, maximum depth | direct-call paths between the two |
ReportRandomnessCandidates |
none | references to C runtime and Windows random and timing functions |
Exporting for comparison (each writes one file and refuses to overwrite where noted):
| Script | Arguments | Writes |
|---|---|---|
ExportBoundedFlow |
entry, instruction limit (1..10000), output path under analysis/original/ |
instruction metadata of one bounded flow as JSON |
ExportFunctionInventory |
output TSV path (must not exist) | every function's start and body size |
ExportCallEdges |
output JSON path (must not exist), function limit (1..128), one or more function entries | the call and tail-jump edges of the functions Ghidra reaches breadth-first from the entries, with file offsets, as the ghidraCallEdges input of callees |
ExportFunctionFingerprints |
output TSV path (replaced only when the export completes) | per-function and per-instruction fingerprints with addresses normalized, for matching functions across versions |
Repairing the analysis (these change the Ghidra program, so run them before reports and keep the argument lists with the evidence that justifies them):
| Script | Arguments | Changes |
|---|---|---|
CreateFunctions |
one or more entry addresses | creates functions at indirect-call targets Ghidra missed |
RecoverCitedFunctions |
file of 8-digit hex addresses, optional CSV column | disassembles and creates a function at each address inside executable memory |
ClearNoReturnFunctions |
one or more addresses | clears a wrong no-return flag on each containing function |
RepairReturningCallers |
callee entry, caller entry, verified call addresses | checks each call targets the callee from inside the caller, clears the callee's no-return flag and the calls' flow overrides, disassembles each continuation and recomputes the caller's body |
MergeFallThroughFragment |
parent entry, fragment entry | merges an orphan fragment reached by the parent's fall-through into the parent |
Restoration tools that build report configs can import the parsers directly, for example to derive a PE32 source's executable sections:
from scientific_method_engine.x86.pe import pe32
sections = [s for s in pe32(data)["sections"] if s["executable"]]
scientific_method_engine.x86.pe.pe32 and scientific_method_engine.x86.image.read_source are
supported imports and change only in a major release. Other modules are internal.
Commands, inputs, limits and acceptance rules are in the bounded evidence reporter guide. The reader and engine check that they speak the same prepared-config protocol and refuse to run otherwise.
The effects command additionally emits path-local effectOrdering timelines,
pre-call write prefixes and bounded local restoration witnesses. Unknown modeled
or nested service effects stay separate; no return code establishes rollback or
transactionality. See the full contract.
Each traced path carries a loops record: the restart edges the path took and, between
consecutive arrivals at a loop head, which registers, flags and written bytes changed, the
signedness and operands of each gate, and whether the state repeats an earlier arrival. It never
reports that a loop terminates or that a retry succeeded. loopIterationLimit caps the records.
See loop progress.
Evidenced segmented16 indirectJumps declarations also expose separate
declaredContinuationPaths. Ordinary paths retain their unresolved transfer;
conditional routes preserve prefix/child effects with target-choice, live-table
and selector assumptions. Effects summaries never mark those routes complete.
Concrete values/field addresses reject inconsistent rows; overlap and shared
path/step/visit/total/boundary limits remain explicit. Partial-table splits are
partial evidence, never complete dispatch or native-reachability claims. See the
table contract.
A call model may declare preservesMemory: up to 32 scopes (4,096 bytes in total), each naming a
segment register, an address-width base register, an optional displacement, a byte count and the
evidence for assuming the service leaves those bytes alone. Scopes resolve against the pre-call
registers. An unknown, wrapping or overlapping interval stops the path. Only the scoped bytes
survive the model's memory invalidation, and each scope is reported with the model, the modeled
return and the effect summary. Everything else the service may do stays unknown. This input needs
prepared protocol 3, so upgrade the reader and the engine together. See the
call model contract.
The trace-family commands (trace, arguments, effects, returns, guards, memory,
allocation) check relationalControls: assertions over reported values (reach, order, last
writer, containment, value relation and value origin) evaluated on every bounded path. A violated
control fails the report. A control left undecided by a stop, a limit or an unread call is
reported as undecided and never counts as held. Access reports also give each byte's
writeOrder, or why it has no modeled value. See the
controls contract.
Metadata
Release files for scientific-method-engine 3.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| scientific_method_engine-3.2.0.tar.gz | 186.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| scientific_method_engine-3.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 343.4 kB
Release files / scientific_method_engine-3.2.0.tar.gz
| Download URL | scientific_method_engine-3.2.0.tar.gz |
|---|---|
| Size | 186.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
85a6f7b1ec7a72e69def7af0549dec2c5881723c9b71f7e4b1f2d1435946ebb6
|
|
BLAKE2b-256 checksum How to use checksums |
1137cf654c620e1728a868aa7d986bce0ab58197f93db12df7be4e59fc52baec
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency logRelease files / scientific_method_engine-3.2.0-py3-none-any.whl
| Download URL | scientific_method_engine-3.2.0-py3-none-any.whl |
|---|---|
| Size | 157.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
41d5b8ca5d7dcea47affdc31f37ea562a1eb4882c5ef9a8d97a049953eb66dcf
|
|
BLAKE2b-256 checksum How to use checksums |
024f5ac1c469884f456ad74d54a9430269802b555265c53493d8490ce631f5c7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 4, 2026.
Transparency log