A reproducible toolchain that takes an upstream TripleA checkout and produces:
port.sqlite— a SQLite database with one row per Java class, interface, enum, and method that runs at game time, layered by dependency depth, with a column tracking implementation status.odin_flat/*.odin— one blank Odin source file per Java owner class, named to match the database, ready for the porter to fill in.- A trace of what the running game actually executes (zero-miss bytecode instrumentation via JaCoCo, not sampling-based JFR).
The intended use: the resulting database becomes a perfect global hit-list for an LLM-driven Java→Odin port, with a deterministic porting order (layer 0 → layer 24) and an authoritative implementation tracker.
We clone TripleA, inject a small JUnit test (Ww2v5JacocoRun.java) into the
smoke-testing module, patch its Gradle file (Groovy or Kotlin DSL — both
supported) so JaCoCo can produce a useful aggregated report, run that test
on the WW2v5_1942_2nd map for several rounds, and capture a JaCoCo XML.
The injected test depends only on stable upstream APIs
(GameTestUtils.setUpGameWithAis), so the bootstrap survives upstream
churn (default branch renames, DSL migrations, etc.). We then walk every
compiled .class file with javap to enumerate every type and method
plus its dependency edges, write that into a SQLite schema, mark the
subset that JaCoCo observed actually executing, and compute layer numbers
via Tarjan SCC condensation + topological depth on two graphs (the full
reference graph and a "what-if ID-based design" graph that drops field-
type edges, leaving only inheritance — the latter is a clean DAG and is
the recommended layout for the Odin port).
- Linux (tested on NixOS WSL)
- Internet access (to clone TripleA)
- ~4 GB RAM, ~3 GB disk
- Either:
- Nix with flakes enabled (recommended; pulls JDK 21, Odin, gradle,
Python, SQLite from a pinned
nixpkgs), or - JDK 21, Python 3.11+, sqlite3, git installed manually. Odin is only needed if you intend to compile the produced stubs.
- Nix with flakes enabled (recommended; pulls JDK 21, Odin, gradle,
Python, SQLite from a pinned
If using Nix:
nix develop
drops you into a shell with everything in $PATH.
nix develop
./bootstrap.shThat's it. Total runtime: ~5 minutes (JaCoCo run dominates).
The script is idempotent — every step skips itself if its output already exists.
If you want to run each step manually (to inspect, to experiment, to
re-run after a code change), the steps are listed below. They use the
same env vars as bootstrap.sh. You can prefix any line with WORK_DIR=...
to redirect outputs.
git clone https://github.com/triplea-game/tripleaThree edits, all idempotent:
- Inject
templates/Ww2v5JacocoRun.javaintogame-app/smoke-testing/src/test/java/org/triplea/portbootstrap/. This is a small JUnit test that runs WW2v5_1942_2nd.xml for--roundsrounds using only the stable upstream APIGameTestUtils.setUpGameWithAis. - Append a JaCoCo aggregator block to
game-app/smoke-testing/build.gradle.kts(or.gradle, auto-detected). Without this, the report is empty.
python3 scripts/patch_triplea.py --triplea triplea --rounds 8The smoke-testing module's test classes hold the actual JaCoCo entry-point
(Ww2v5JacocoRun.run, SnapshotHarness.wrapStep, GameTestUtils). They
are scanned by extract_entities.py --include-tests so layering bottoms
out at the real top-of-stack.
( cd triplea && ./gradlew --no-daemon compileJava compileTestJava \
-x checkstyleMain -x checkstyleTest -x pmdMain -x pmdTest )( cd triplea && ./gradlew --no-daemon \
:game-app:smoke-testing:test \
--tests "*Ww2v5JacocoRun.run" \
:game-app:smoke-testing:jacocoTestReport \
--rerun-tasks \
-x checkstyleMain -x checkstyleTest -x pmdMain -x pmdTest )This produces triplea/game-app/smoke-testing/build/jacoco.xml.
Runs after JaCoCo so the test classes are guaranteed compiled and the harness procs are scanned in the same javap pass as main.
This walks every .class file and parses javap -p -c -s -v output to extract:
- One
struct:fqcnrow per class/interface/enum (withstruct_kind). - One
proc:fqcn#name(args)row per method (withis_abstract). - One edge per
extends/implements/new/invoke*/get/putfieldreference, plus method-reference lambdas pulled from the constant pool. - Each row scanned from
build/classes/java/testis taggedis_test_harness = 1.
Run with --include-tests (or INCLUDE_TEST_CLASSES=1) to include the
harness:
INCLUDE_TEST_CLASSES=1 python3 scripts/extract_entities.py \
--db port.sqlite --triplea triplea --include-testsOutput: ~20,000 entities, ~130,000 dependency edges.
Sets entities.actually_called_in_ai_test = 1 for every class/method
that JaCoCo observed executing.
python3 scripts/apply_jacoco.py \
--db port.sqlite \
--xml triplea/game-app/smoke-testing/build/jacoco.xmlRe-layers using only entities flagged actually_called_in_ai_test=1.
Uses Tarjan's algorithm so cycles collapse into single layer-bands.
python3 scripts/build_called_layered_tables.py --db port.sqliteRe-runs the layering with only the inheritance edges (extends /
implements). Under the ID-based design — where every cross-struct field
becomes a *_Id :: distinct u32 — these are the only unbreakable
edges, and the result is a clean DAG (depth ~5, zero cycles).
python3 scripts/id_design_layering.py --db port.sqlite --triplea tripleaOne file per Java owner class, named with snake_case + __ separators.
Each file gets a TODO header so file-level scans can detect it as
"not yet implemented" until the developer/LLM removes the marker.
python3 scripts/generate_odin_stubs.py \
--db port.sqlite --out odin_flatThe path is also written to each row's odin_file_path column.
After a clean run:
$WORK_DIR/
triplea/ # upstream checkout, patched
port.sqlite # the tracker database
odin_flat/ # one blank .odin per Java class
jacoco.exec, jacoco.xml # raw coverage artifacts (under triplea/...)
Schema highlights:
-- All entities ever seen (unfiltered)
entities(primary_key, java_file_path, java_lines, odin_file_path,
layer_number, is_fully_implemented_error_free_no_todo_no_stub,
included, actually_called_in_ai_test)
-- Edge list
dependencies(primary_key, depends_on_key)
-- Called-only, layered (the canonical hit list for porting)
structs(struct_key, java_file_path, java_lines, odin_file_path,
is_implemented, struct_layer, scc_id, id_design_layer)
methods(method_key, owner_struct_key, java_file_path, java_lines,
odin_file_path, is_implemented, method_layer)Two layer columns are present for structs:
| column | graph used | meaning |
|---|---|---|
struct_layer |
full reference | layer with cycles collapsed (will contain a 50-struct band) |
id_design_layer |
inheritance only | layer in the cycle-free what-if DAG (recommended order) |
Recommended porting order: ascending id_design_layer, then ascending
scc_id, then alphabetical.
Once port.sqlite and odin_flat/ exist, the LLM's job is mechanical.
The next step after bootstrap.sh is to open a fresh chat and paste
the PROMPT block from resume-prompt.md. That
prompt is idempotent: it queries port.sqlite for unfinished work,
dispatches ~12 subagents in parallel (one per entity / .odin file),
updates is_implemented after each batch, and stops cleanly when the
context window fills. Re-paste the same prompt in a new chat to resume
until both structs and methods are 100% implemented and Phase C
snapshot validation passes.
What each subagent does:
- Reads the Java source at
java_file_path. - Translates it to Odin in
odin_file_path(struct only in Phase A, method body in Phase B). - References — never re-implements — types/procs already on disk
under
odin_flat/; lower-layer entities are guaranteed complete before higher layers begin. - Reports
done/blocked; the orchestrator updates the database.
Final verification is the snapshot harness: 52 paired before/after JSON snapshots captured per delegate step during the JaCoCo run.
See llm-instructions.md for the full ruleset
and subagent dispatch model, resume-prompt.md
for the copy-paste resumable prompt, and plan.md for
the high-level plan template.
- Coverage represents one seed × one map × one game. A different RNG
seed or map will exercise different code paths. To strengthen the
hit list, run step 5 multiple times with different seeds and OR the
results together. The schema already supports this:
apply_jacoco.pyuses anUPDATE ... SET = 1, so re-running with a second XML adds newly-covered entities without un-flagging existing ones. <clinit>, anonymous inner classes (Foo$1), and lambdas (lambda$N$M) are not first-class entities in the schema. JaCoCo reports them but the entity extractor folds them into their owner class. ~5% of JaCoCo's reported method count is unrepresented in the database for this reason.- The implementation flag is a heuristic when set en masse — file
exists ∧ no
FIXME|TODO|stubmarker. Per-symbol resolution requires a future pass.