106 KiB
AGENTS.md -- Migration Toolkit
READ THIS BEFORE TOUCHING ANY MIGRATION WORK. These tools exist because agents kept panicking on build failures and deleting progress. The toolkit eliminates the need for slow read/edit/write cycles on individual files. One command = hundreds of file modifications in seconds.
Golden Rules
- Tools are METHODS. You FEED them PARAMS. You NEVER edit their source. Unless they are broken or the core logic needs modification.
- Every tool follows one principle: NEVER DELETE. ONLY ADD AND TRANSFORM.
- Build failures after migration are EXPECTED. Do NOT panic. Do NOT delete files. Run
Build-Reportand fix categorically. - Always load tools from
.ps1files using dot-sourcing (. path/to/Script.ps1). NEVER paste inline PowerShell through bash --$escaping will destroy regex capture groups.
Quick Reference Card
| Tool | Type | Purpose | Key Params |
|---|---|---|---|
pa_batch.py |
Python | BATCH create 5+ Java files from JSON manifest (auto-derives paths) | manifest.json, --force, --dry-run |
pa_editor.py |
Python | BATCH edit existing files from JSON manifest (atomic insert/delete/replace) | manifest.json, --force, --dry-run, --backup |
pa_analyzer.py |
Python | Analyze PA member usage across mc1710 from classification + index data | --execute, --pa, --classification, --verbose |
Copy-Recursive |
PS | Copy files from mc1710 to core, recursively following imports | -Sources, -AllowPaths, -AllowRecursePaths |
Symbol-Swap |
PS | Replace type names + import surgery across all files | -Directory, -Swaps |
Nuke-Imports |
PS | Remove all forbidden imports from a directory | -Directory, -ForbiddenPrefixes |
Create-And-Propagate |
PS | Create a PA interface AND replace its MC equivalent everywhere atomically | -FilePath, -Content, -TargetDirectory, -Swaps |
Generate-Stubs |
PS | Create temporary placeholder classes so the build passes | -Stubs, -TargetRoot |
Build-Report |
PS | Run Gradle build and categorize errors into actionable buckets | -GradleTask, -MaxSamples |
Static-Transform |
PS | Regex-based method call rewriting (structural changes) | -Directory, -Transforms |
Dedupe-Imports |
PS | Remove duplicate imports and sort | -Directory |
PS tools auto-detect $ProjectRoot from script location. Override with -ProjectRoot.
pa_batch.py auto-detects ProjectRoot by walking up for settings.gradle. Override with --project-root.
How to Load Tools
Always write a .ps1 script file, then execute it with -File. This avoids bash $ escaping issues that destroy regex capture groups like $1, $2.
# In your .ps1 script file, load tools at the top:
. (Join-Path $PSScriptRoot 'Copy-Recursive.ps1')
. (Join-Path $PSScriptRoot 'Symbol-Swap.ps1')
. (Join-Path $PSScriptRoot 'Nuke-Imports.ps1')
. (Join-Path $PSScriptRoot 'Create-And-Propagate.ps1')
. (Join-Path $PSScriptRoot 'Generate-Stubs.ps1')
. (Join-Path $PSScriptRoot 'Build-Report.ps1')
. (Join-Path $PSScriptRoot 'Static-Transform.ps1')
. (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')
Then execute:
powershell -ExecutionPolicy Bypass -File "tools/migration/my_wave.ps1"
WARNING: Single-swap calls require ,@() syntax to prevent PowerShell array flattening:
# WRONG -- PowerShell flattens @(@('A','B','C','D')) into @('A','B','C','D')
Symbol-Swap -Directory '...' -Swaps @( @('Old', 'New', 'old.imp', 'new.imp') )
# CORRECT -- comma prefix forces array-of-arrays
Symbol-Swap -Directory '...' -Swaps @( ,@('Old', 'New', 'old.imp', 'new.imp') )
# Also correct -- 2+ entries don't need the comma trick
Symbol-Swap -Directory '...' -Swaps @(
@('OldA', 'NewA', 'old.a', 'new.a'),
@('OldB', 'NewB', 'old.b', 'new.b')
)
The tools have built-in guards for this, but it is still good practice.
Tool Reference
0. PA Batch Writer (Python)
File: pa_batch.py
Purpose: Batch-create 5+ Java source files from a single JSON manifest. Auto-derives file paths from package + class name. THE PRIMARY FILE CREATION TOOL — use this instead of individual write calls.
Usage
# Dry run (preview, no write)
python tools/migration/pa_batch.py manifest.json --dry-run
# Actually create files
python tools/migration/pa_batch.py manifest.json --force
# Machine-readable JSON output
python tools/migration/pa_batch.py manifest.json --dry-run --json
# Override project root
python tools/migration/pa_batch.py manifest.json --force --project-root /path/to/project
Manifest Format (JSON)
{
"root": "platform-api/src/main/java",
"files": [
{
"content": "package noppes.npcs.api.entity;\n\npublic interface IFoo extends IBar {\n String getName();\n}\n"
},
{
"root": "core/src/main/java",
"content": "package noppes.npcs.util;\n\npublic class Util {\n public static int clamp(int v, int min, int max) { ... }\n}\n"
},
{
"path": "src/main/java/noppes/npcs/wrappers/FooWrapper.java",
"content": "package noppes.npcs.wrappers;\n\npublic class FooWrapper implements IFoo { ... }\n"
}
]
}
Key Behaviors
Auto-path derivation (KILLER FEATURE):
- Parses
package noppes.npcs.api.entity;from content - Parses
public interface IFoofrom content - Derives path:
platform-api/src/main/java/noppes/npcs/api/entity/IFoo.java - No need to specify path for 90% of files — just write raw Java, tool figures out where it goes
Per-file root override:
- Global
rootapplies to all files unless overridden - Each file entry can specify its own
root(for mixed batches: PA interfaces + core + mc1710)
Explicit path override:
- If
pathis provided in entry, it takes precedence over auto-derivation - Useful for non-standard locations or when auto-derivation would fail
Safety defaults:
- No overwrite by default — existing files are skipped, listed in SKIPPED summary
- Use
--forceto overwrite existing files - Directories auto-created (
mkdir -p)
Validation:
- Warns if explicit
pathdoesn't match the package declaration (detects typos) - Errors if
contentfield missing - Errors if no
packagedeclaration found (and no explicitpath) - Errors if no
public class/interface/enum/@interfacefound (and no explicitpath)
CLI Options
| Option | Default | Description |
|---|---|---|
manifest |
(required) | Path to JSON manifest file |
--force |
false | Overwrite existing files (default: skip) |
--dry-run |
false | Preview what would be created without writing |
--json |
false | Machine-readable JSON output (instead of human-readable) |
--project-root |
auto-detect | Project root (auto-walks up for settings.gradle) |
Output
Human-readable (default):
PA Batch Writer — 20 files processed
CREATED: 12 files
SKIPPED: 8 files (already exist)
ERRORS: 0
Created:
platform-api/src/main/java/noppes/npcs/api/entity/IFoo.java
platform-api/src/main/java/noppes/npcs/api/entity/IBar.java
...
Machine-readable (--json):
{
"status": "success",
"created_count": 12,
"skipped_count": 8,
"error_count": 0,
"created": ["platform-api/src/main/java/noppes/npcs/api/entity/IFoo.java", ...],
"skipped": ["platform-api/src/main/java/noppes/npcs/api/nbt/INbt.java", ...],
"errors": [],
"warnings": []
}
Performance Impact
Batch creation eliminates tool call overhead:
- Creating 30 files individually = 30+ tool calls = 60-90s overhead
- Creating 30 files via pa_batch.py = 2 tool calls (write manifest + execute) = 4-6s overhead
- Result: 15x fewer round trips for large batches
Token usage:
- Both approaches output the same Java source code
- Difference: batching reduces tool-call ceremony overhead, not token generation
Example Workflow
# Step 1: Compose manifest with PA interfaces + wrappers
cat > tools/migration/_my_wave_manifest.json << 'EOF'
{
"root": "platform-api/src/main/java",
"files": [
{ "content": "package noppes.npcs.api.entity; public interface IFoo { ... }" },
{ "content": "package noppes.npcs.api.entity; public interface IBar { ... }" },
{ "root": "src/main/java", "content": "package noppes.npcs.wrappers; public class FooWrapper { ... }" }
]
}
EOF
# Step 2: Dry-run to verify paths
python tools/migration/pa_batch.py tools/migration/_my_wave_manifest.json --dry-run
# Step 3: Create files
python tools/migration/pa_batch.py tools/migration/_my_wave_manifest.json --force
# Step 4: Propagate (existing tools)
Symbol-Swap -Directory 'core/src/main/java' -Swaps @(
@('Foo', 'IFoo', 'net.minecraft.foo.Foo', 'noppes.npcs.api.entity.IFoo')
)
When to Use pa_batch.py vs Create-And-Propagate
| Scenario | Tool |
|---|---|
| Creating 1 PA interface + propagating references everywhere | Create-And-Propagate |
| Creating 5+ PA interfaces / classes in one batch | pa_batch.py |
| Need to compose files programmatically (from code generation) | pa_batch.py (write manifest, run tool) |
| Creating mixed batch (PA interfaces + core + mc1710 wrappers) | pa_batch.py (per-file root override) |
| One-off single file | Direct write tool (slower but acceptable) |
0b. PA Batch Editor (Python)
File: pa_editor.py
Purpose: Batch-edit existing files from a single JSON manifest with atomic transaction semantics. THE PRIMARY FILE EDITING TOOL — use this for surgical multi-file edits instead of individual edit calls.
Companion to pa_batch.py (which creates files). This tool EDITS existing files.
Usage
# Dry run — preview diffs without writing (default if --force not given)
python tools/migration/pa_editor.py manifest.json --dry-run
# Actually apply edits
python tools/migration/pa_editor.py manifest.json --force
# Apply with backups (.bak files)
python tools/migration/pa_editor.py manifest.json --force --backup
# Machine-readable JSON output
python tools/migration/pa_editor.py manifest.json --dry-run --json
# Override project root
python tools/migration/pa_editor.py manifest.json --force --project-root /path/to/project
Manifest Format (JSON)
{
"edits": [
{
"path": "core/src/main/java/noppes/npcs/controllers/FactionController.java",
"ops": [
{
"op": "replace",
"target": {"pattern": "^import net\\.minecraft\\.nbt\\.NBTTagCompound;"},
"content": "import noppes.npcs.api.nbt.INbt;"
},
{
"op": "insert",
"target": {"pattern": "^public class"},
"position": "before",
"content": "/** Migrated to PA types. */"
},
{
"op": "delete",
"target": {"line": 5},
"count": 2
}
]
}
]
}
Operations
| Op | Required Fields | Description |
|---|---|---|
replace |
target, content |
Replace count lines (default 1) starting at target with content |
insert |
target, content, position |
Insert content before/after target line. position: "before" or "after" (default: "after") |
delete |
target |
Delete count lines (default 1) starting at target |
Targeting
Each op has a target that resolves to a line. Two modes:
| Mode | Format | Description |
|---|---|---|
| Line number | {"line": 10} |
1-based line number |
| Regex pattern | {"pattern": "^import.*NBT"} |
First line matching regex |
| Pattern + occurrence | {"pattern": "^import", "occurrence": 3} |
3rd match of pattern |
Content Format
content accepts either a string or a list of strings:
{"content": "single line replacement"}
{"content": "line 1\nline 2\nline 3"}
{"content": ["line 1", "line 2", "line 3"]}
Atomicity
All-or-nothing transaction model:
- PHASE 1: Validate all edit specs structurally (no I/O)
- PHASE 2: Read all files, resolve all targets, compute all results in memory
- PHASE 3: Write all files (or rollback on any failure)
If ANY edit in ANY file fails validation or application, NO files are written. This prevents partial state where some files are edited and others aren't.
CLI Options
| Option | Default | Description |
|---|---|---|
manifest |
(required) | Path to JSON edit manifest file |
--force |
false | Actually write edits (default: dry-run) |
--dry-run |
true | Show diffs without writing |
--backup |
false | Save .bak copies before editing |
--json |
false | Machine-readable JSON output |
--project-root |
auto-detect | Project root override |
Output
Human-readable (default):
PA Batch Editor — 4 files, 14 edits
MODIFIED: 4 files
ERRORS: 0
Modified:
core/.../FactionController.java (3 edits)
core/.../Faction.java (4 edits)
Dry-run shows unified diffs:
--- a/core/.../FactionController.java
+++ b/core/.../FactionController.java
@@ -3,4 +3,4 @@
-import net.minecraft.nbt.NBTTagCompound;
+import noppes.npcs.api.nbt.INbt;
When to Use pa_editor.py vs Other Tools
| Scenario | Tool |
|---|---|
| Bulk import replacement across all files in a directory | Symbol-Swap (PS) |
| Surgical edits to specific lines in specific files | pa_editor.py |
Adding implements clauses, inserting methods, deleting blocks |
pa_editor.py |
| Post-migration fixups (add imports, change signatures) | pa_editor.py |
| Creating new files from scratch | pa_batch.py |
| Regex-based method call rewriting | Static-Transform (PS) |
Example: Post-Migration Import + Signature Fixup
{
"edits": [
{
"path": "core/src/main/java/noppes/npcs/controllers/QuestController.java",
"ops": [
{"op": "replace", "target": {"pattern": "^import net\\.minecraft\\.nbt\\.NBTTagCompound;"}, "content": "import noppes.npcs.api.nbt.INbt;"},
{"op": "replace", "target": {"pattern": "^import net\\.minecraft\\.nbt\\.NBTTagList;"}, "content": "import noppes.npcs.api.nbt.INbtList;"},
{"op": "insert", "target": {"pattern": "^import noppes.npcs.api.nbt.INbt;"}, "position": "after", "content": "import noppes.npcs.api.nbt.NBT;"},
{"op": "replace", "target": {"pattern": "public void loadData\\(NBTTagCompound"}, "content": " public void loadData(INbt nbt) {"}
]
},
{
"path": "core/src/main/java/noppes/npcs/controllers/DialogController.java",
"ops": [
{"op": "replace", "target": {"pattern": "^import net\\.minecraft\\.nbt\\.NBTTagCompound;"}, "content": "import noppes.npcs.api.nbt.INbt;"},
{"op": "insert", "target": {"pattern": "public class DialogController"}, "position": "before", "content": "/** Migrated to core — platform-api types only. */"}
]
}
]
}
Testing
python tools/migration/test_pa_editor.py
17 tests covering: replace, insert before/after, delete, multi-line insert, multi-line replace, dry-run, JSON output, backup, file-not-found, line-out-of-bounds, pattern-not-found, invalid-regex, atomicity (partial failure rollback), multi-file, occurrence targeting, list content, structural validation.
1. Copy-Recursive
File: Copy-Recursive.ps1
Purpose: Recursively copy Java files from mc1710 to core, chasing import dependencies.
| Param | Required | Default | Description |
|---|---|---|---|
-Sources |
Yes | -- | Array of relative paths within McRoot (e.g., 'noppes/npcs/controllers/DialogController.java') |
-AllowPaths |
No | @() (all allowed) |
WHITELIST of path prefixes. Only files under these paths get recursively copied. Initial -Sources always bypass this. |
-AllowRecursePaths |
No | @() (all recursed) |
RECURSION WHITELIST: only follow/scan imports of files whose path starts with one of these prefixes. Files from non-whitelisted paths are still COPIED but their imports are NOT recursively followed. Initial -Sources always have their imports followed. Different from -AllowPaths — see below. |
-McRoot |
No | 'mc1710/src/main/java' |
Source root |
-CoreRoot |
No | 'core/src/main/java' |
Destination root |
-ProjectRoot |
No | Auto-detected | Absolute project root |
-ForbiddenPrefixes |
No | java/MC/Forge/etc | Import prefixes never followed |
-AllowPaths vs -AllowRecursePaths — Critical Distinction:
-AllowPaths = WHERE files can be copied TO (destination filter)
-AllowRecursePaths = WHERE imports can be recursively FOLLOWED FROM (recursion depth filter)
| Scenario | -AllowPaths |
-AllowRecursePaths |
Result |
|---|---|---|---|
| File in controllers/ imports file in util/ | util/ in AllowPaths | util/ NOT in AllowRecursePaths | util/ file IS copied but its imports are NOT followed |
| File in controllers/ imports file in roles/ | roles/ NOT in AllowPaths | N/A | roles/ file is NOT copied at all |
| File in controllers/ imports file in config/ | config/ in AllowPaths | config/ in AllowRecursePaths | config/ file IS copied AND its imports ARE followed |
Behavior:
- Initial
-Sourcesfiles are ALWAYS copied regardless of whitelist. - Initial
-Sourcesfiles ALWAYS have their imports followed regardless of-AllowRecursePaths. - Files already in core are NOT re-copied but ARE scanned for transitive deps (if recursion allowed).
- Handles wildcard imports (
import foo.bar.*), static imports, inner class imports. - Returns array of copied file paths.
Output summary shows: COPIED, EXISTS, skipped(not whitelisted), recursion restricted, not found.
Summary line format: Copied: X files (Y initial sources, Z recursively discovered, A non-recurse imports)
Usage example with -AllowRecursePaths:
# Copy DialogController, allow copying from controllers/ and util/,
# but only recurse into imports from controllers/.
# Files from util/ will be copied as dependencies but their imports won't be followed.
Copy-Recursive `
-Sources @('noppes/npcs/controllers/DialogController.java') `
-AllowPaths @('noppes/npcs/controllers/', 'noppes/npcs/util/') `
-AllowRecursePaths @('noppes/npcs/controllers/')
2. Symbol-Swap
File: Symbol-Swap.ps1
Purpose: Bulk replace type names and perform import surgery across all .java files in a directory.
| Param | Required | Default | Description |
|---|---|---|---|
-Directory |
Yes | -- | Relative path to scan recursively |
-Swaps |
Yes | -- | Array of @(oldSymbol, newSymbol, oldImport, newImport) |
-ProjectRoot |
No | Auto-detected | Absolute project root |
Swap tuple format: @('OldTypeName', 'NewTypeName', 'old.full.qualified.Import', 'new.full.qualified.Import')
- Use
''(empty string) for newImport if no import should be added.
Operation order (critical for correctness):
- Remove ALL old imports first
- Replace symbols in code body
- Add new imports
This prevents the bug where replacing NBTTagCompound with INbt inside an import line makes the import unremovable.
3. Nuke-Imports
File: Nuke-Imports.ps1
Purpose: Remove ALL imports matching forbidden prefixes from every .java file in a directory.
| Param | Required | Default | Description |
|---|---|---|---|
-Directory |
Yes | -- | Relative path to scan recursively |
-ForbiddenPrefixes |
Yes | -- | Array of dotted package prefixes (e.g., 'net.minecraft.') |
-ProjectRoot |
No | Auto-detected | Absolute project root |
When to use: Run AFTER Copy-Recursive, BEFORE Symbol-Swap. Removes the worst offenders so Symbol-Swap only handles the semantic replacements.
4. Create-And-Propagate
File: Create-And-Propagate.ps1
Purpose: Atomically create a new file (usually a PA interface) AND replace its MC equivalent everywhere.
| Param | Required | Default | Description |
|---|---|---|---|
-FilePath |
Yes | -- | Relative path for the new file (e.g., 'platform-api/src/main/java/.../IFoo.java') |
-Content |
Yes | -- | Full Java source content of the new file |
-TargetDirectory |
Yes | -- | Relative path to scan for propagation |
-Swaps |
Yes | -- | Same format as Symbol-Swap |
-ProjectRoot |
No | Auto-detected | Absolute project root |
Why this exists: Creation and propagation are ATOMIC. The interface and ALL its usages appear in the same millisecond. No "I created the interface but forgot to update 3 files" bugs.
5. Generate-Stubs
File: Generate-Stubs.ps1
Purpose: Create temporary placeholder classes so the build passes despite missing future-wave dependencies.
| Param | Required | Default | Description |
|---|---|---|---|
-Stubs |
Yes | -- | Array of hashtables with pkg, name, type, body |
-TargetRoot |
No | 'core/src/main/java' |
Where stubs are created |
-ProjectRoot |
No | Auto-detected | Absolute project root |
Stub hashtable format:
@{ pkg='noppes.npcs.controllers'; name='SyncController'; type='class';
body=' public static void updateDialog(Object d) {}
public static void removeDialog(int id) {}' }
Safety: Will NOT overwrite real classes (only replaces files containing the TEMPORARY STUB marker).
6. Build-Report
File: Build-Report.ps1
Purpose: Run a Gradle build and categorize errors into actionable buckets instead of dumping raw output.
| Param | Required | Default | Description |
|---|---|---|---|
-GradleTask |
Yes | -- | Gradle task to run (e.g., ':core:compileJava') |
-MaxSamples |
No | 3 |
Max errors shown per category |
-ProjectRoot |
No | Auto-detected | Absolute project root |
Error categories:
MISSING_CLASS:ClassName-- needs stub or PA interfaceMISSING_PACKAGE:pkg.name-- leftover MC import, run Nuke-Imports or Symbol-SwapMISSING_METHOD:methodName-- method signature changed or missingMISSING_VARIABLE:varName-- field not migratedMISSING_SYMBOL-- general "cannot find symbol" (multi-line Gradle output)TYPE_MISMATCH-- incompatible types, usually needs a cast or wrapperBAD_ARGS-- wrong argument types to a methodABSTRACT_VIOLATION-- class doesn't implement required abstract methodsUNCHECKED_EXCEPTION-- missing throws/try-catchOTHER-- anything uncategorized
Returns: @{ Success = $bool; Errors = $hashtable; Total = $int }
7. Static-Transform
File: Static-Transform.ps1
Purpose: Regex-based method call rewriting. For structural changes that Symbol-Swap can't handle.
| Param | Required | Default | Description |
|---|---|---|---|
-Directory |
Yes | -- | Relative path to scan recursively |
-Transforms |
Yes | -- | Array of @(regexPattern, replacement) |
-ProjectRoot |
No | Auto-detected | Absolute project root |
Transform tuple format: @('regex\.pattern\(([^)]+)\)', 'replacement($1)')
- Uses .NET regex syntax.
$1,$2etc. are capture group references in the replacement.
8. Dedupe-Imports
File: Dedupe-Imports.ps1
Purpose: Remove duplicate import lines and sort. Run LAST, after all other tools.
| Param | Required | Default | Description |
|---|---|---|---|
-Directory |
Yes | -- | Relative path to scan recursively |
-ProjectRoot |
No | Auto-detected | Absolute project root |
Complex Workflow Examples
These examples show end-to-end migration workflows.
For PowerShell workflows: Write a .ps1 script file and execute with -File. NEVER paste inline through bash.
For Python + batch creation: Compose the JSON manifest in a file, then invoke pa_batch.py. Recommended for waves that need multiple PA interfaces.
PA Batch Writer Integration Pattern
When you need to create 5+ Java files in a migration wave, use this pattern:
# Step 1: Agent composes manifest JSON with all PA interfaces + wrappers + utility classes
write("tools/migration/_wave_manifest.json", {all files' raw Java content})
# Step 2: Verify paths with dry-run
python tools/migration/pa_batch.py tools/migration/_wave_manifest.json --dry-run
# Step 3: Create files
python tools/migration/pa_batch.py tools/migration/_wave_manifest.json --force
# Step 4: Propagate (existing PowerShell tools)
powershell -ExecutionPolicy Bypass -File tools/migration/wave_propagate.ps1
Benefit: 5+ files created in 2 tool calls (write manifest + execute) instead of 5+ individual write calls.
For single PA interface creation, use Create-And-Propagate (simpler, all-in-one). For batches, use pa_batch.py (higher throughput, lower overhead).
WORKFLOW 1: Full Wave Migration (Copy + Nuke + Swap + Stub + Build)
This is the standard workflow for migrating a batch of files from mc1710 to core. Used for any migration wave.
Scenario: Migrate Dialog system controllers and data classes.
# File: tools/migration/wave_dialog.ps1
# Execute: powershell -ExecutionPolicy Bypass -File tools/migration/wave_dialog.ps1
. (Join-Path $PSScriptRoot 'Copy-Recursive.ps1')
. (Join-Path $PSScriptRoot 'Nuke-Imports.ps1')
. (Join-Path $PSScriptRoot 'Symbol-Swap.ps1')
. (Join-Path $PSScriptRoot 'Static-Transform.ps1')
. (Join-Path $PSScriptRoot 'Generate-Stubs.ps1')
. (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')
. (Join-Path $PSScriptRoot 'Build-Report.ps1')
# ============================================================
# STEP 1: Copy files from mc1710 to core, recursively pulling deps
# ============================================================
# The whitelist constrains which TRANSITIVE deps get copied.
# Initial sources always get copied regardless of whitelist.
Copy-Recursive `
-Sources @(
'noppes/npcs/controllers/DialogController.java',
'noppes/npcs/controllers/data/Dialog.java',
'noppes/npcs/controllers/data/DialogOption.java',
'noppes/npcs/controllers/data/DialogCategory.java'
) `
-AllowPaths @(
'noppes/npcs/controllers/',
'noppes/npcs/controllers/data/',
'noppes/npcs/config/',
'noppes/npcs/util/'
)
# ============================================================
# STEP 2: Nuke ALL MC/Forge imports from core
# ============================================================
Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes @(
'net.minecraft.',
'cpw.mods.',
'net.minecraftforge.',
'noppes.npcs.entity.',
'noppes.npcs.client.',
'noppes.npcs.mixin.',
'kamkeel.npcs.network.',
'noppes.npcs.containers.'
)
# ============================================================
# STEP 3: Symbol-Swap - replace MC types with PA interfaces
# ============================================================
Symbol-Swap -Directory 'core/src/main/java' -Swaps @(
@('NBTTagCompound', 'INbt', 'net.minecraft.nbt.NBTTagCompound', 'noppes.npcs.api.nbt.INbt'),
@('NBTTagList', 'INbtList', 'net.minecraft.nbt.NBTTagList', 'noppes.npcs.api.nbt.INbtList'),
@('EntityPlayer', 'IPlayer', 'net.minecraft.entity.player.EntityPlayer', 'noppes.npcs.api.entity.IPlayer'),
@('EntityPlayerMP', 'IPlayer', 'net.minecraft.entity.player.EntityPlayerMP', 'noppes.npcs.api.entity.IPlayer'),
@('ItemStack', 'IItemStack','net.minecraft.item.ItemStack', 'noppes.npcs.api.item.IItemStack')
)
# ============================================================
# STEP 4: Static transforms - structural code rewrites
# ============================================================
Static-Transform -Directory 'core/src/main/java' -Transforms @(
@('new\s+NBTTagCompound\s*\(\)', 'NBT.compound()'),
@('new\s+NBTTagList\s*\(\)', 'NBT.list()'),
@('CompressedStreamTools\.readCompressed\s*\(([^)]+)\)', 'PlatformServiceHolder.get().readCompressedNBT($1)'),
@('CompressedStreamTools\.writeCompressed\s*\(([^,]+),\s*([^)]+)\)', 'PlatformServiceHolder.get().writeCompressedNBT($1, $2)')
)
# ============================================================
# STEP 5: Generate stubs for classes not yet migrated
# ============================================================
Generate-Stubs -Stubs @(
@{ pkg='noppes.npcs.controllers'; name='SyncController'; type='class';
body=' public static void updateDialog(Object d) {}
public static void removeDialog(int id) {}
public static void updateQuest(Object q) {}
public static void removeQuest(int id) {}' },
@{ pkg='noppes.npcs.controllers'; name='ScriptController'; type='class';
body=' public static boolean HasStart = false;' }
)
# ============================================================
# STEP 6: Deduplicate imports
# ============================================================
Dedupe-Imports -Directory 'core/src/main/java'
# ============================================================
# STEP 7: Build and report (DO NOT PANIC on errors)
# ============================================================
Build-Report -GradleTask ':core:compileJava'
What happens:
- Files copied from mc1710 to core with all allowed transitive deps
- MC imports stripped
- Types replaced with PA equivalents
- Constructor/static method patterns rewritten
- Missing deps stubbed
- Import duplicates cleaned
- Build errors categorized for targeted fixing
WORKFLOW 2: Create New PA Abstraction + Propagate Everywhere
This workflow is for when you need to introduce a NEW platform-api interface to replace an MC type. The interface is created AND all references across core/ are updated in a single atomic operation.
Scenario: Create IMessageService to replace ChatComponentText and ChatComponentTranslation.
# File: tools/migration/create_message_service.ps1
. (Join-Path $PSScriptRoot 'Create-And-Propagate.ps1')
. (Join-Path $PSScriptRoot 'Nuke-Imports.ps1')
. (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')
. (Join-Path $PSScriptRoot 'Build-Report.ps1')
# ============================================================
# STEP 1: Define the new interface content
# ============================================================
$interfaceContent = @'
package noppes.npcs.api.handler;
import noppes.npcs.api.entity.IPlayer;
/**
* Platform abstraction for Minecraft chat/message sending.
* mc1710 implementation wraps ChatComponentText/Translation.
*/
public interface IMessageService {
void sendMessage(IPlayer player, String message);
void sendTranslatedMessage(IPlayer player, String key, Object... args);
void broadcastMessage(String message);
}
'@
# ============================================================
# STEP 2: Create the interface AND propagate replacements
# ============================================================
Create-And-Propagate `
-FilePath 'platform-api/src/main/java/noppes/npcs/api/handler/IMessageService.java' `
-Content $interfaceContent `
-TargetDirectory 'core/src/main/java' `
-Swaps @(
@('ChatComponentText', 'IMessageService', 'net.minecraft.util.ChatComponentText', 'noppes.npcs.api.handler.IMessageService'),
@('ChatComponentTranslation', 'IMessageService', 'net.minecraft.util.ChatComponentTranslation', 'noppes.npcs.api.handler.IMessageService')
)
# ============================================================
# STEP 3: Also nuke leftover chat formatting imports
# ============================================================
Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes @(
'net.minecraft.util.EnumChatFormatting'
)
# ============================================================
# STEP 4: Clean up duplicates and verify
# ============================================================
Dedupe-Imports -Directory 'core/src/main/java'
Build-Report -GradleTask ':core:compileJava'
What happens:
IMessageService.javais created inplatform-api/- Every
ChatComponentTextandChatComponentTranslationreference in core/ is replaced withIMessageService - Old MC imports removed, new PA import added -- in every file that had the reference
- All in one atomic operation -- no partial state
WORKFLOW 3: Build-Report-Driven Fix Cycle
This workflow is for when you have build errors after a migration and need to fix them systematically. The pattern is: report -> categorize -> fix per category -> re-report -> repeat.
Scenario: After a migration wave, core has 47 build errors. Fix them categorically.
# File: tools/migration/fix_cycle.ps1
. (Join-Path $PSScriptRoot 'Symbol-Swap.ps1')
. (Join-Path $PSScriptRoot 'Nuke-Imports.ps1')
. (Join-Path $PSScriptRoot 'Generate-Stubs.ps1')
. (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')
. (Join-Path $PSScriptRoot 'Build-Report.ps1')
# ============================================================
# STEP 1: Get the initial error report
# ============================================================
Write-Host "===== INITIAL BUILD =====" -ForegroundColor Cyan
$report = Build-Report -GradleTask ':core:compileJava' -MaxSamples 5
if ($report.Success) {
Write-Host "Build already passes!" -ForegroundColor Green
exit 0
}
# ============================================================
# STEP 2: Fix MISSING_PACKAGE errors (leftover MC imports)
# ============================================================
# These mean Nuke-Imports missed some prefixes. Fix:
Write-Host "`n===== FIXING: Leftover MC imports =====" -ForegroundColor Yellow
Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes @(
'net.minecraft.',
'cpw.mods.',
'net.minecraftforge.',
'noppes.npcs.entity.',
'noppes.npcs.client.',
'noppes.npcs.mixin.',
'kamkeel.npcs.network.',
'kamkeel.npcs.entity.',
'noppes.npcs.containers.',
'noppes.npcs.wrapper.',
'noppes.npcs.scripted.',
'noppes.npcs.janino.'
)
# ============================================================
# STEP 3: Fix MISSING_CLASS errors (need stubs)
# ============================================================
# These are classes from future waves. Create stubs so build passes.
Write-Host "`n===== FIXING: Missing class stubs =====" -ForegroundColor Yellow
Generate-Stubs -Stubs @(
@{ pkg='noppes.npcs.controllers'; name='SyncController'; type='class';
body=' public static void syncUpdate(Object obj, Object... args) {}
public static void syncRemove(String type, int id) {}' },
@{ pkg='noppes.npcs.controllers'; name='ScriptController'; type='class';
body=' public static boolean HasStart = false;
public static boolean HasScriptEngine = false;' },
@{ pkg='noppes.npcs'; name='CustomNpcs'; type='class';
body=' public static java.io.File Dir = new java.io.File(".");
public static int DefaultInteractLine = 0;' },
@{ pkg='noppes.npcs'; name='EventHooks'; type='class';
body=' // STUB' },
@{ pkg='noppes.npcs'; name='LogWriter'; type='class';
body=' public static void info(String msg) {}
public static void warn(String msg) {}
public static void error(String msg) {}
public static void error(String msg, Throwable e) {}' }
)
# ============================================================
# STEP 4: Clean up and re-test
# ============================================================
Dedupe-Imports -Directory 'core/src/main/java'
Write-Host "`n===== SECOND BUILD =====" -ForegroundColor Cyan
$report2 = Build-Report -GradleTask ':core:compileJava' -MaxSamples 5
if ($report2.Success) {
Write-Host "`nAll errors fixed!" -ForegroundColor Green
} else {
Write-Host "`nRemaining errors: $($report2.Total) (was $($report.Total))" -ForegroundColor Yellow
Write-Host "Review the report above and create additional stubs or swaps as needed."
}
What happens:
- First build report shows categorized errors
MISSING_PACKAGEerrors fixed by nuking leftover MC importsMISSING_CLASSerrors fixed by generating stubs- Second build report shows remaining errors (should be much fewer)
- Agent reviews remaining errors and adds more fixes if needed
WORKFLOW 4: Targeted Controller Migration (Copy -> Swap -> Stub -> Verify)
This is the most common real-world pattern: migrate a specific controller and its data classes.
Scenario: Migrate QuestController and all quest data classes.
# File: tools/migration/wave_quests.ps1
. (Join-Path $PSScriptRoot 'Copy-Recursive.ps1')
. (Join-Path $PSScriptRoot 'Nuke-Imports.ps1')
. (Join-Path $PSScriptRoot 'Symbol-Swap.ps1')
. (Join-Path $PSScriptRoot 'Static-Transform.ps1')
. (Join-Path $PSScriptRoot 'Generate-Stubs.ps1')
. (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')
. (Join-Path $PSScriptRoot 'Build-Report.ps1')
# ============================================================
# STEP 1: Copy the quest subsystem and its dependencies
# ============================================================
# AllowPaths controls what transitive deps get pulled in.
# Quest files import controllers/data, config, and utils -- allow those.
# Do NOT allow entity/, client/, network/ -- those stay in mc1710.
Copy-Recursive `
-Sources @(
'noppes/npcs/controllers/QuestController.java',
'noppes/npcs/controllers/PlayerQuestController.java',
'noppes/npcs/quests/QuestItem.java',
'noppes/npcs/quests/QuestKill.java',
'noppes/npcs/quests/QuestLocation.java',
'noppes/npcs/quests/QuestDialog.java',
'noppes/npcs/quests/QuestManual.java'
) `
-AllowPaths @(
'noppes/npcs/controllers/',
'noppes/npcs/controllers/data/',
'noppes/npcs/quests/',
'noppes/npcs/config/',
'noppes/npcs/util/',
'kamkeel/npcs/controllers/',
'kamkeel/npcs/config/'
)
# ============================================================
# STEP 2: Nuke MC imports from core
# ============================================================
Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes @(
'net.minecraft.', 'cpw.mods.', 'net.minecraftforge.',
'noppes.npcs.entity.', 'noppes.npcs.client.',
'noppes.npcs.mixin.', 'kamkeel.npcs.network.',
'kamkeel.npcs.entity.', 'noppes.npcs.containers.',
'noppes.npcs.scripted.', 'noppes.npcs.janino.',
'noppes.npcs.wrapper.'
)
# ============================================================
# STEP 3: Symbol-Swap MC types for PA types
# ============================================================
Symbol-Swap -Directory 'core/src/main/java' -Swaps @(
@('NBTTagCompound', 'INbt', 'net.minecraft.nbt.NBTTagCompound', 'noppes.npcs.api.nbt.INbt'),
@('NBTTagList', 'INbtList', 'net.minecraft.nbt.NBTTagList', 'noppes.npcs.api.nbt.INbtList'),
@('EntityPlayer', 'IPlayer', 'net.minecraft.entity.player.EntityPlayer', 'noppes.npcs.api.entity.IPlayer'),
@('EntityPlayerMP', 'IPlayer', 'net.minecraft.entity.player.EntityPlayerMP', 'noppes.npcs.api.entity.IPlayer'),
@('ItemStack', 'IItemStack', 'net.minecraft.item.ItemStack', 'noppes.npcs.api.item.IItemStack'),
@('EntityLivingBase', 'IEntityLivingBase', 'net.minecraft.entity.EntityLivingBase', 'noppes.npcs.api.entity.IEntityLivingBase'),
@('EntityNPCInterface', 'ICustomNpc', 'noppes.npcs.entity.EntityNPCInterface', 'noppes.npcs.api.entity.ICustomNpc')
)
# ============================================================
# STEP 4: Static transforms
# ============================================================
Static-Transform -Directory 'core/src/main/java' -Transforms @(
@('new\s+NBTTagCompound\s*\(\)', 'NBT.compound()'),
@('new\s+NBTTagList\s*\(\)', 'NBT.list()')
)
# ============================================================
# STEP 5: Stubs for deps not yet migrated
# ============================================================
Generate-Stubs -Stubs @(
@{ pkg='noppes.npcs.controllers'; name='SyncController'; type='class';
body=' public static void updateQuest(Object q) {}
public static void removeQuest(int id) {}
public static void updateDialog(Object d) {}
public static void removeDialog(int id) {}' },
@{ pkg='noppes.npcs.controllers'; name='ScriptController'; type='class';
body=' public static boolean HasStart = false;' },
@{ pkg='noppes.npcs'; name='CustomNpcs'; type='class';
body=' public static java.io.File Dir = new java.io.File(".");' },
@{ pkg='noppes.npcs'; name='LogWriter'; type='class';
body=' public static void info(String msg) {}
public static void warn(String msg) {}
public static void error(String msg) {}
public static void error(String msg, Throwable e) {}' }
)
# ============================================================
# STEP 6: Clean up and build
# ============================================================
Dedupe-Imports -Directory 'core/src/main/java'
Build-Report -GradleTask ':core:compileJava'
WORKFLOW 5: Multiple PA Interfaces in One Shot
When a migration wave needs several new PA abstractions created simultaneously.
# File: tools/migration/create_multiple_pa.ps1
. (Join-Path $PSScriptRoot 'Create-And-Propagate.ps1')
. (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')
. (Join-Path $PSScriptRoot 'Build-Report.ps1')
# ============================================================
# Interface 1: IServerService
# ============================================================
Create-And-Propagate `
-FilePath 'platform-api/src/main/java/noppes/npcs/api/handler/IServerService.java' `
-Content @'
package noppes.npcs.api.handler;
public interface IServerService {
boolean isDedicatedServer();
java.io.File getWorldDirectory();
}
'@ `
-TargetDirectory 'core/src/main/java' `
-Swaps @(
@('MinecraftServer', 'IServerService', 'net.minecraft.server.MinecraftServer', 'noppes.npcs.api.handler.IServerService'),
@('DedicatedServer', 'IServerService', 'net.minecraft.server.dedicated.DedicatedServer', 'noppes.npcs.api.handler.IServerService')
)
# ============================================================
# Interface 2: IDamageService
# ============================================================
Create-And-Propagate `
-FilePath 'platform-api/src/main/java/noppes/npcs/api/handler/IDamageService.java' `
-Content @'
package noppes.npcs.api.handler;
public interface IDamageService {
// Platform damage abstraction
}
'@ `
-TargetDirectory 'core/src/main/java' `
-Swaps @(
,@('DamageSource', 'IDamageService', 'net.minecraft.util.DamageSource', 'noppes.npcs.api.handler.IDamageService')
)
# ============================================================
# Clean up and verify
# ============================================================
Dedupe-Imports -Directory 'core/src/main/java'
Build-Report -GradleTask ':core:compileJava'
WORKFLOW 6: Batch PA Interface Creation (pa_batch.py + Propagate)
NEW: Use this when you need to create 5+ PA interfaces in one wave.
This workflow demonstrates the power of pa_batch.py for high-throughput file creation.
Scenario: Create multiple PA interfaces (IServerService, IMessageService, IIOService, etc.) and propagate them everywhere.
# File: tools/migration/_quest_wave_manifest.json
# (Agent composes this JSON with all PA interfaces + wrappers)
cat > tools/migration/_quest_wave_manifest.json << 'EOF'
{
"root": "platform-api/src/main/java",
"files": [
{
"content": "package noppes.npcs.api.handler;\n\npublic interface IServerService {\n boolean isDedicatedServer();\n Object getPlayerList();\n}\n"
},
{
"content": "package noppes.npcs.api.handler;\n\nimport noppes.npcs.api.entity.IPlayer;\n\npublic interface IMessageService {\n void sendMessage(IPlayer player, String message);\n void broadcastMessage(String message);\n}\n"
},
{
"content": "package noppes.npcs.api.handler;\n\nimport noppes.npcs.api.nbt.INbt;\n\npublic interface IIOService {\n INbt readCompressed(java.io.DataInputStream stream);\n void writeCompressed(INbt nbt, java.io.DataOutputStream stream);\n}\n"
},
{
"root": "src/main/java",
"content": "package noppes.npcs.wrappers.handler;\n\nimport net.minecraft.server.MinecraftServer;\nimport noppes.npcs.api.handler.IServerService;\n\npublic class ServerServiceWrapper implements IServerService {\n private MinecraftServer server;\n public ServerServiceWrapper(MinecraftServer server) { this.server = server; }\n @Override\n public boolean isDedicatedServer() { return server instanceof net.minecraft.server.dedicated.DedicatedServer; }\n @Override\n public Object getPlayerList() { return server.getPlayerList(); }\n}\n"
}
]
}
EOF
# Step 1: Dry-run to verify all paths are correct
python tools/migration/pa_batch.py tools/migration/_quest_wave_manifest.json --dry-run
# Output shows exactly where each file will be created
# Step 2: Create all files at once
python tools/migration/pa_batch.py tools/migration/_quest_wave_manifest.json --force
# 3 PA interfaces + 1 wrapper = 4 files created in one command
# Step 3: Propagate (PowerShell script with Symbol-Swap calls)
# File: tools/migration/quest_wave_propagate.ps1
powershell -ExecutionPolicy Bypass -File tools/migration/quest_wave_propagate.ps1
quest_wave_propagate.ps1:
. (Join-Path $PSScriptRoot 'Symbol-Swap.ps1')
. (Join-Path $PSScriptRoot 'Nuke-Imports.ps1')
. (Join-Path $PSScriptRoot 'Dedupe-Imports.ps1')
. (Join-Path $PSScriptRoot 'Build-Report.ps1')
# Nuke leftover MC imports from core
Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes @(
'net.minecraft.server.',
'net.minecraft.util.text'
)
# Propagate the new PA interfaces
Symbol-Swap -Directory 'core/src/main/java' -Swaps @(
@('MinecraftServer', 'IServerService', 'net.minecraft.server.MinecraftServer', 'noppes.npcs.api.handler.IServerService'),
@('ChatComponentText', 'IMessageService', 'net.minecraft.util.ChatComponentText', 'noppes.npcs.api.handler.IMessageService'),
@('CompressedStreamTools', 'IIOService', 'net.minecraft.nbt.CompressedStreamTools', 'noppes.npcs.api.handler.IIOService')
)
Dedupe-Imports -Directory 'core/src/main/java'
Build-Report -GradleTask ':core:compileJava'
Performance comparison:
| Approach | Tool Calls | Wall Clock | Best For |
|---|---|---|---|
Individual write() calls (4 files) |
8-12 | 8-15s | 1-2 files |
Create-And-Propagate × 4 |
4+ | 5-10s | 1 PA interface |
pa_batch.py (NEW) |
2 | ~4s | 5+ files in one batch |
For the quest wave: pa_batch.py creates all 4 files in 2 tool calls. Individual approach would take 8-12 calls. Result: 75-85% fewer round trips.
Common MC Type -> PA Replacement Table
Use these in your -Swaps parameter:
# Standard NBT swaps (use in almost every wave)
@('NBTTagCompound', 'INbt', 'net.minecraft.nbt.NBTTagCompound', 'noppes.npcs.api.nbt.INbt')
@('NBTTagList', 'INbtList', 'net.minecraft.nbt.NBTTagList', 'noppes.npcs.api.nbt.INbtList')
# Entity swaps
@('EntityPlayer', 'IPlayer', 'net.minecraft.entity.player.EntityPlayer', 'noppes.npcs.api.entity.IPlayer')
@('EntityPlayerMP', 'IPlayer', 'net.minecraft.entity.player.EntityPlayerMP', 'noppes.npcs.api.entity.IPlayer')
@('EntityLivingBase','IEntityLivingBase', 'net.minecraft.entity.EntityLivingBase', 'noppes.npcs.api.entity.IEntityLivingBase')
@('EntityNPCInterface','ICustomNpc', 'noppes.npcs.entity.EntityNPCInterface', 'noppes.npcs.api.entity.ICustomNpc')
# Item swaps
@('ItemStack', 'IItemStack', 'net.minecraft.item.ItemStack', 'noppes.npcs.api.item.IItemStack')
Common Static Transforms
# NBT constructors
@('new\s+NBTTagCompound\s*\(\)', 'NBT.compound()')
@('new\s+NBTTagList\s*\(\)', 'NBT.list()')
# Compressed stream tools
@('CompressedStreamTools\.readCompressed\s*\(([^)]+)\)', 'PlatformServiceHolder.get().readCompressedNBT($1)')
@('CompressedStreamTools\.writeCompressed\s*\(([^,]+),\s*([^)]+)\)', 'PlatformServiceHolder.get().writeCompressedNBT($1, $2)')
# MathHelper
@('MathHelper\.clamp_int\s*\(', 'ValueUtil.clampInt(')
@('MathHelper\.clamp_float\s*\(', 'ValueUtil.clampFloat(')
@('MathHelper\.clamp_double\s*\(', 'ValueUtil.clampDouble(')
# Annotations
@('@SideOnly\s*\(\s*Side\.CLIENT\s*\)', '@ClientOnly')
@('@SideOnly\s*\(\s*Side\.SERVER\s*\)', '@ServerOnly')
Common Nuke Prefixes
# The standard set for any wave:
@(
'net.minecraft.',
'cpw.mods.',
'net.minecraftforge.',
'noppes.npcs.entity.',
'noppes.npcs.client.',
'noppes.npcs.mixin.',
'kamkeel.npcs.network.',
'kamkeel.npcs.entity.',
'noppes.npcs.containers.',
'noppes.npcs.scripted.',
'noppes.npcs.janino.',
'noppes.npcs.wrapper.'
)
Decision Tree: Which Tool When
I need to migrate files from mc1710 to core/
--> Copy-Recursive (with AllowPaths whitelist)
I have MC imports in core/ files
--> Nuke-Imports (remove them all)
I need to replace MC type names with PA types
--> Symbol-Swap (bulk replace + import surgery)
I need to replace method calls, not just names
--> Static-Transform (regex with capture groups)
I need to create a NEW PA interface
--> Create-And-Propagate (atomic create + replace everywhere)
Build fails with "cannot find symbol: class X"
--> Generate-Stubs (create placeholder X)
Build fails and I don't know where to start
--> Build-Report (categorize errors, fix per category)
I see duplicate imports after swaps
--> Dedupe-Imports (clean up)
Anti-Patterns (DO NOT DO THESE)
-
DO NOT paste PowerShell inline through bash with
$1/$2in strings. Bash eats them. Write a.ps1file and use-File. -
DO NOT run Copy-Recursive without AllowPaths. It will copy the entire codebase via transitive imports (tested: 1,360 files from one seed file).
-
DO NOT panic on build failures after migration. Build failures are EXPECTED. Run Build-Report. Fix categorically.
-
DO NOT delete files from core/ when the build fails. EVER. Add stubs. Fix imports. Transform code. NEVER delete.
-
DO NOT edit the tool source files. They are stable functions. You provide params.
-
DO NOT run Symbol-Swap before Nuke-Imports. Nuke first to remove the worst offenders, then Swap for the semantic replacements.
-
DO NOT forget Dedupe-Imports at the end. Two MC types mapping to the same PA type (e.g.,
NBTTagCompoundandNBTTagStringboth toINbt) produces duplicate imports.
Testing
All tools have a test suite at tools/migration/test_runner.ps1:
powershell -ExecutionPolicy Bypass -File "tools/migration/test_runner.ps1"
7 tests covering: Symbol-Swap, Nuke-Imports, Static-Transform, Dedupe-Imports, Generate-Stubs, Create-And-Propagate, and a full pipeline integration test.
PA Analyzer — pa_analyzer.py
Maps which MC type members each PA uses across mc1710 source files.
Reads
classification_results.json(non-suppressed PAs) andmc_usage_index.json(member usage) to produce a deterministic JSON mapping:{ "PAName": { mc_types, members, source_files, usage_count, confidence } }.
Usage
# Dry-run (default) — shows summary without writing
python tools/migration/pa_analyzer.py
# Execute — writes output JSON
python tools/migration/pa_analyzer.py --execute
# Filter to specific PAs
python tools/migration/pa_analyzer.py --execute --pa INbt --pa IPlayer
# Filter by classification
python tools/migration/pa_analyzer.py --execute --classification EXISTING INTERFACE
# Verbose (detailed per-PA breakdown)
python tools/migration/pa_analyzer.py --execute --verbose
# Machine-readable JSON summary to stdout
python tools/migration/pa_analyzer.py --execute --json
# Custom output path
python tools/migration/pa_analyzer.py --execute --output /tmp/pa_usage.json
CLI Options
| Option | Default | Description |
|---|---|---|
--execute |
false | Actually write output JSON (default: dry-run preview) |
--output |
tools/migration/pa_usage_analysis.json |
Output path |
--classifications |
tools/migration/classification_results.json |
Input classifications |
--index |
tools/migration/mc_usage_index.json |
Input usage index |
--pa |
all | Filter to specific PA name(s). Repeatable. |
--classification |
all non-suppressed | Filter by classification: EXISTING, INTERFACE, AUTO |
--json |
false | Machine-readable JSON summary to stdout |
--verbose |
false | Show detailed per-PA member breakdown |
--project-root |
auto-detect | Project root override |
Output Format
{
"metadata": {
"analysis_date": "2026-03-15T23:03:13Z",
"tool_version": "1.0.0",
"total_PAs_scanned": 238,
"total_mc_types_covered": 245,
"confidence_thresholds": { "high": ">=90%", "medium": ">=50%", "low": "<50%" }
},
"summary": {
"by_confidence": { "high": ["INbt", ...], "medium": [...], "low": [...] },
"by_classification": { "EXISTING": [...], "INTERFACE": [...], "AUTO": [...] },
"top_by_usage": [{ "pa_name": "INbt", "usage_count": 4452, "file_count": 356 }, ...],
"unused_pas": ["IEnchantment", ...]
},
"pa_usage": {
"INbt": {
"mc_types": [{ "fqn": "net.minecraft.nbt.NBTTagCompound", "shortName": "NBTTagCompound", "classification": "EXISTING", "import_count": 593 }],
"classifications": ["EXISTING"],
"members": {
"methods": { "setInteger": { "count": 569, "file_count": 164, "files": [...] } },
"static_methods": {},
"fields": {}
},
"source_files": ["noppes/npcs/controllers/FactionController.java", ...],
"usage_count": 4452,
"unique_members": 32,
"file_count": 356,
"constructors": { "count": 0, "file_count": 0, "files": [] },
"casts": { "count": 0, "file_count": 0, "files": [] },
"instanceof": { "count": 5, "file_count": 4, "files": [...] },
"confidence": 100.0
}
}
}
Confidence Scoring
Weighted score from usage density, member coverage, and file spread:
- Usage density (40%):
min(total_usage / 100, 1.0)— caps at 100 usages - Member coverage (30%):
min(unique_members / 10, 1.0)— caps at 10 unique members - File spread (30%):
min(file_count / 20, 1.0)— caps at 20 files
| Level | Threshold | Meaning |
|---|---|---|
| High | ≥90% | Core PA — heavily used, well-covered, wide spread |
| Medium | ≥50% | Significant PA — meaningful usage worth abstracting |
| Low | <50% | Niche PA — limited usage, may not need full interface |
| Unused | 0% | No member usage found in index — review classification |
Integration with Migration Workflow
PHASE 0: PA ANALYSIS (run once, reference throughout)
├── pa_analyzer.py --execute → full PA usage map
├── Review unused_pas list → candidates for SUPPRESS
├── Review high-confidence PAs → these MUST have complete interfaces
└── Compare pa_usage members vs surface-miner → find gaps in existing interfaces
PHASE 1: Use pa_usage_analysis.json during migration
├── Check PA entry before creating interface → know exactly which methods to include
├── Cross-reference with surface-miner → pa_analyzer shows WHAT is used,
│ surface-miner shows what's MISSING from the interface
└── Review cast/instanceof counts → high counts = harder migration
Diagnostic Toolchain — analyze.py
Diagnostic tools answer questions. Migration tools change code. ALWAYS diagnose first.
analyze.pyis a unified Python CLI with 8 subcommands that query the mc1710 codebase index, extract signatures via tree-sitter, compare migrations, and compute migration readiness. These tools are READ-ONLY — they never modify files.
Prerequisites
# Generate the index (required before any index-dependent command)
python tools/migration/scan_codebase.py
# Dependencies (tree-sitter needed for extract-signatures, diff-signatures, surface-miner --compare-existing)
pip install tree-sitter tree-sitter-java
The index file (tools/migration/mc_usage_index.json, ~2.6 MB) maps ALL 400 unique MC/Forge types across 1,722 files. It records:
- Every import, method call, static call, field access, constructor, cast, instanceof
- Which files use which types and how
- Class hierarchy (extends/implements MC types)
Regenerate the index whenever you add/remove files from mc1710/. Scan takes ~4 seconds.
Diagnostic Quick Reference Card
| Subcommand | Purpose | Depends On |
|---|---|---|
query-type |
"How is this MC type used across the codebase?" | Index |
query-file |
"How MC-coupled is this specific file?" | Index |
extract-signatures |
"What methods does this Java file declare?" | tree-sitter |
surface-miner |
"What's the minimal PA interface surface for this MC type?" | Index + optional tree-sitter |
type-usage |
"Show ALL members of this type used within a directory, with per-file detail" | Index |
diff-signatures |
"What changed between two versions of this file?" | tree-sitter |
migration-diff |
"Is this migrated file complete? Any unmigrated types, missing methods, body drift?" PASS/WARN/FAIL | tree-sitter + classifications JSON |
coverage-report |
"How far along is the overall migration?" | Index |
All subcommands support --json for machine-readable output. All auto-detect project root.
Subcommand Reference
1. query-type — MC Type Intelligence
What it answers: "How bad is this type? How many files use it? What methods/fields are called on it? Does it have a PA abstraction?"
python tools/migration/analyze.py query-type <TypeName> [options]
| Param | Required | Default | Description |
|---|---|---|---|
type |
Yes | — | Simple class name (e.g. EntityPlayer, NBTTagCompound) |
--fqn |
No | — | Exact FQN override (e.g. net.minecraft.entity.player.EntityPlayer) |
--top N |
No | 0 (all) |
Limit output to top N methods/fields by usage count |
--methods-only |
No | — | Suppress field access output |
--field-accesses-only |
No | — | Suppress method call output |
--show-files |
No | — | List files where each method is called (shows top 3 + count) |
--json |
No | — | Machine-readable JSON output |
--index |
No | tools/migration/mc_usage_index.json |
Custom index path |
--project-root |
No | Auto-detected | Project root override |
Output includes:
- PA abstraction status (mapped or NONE)
- Import count (how many files import this type)
- Method calls: each method name, call count, file count
- Static calls: same format
- Field accesses: each field name, access count, file count
- Hierarchy: which classes extend/implement this MC type
- Usage summary: constructor count, cast count, instanceof count
Real example — EntityPlayer:
TYPE: EntityPlayer (net.minecraft.entity.player.EntityPlayer)
PA Abstraction: IPlayer
Imported by: 551 files
METHOD CALLS (46 unique methods):
getCommandSenderName 100 39
getUniqueID 70 25
addChatMessage 29 21
getHeldItem 21 11
...
FIELD ACCESSES (47 unique fields):
inventory 64 28
worldObj 63 32
capabilities 30 16
...
USAGE SUMMARY:
Constructors: 0
Casts: 180
instanceof: 122
When to use:
- Before creating a PA interface → know what methods to include
- Before migrating a wave → understand how deep the coupling goes
- Evaluating migration difficulty → high cast/instanceof count = hard to abstract
- Checking if a PA mapping already exists →
PA Abstraction: IPlayervsNONE
2. query-file — File Migration Readiness
What it answers: "How MC-coupled is this file? What % of its dependencies already have PA abstractions? What's blocking migration?"
python tools/migration/analyze.py query-file <file-path> [options]
| Param | Required | Default | Description |
|---|---|---|---|
file |
Yes | — | File path (relative to mc1710/src/main/java, or full path). Smart matching — partial paths work. |
--readiness |
No | — | Show migration readiness score (%) and blocking types |
--json |
No | — | Machine-readable JSON output |
--index |
No | tools/migration/mc_usage_index.json |
Custom index path |
--project-root |
No | Auto-detected | Project root override |
Readiness score formula: (MC types with PA abstraction) / (total MC types used) * 100
Real example — QuestController.java:
FILE: noppes/npcs/controllers/QuestController.java
MC Import Count: 3
MC Types: CompressedStreamTools, NBTTagCompound, NBTTagList
MIGRATION READINESS: 66.7%
Abstracted: 2/3
Blocking: CompressedStreamTools
MC TYPES USED:
CompressedStreamTools (NOT ABSTRACTED)
NBTTagCompound -> INbt
NBTTagList -> INbtList
Real example — EntityNPCInterface.java (heaviest file):
FILE: noppes/npcs/entity/EntityNPCInterface.java
MC Import Count: 59
MIGRATION READINESS: 15.3%
Abstracted: 9/59
Blocking: FMLCommonHandler, IEntityAdditionalSpawnData, Side, SideOnly, Block
... and 45 more
When to use:
- Before starting a migration wave → check readiness for each candidate file
- Picking migration targets → files with high readiness scores (60%+) are low-hanging fruit
- Identifying blockers → the blocking list tells you exactly which PA interfaces to create first
- JSON mode for batch queries → script over multiple files to find the easiest migration targets
3. extract-signatures — Method Signature Extraction
What it answers: "What methods does this Java file declare? With exact types, params, annotations, and line numbers."
python tools/migration/analyze.py extract-signatures <file-path> [options]
| Param | Required | Default | Description |
|---|---|---|---|
file |
Yes | — | Path to any Java file (interface, class, etc.) |
--public-only |
No | — | Filter to public methods only (interface methods count as public) |
--json |
No | — | Machine-readable JSON with full signature objects |
--project-root |
No | Auto-detected | Project root override |
Requires: tree-sitter + tree-sitter-java Python packages.
JSON signature object format:
{
"name": "setString",
"return_type": "void",
"params": [{"type": "String", "name": "key"}, {"type": "String", "name": "value"}],
"modifiers": [],
"annotations": [],
"line": 21,
"kind": "method",
"javadoc": null
}
Real example — INbt.java (46 methods):
SIGNATURES: INbt.java
Total methods: 46
L21 void setString(String key, String value)
L23 void setInteger(String key, int value)
L25 void setBoolean(String key, boolean value)
...
L143 INbt copy()
L149 Object getMCNBT()
L155 boolean isEqual(INbt nbt)
L157 void clear()
Real example — IPlayer.java (104 methods):
SIGNATURES: IPlayer.java
Total methods: 104
L20 String getDisplayName()
L25 String getName()
L32 void kick(String reason)
...
L738 boolean isUsingVaultCurrency()
L746 String getFormattedCurrencyBalance()
L753 IEnergyProjectile[] getActiveEnergyProjectiles()
When to use:
- Feed into surface-miner →
--compare-existinguses this internally - Pre-migration inventory → know exactly what a PA interface already provides
- Wrapper generation → extract method list for Generate-Wrapper input
- JSON mode → pipe into scripts for automated interface design
4. surface-miner — Minimal Interface Surface Discovery
What it answers: "Based on actual codebase usage, what's the minimal set of methods needed in the PA interface for this MC type? How does it compare to what we already have?"
This is the most powerful diagnostic tool. It answers the question: "What methods MUST the PA interface have?"
python tools/migration/analyze.py surface-miner <TypeName> [options]
| Param | Required | Default | Description |
|---|---|---|---|
type |
Yes | — | MC type simple name (e.g. NBTTagCompound) |
--fqn |
No | — | Exact FQN override |
--scope |
No | all files | Path prefix filter (e.g. core/src/main/java to only see core usage) |
--compare-existing |
No | — | Path to existing PA interface file — enables gap analysis |
--json |
No | — | Machine-readable JSON output |
--index |
No | tools/migration/mc_usage_index.json |
Custom index path |
--project-root |
No | Auto-detected | Project root override |
The killer feature: --compare-existing
This compares actual codebase usage against an existing PA interface and shows:
[PA]— method exists in PA interface AND is used in codebase (covered)[MISSING]— method is used in codebase but NOT in PA interface (needs adding)- Unused in PA — method exists in PA interface but nobody calls it (future-proofing or dead code)
Real example — NBTTagCompound vs INbt.java:
MINIMAL SURFACE: NBTTagCompound (net.minecraft.nbt.NBTTagCompound)
PA Abstraction: INbt
METHODS (32 unique):
setInteger instance 164 [PA]
getInteger instance 160 [PA]
setTag instance 159 [PA]
hasKey instance 155 [PA]
...
getCompoundTag instance 89 [MISSING]
func_150296_c instance 16 [MISSING]
getTag instance 15 [MISSING]
hasNoTags instance 10 [MISSING]
...
COMPARISON WITH EXISTING PA INTERFACE:
Covered by PA: 25/32 methods
Missing from PA: 7
+ getCompoundTag
+ func_150296_c
+ getTag
+ hasNoTags
+ equals
+ func_150299_b
+ hashCode
Unused in PA: 20
- clear
- getCompound
- getKeySet
- getMCNBT
- isEmpty
- merge
- toJsonString
...
Scope filtering:
# Only show usage within core/ (already-migrated code)
python tools/migration/analyze.py surface-miner EntityPlayer --scope core/src/main/java
# Only show usage within the quest subsystem
python tools/migration/analyze.py surface-miner NBTTagCompound --scope noppes/npcs/quests
When to use:
- Designing a new PA interface → run without
--compare-existingto see what methods are actually needed - Auditing an existing PA interface → run with
--compare-existingto find gaps and dead methods - Scoped analysis →
--scope core/src/main/javashows only what core needs (post-migration surface) - Prioritizing method additions → methods with higher usage counts should be added first
5. type-usage — Scoped Type Member Census
What it answers: "For this MC type, within this directory, show me EVERY method call, field access, constructor, cast, and instanceof — with the exact files where they occur."
This is the deep-dive tool. While query-type shows global usage and surface-miner shows minimal interface surface, type-usage gives you the complete per-file breakdown scoped to a specific directory. It's what you use when you're about to migrate a subsystem and need to know exactly how it touches an MC type.
python tools/migration/analyze.py type-usage <TypeName> --scope <directory> [options]
| Param | Required | Default | Description |
|---|---|---|---|
type |
Yes | — | MC type simple name (e.g. EntityPlayer) |
--fqn |
No | — | Exact FQN override |
--scope |
No | all files | Directory path prefix filter (e.g. noppes/npcs/controllers/) |
--top N |
No | 0 (all) |
Limit methods/fields to top N by count |
--json |
No | — | Machine-readable JSON output |
--index |
No | tools/migration/mc_usage_index.json |
Custom index path |
--project-root |
No | Auto-detected | Project root override |
What it shows (all scoped to directory):
- Method calls — each method, call count, file list with per-file counts
- Static calls — same format
- Field accesses — each field, access count, file list
- Constructors — count + file list with per-file breakdown
- Casts — count + file list with per-file breakdown
- instanceof — count + file list with per-file breakdown
Real example — EntityPlayer in controllers/:
TYPE USAGE: EntityPlayer in noppes/npcs/controllers/
PA Abstraction: IPlayer
Files in scope: 20 (of 551 total)
METHOD CALLS (11 unique, 20 total calls):
getCommandSenderName 5 5
- noppes/npcs/controllers/AuctionController.java
- noppes/npcs/controllers/PartyController.java
- noppes/npcs/controllers/data/Party.java
...
addChatMessage 4 4
- noppes/npcs/controllers/AuctionController.java
- noppes/npcs/controllers/PlayerQuestController.java
...
FIELD ACCESSES (20 unique, 29 total accesses):
posX 2 2
posY 2 2
posZ 2 2
worldObj 2 2
...
CONSTRUCTORS: 0 occurrences
CASTS: 42 occurrences in 12 files
- noppes/npcs/controllers/CustomEffectController.java (11)
- noppes/npcs/controllers/CustomGuiController.java (9)
...
INSTANCEOF: 5 occurrences in 3 files
- noppes/npcs/controllers/PlayerDataController.java (2)
...
Real example — NBTTagCompound in quests/:
TYPE USAGE: NBTTagCompound in noppes/npcs/quests/
PA Abstraction: INbt
Files in scope: 5 (of 593 total)
METHOD CALLS (10 unique, 17 total calls):
setTag 4 4
getTagList 3 3
setString 2 2
...
CONSTRUCTORS: 0 occurrences
CASTS: 0 occurrences
INSTANCEOF: 0 occurrences
JSON output includes all fields:
{
"type": "net.minecraft.entity.player.EntityPlayer",
"shortName": "EntityPlayer",
"paAbstraction": "IPlayer",
"scope": "noppes/npcs/controllers/",
"totalFilesInScope": 20,
"totalFilesGlobal": 551,
"methodCalls": { "getCommandSenderName": { "count": 5, "files": [...] }, ... },
"staticCalls": {},
"fieldAccesses": { "posX": { "count": 2, "files": [...] }, ... },
"constructors": { "count": 0, "files": [] },
"casts": { "count": 42, "files": [{"file": "...", "count": 11}, ...] },
"instanceof": { "count": 5, "files": [{"file": "...", "count": 2}, ...] }
}
When to use:
- Pre-migration deep dive → "Before I migrate controllers/, show me exactly how EntityPlayer is used there"
- Scope-specific PA interface design → "I only need to abstract what quests/ uses from NBTTagCompound"
- Cast/instanceof hotspot detection → high cast counts mean the migration needs wrapper conversion utilities
- Subsystem dependency audit → understand exactly which MC methods a module depends on before cutting the cord
Key difference from other tools:
query-type→ global aggregate, no scope filtering, no per-file detail on casts/instanceofsurface-miner→ interface-design focused (method names only), no casts/instanceof/constructorstype-usage→ complete census with per-file attribution, scoped to any directory
6. diff-signatures — Signature Change Detection
What it answers: "What methods were added, removed, or changed between two versions of a file?"
python tools/migration/analyze.py diff-signatures --old <file1> --new <file2> [options]
| Param | Required | Default | Description |
|---|---|---|---|
--old |
Yes | — | Path to original file |
--new |
Yes | — | Path to updated file |
--json |
No | — | Machine-readable JSON output |
--project-root |
No | Auto-detected | Project root override |
Comparison key: Method name + parameter types (ignores param names, ignoring annotations/modifiers for matching).
Change types detected:
- ADDED — method exists in new but not old (name+params don't match anything)
- REMOVED — method exists in old but not new
- CHANGED — same name+params, but return type or modifiers differ
Real example — FactionController mc1710 vs core:
SIGNATURE DIFF
Old: FactionController.java (20 methods)
New: FactionController.java (20 methods)
ADDED (1):
+ public void loadFactions(INbt nbttagcompound1)
REMOVED (1):
- public void loadFactions(DataInputStream stream)
CHANGED (3):
OLD: public List<IFaction> list()
NEW: public List<Faction> list()
OLD: public NBTTagCompound getNBT()
NEW: public INbt getNBT()
OLD: public IFaction delete(int id)
NEW: public Faction delete(int id)
Summary: +1 added, -1 removed, ~3 changed, =16 unchanged
When to use:
- Post-migration validation → compare mc1710 original with core migrated version to see exactly what changed
- Shadow file audit → verify that mc1710 shadow files add the expected extensions
- API evolution tracking → compare old API interface with new platform-api version
- Regression detection → catch unintended method signature changes after a migration wave
7. coverage-report — Global Migration Progress
What it answers: "How far along is the overall migration? What types are blocking the most files? Which packages need the most work?"
python tools/migration/analyze.py coverage-report [options]
| Param | Required | Default | Description |
|---|---|---|---|
--detailed |
No | — | Show package breakdown and top files by MC dependency |
--json |
No | — | Machine-readable JSON output |
--index |
No | tools/migration/mc_usage_index.json |
Custom index path |
--project-root |
No | Auto-detected | Project root override |
Two coverage metrics:
- Type coverage: % of unique MC types that have PA abstractions (currently 2.8% — 11/400)
- Import coverage: % of all MC imports that reference abstracted types (currently 37.1% — 2,494/6,723)
Import coverage is much higher because the 11 abstracted types (NBTTagCompound, EntityPlayer, etc.) are the most heavily imported. This means the 80/20 rule applies hard — abstracting a few more high-impact types will rapidly increase import coverage.
Real output:
MIGRATION COVERAGE REPORT
OVERALL STATISTICS:
Total files scanned: 1,722
Files with MC deps: 1,453
Total MC imports: 6,723
Unique MC types: 400
ABSTRACTION COVERAGE:
Type coverage: 11/400 (2.8%)
Import coverage: 2494/6723 (37.1%)
TOP BLOCKING TYPES (no PA abstraction):
1. Side 405
2. SideOnly 395
3. GuiButton 218
4. Minecraft 180
5. ResourceLocation 145
...
PACKAGE BREAKDOWN:
net.minecraft.entity.player 3 2 858
net.minecraft.nbt 15 2 812
cpw.mods.fml.relauncher 4 0 802
...
TOP FILES BY MC DEPENDENCY:
noppes/npcs/entity/EntityNPCInterface.java 59
noppes/npcs/scripted/NpcAPI.java 39
...
When to use:
- Progress tracking → run after each migration wave to see coverage improvement
- Strategic planning → top blocking types tell you which PA interfaces to create next for maximum impact
- Package prioritization → package breakdown shows which MC packages have the most unabstracted types
- Risk assessment → top files by MC dependency are the hardest to migrate — plan these for later waves
8. migration-diff — Migration Completeness Verifier
What it answers: "Is my migrated core/ file complete? Did I miss any type replacements? Are all methods present? Did any method bodies change in non-migration ways?"
This is the final validation gate after migrating a file. It compares the original mc1710 file against the migrated core/ file and gives a definitive PASS/WARN/FAIL verdict.
python tools/migration/analyze.py migration-diff --old <path> --new <path> [options]
| Param | Required | Default | Description |
|---|---|---|---|
--old |
Yes | — | Path to original mc1710 Java file |
--new |
Yes | — | Path to migrated core/ Java file |
--classifications |
No | tools/migration/mc_type_classifications.json |
Path to type classifications JSON (relative to project root) |
--json |
No | — | Machine-readable JSON output |
--project-root |
No | Auto-detected | Project root override |
Three detection categories:
-
UNMIGRATED — MC type names from the classifications (EXISTING, INTERFACE, SERVICE categories) that still appear verbatim in the new file. These should have been replaced by Symbol-Swap. Reports line numbers and the expected PA replacement.
-
MISSING_METHOD — Methods present in the old file but absent in the new file after normalizing parameter types. Uses the classifications to normalize
NBTTagCompound→INbt,EntityPlayer→IPlayer, etc. in param types before matching, soloadData(NBTTagCompound nbt)in old correctly matchesloadData(INbt nbt)in new. -
SEMANTIC_BODY_DIFF — Methods present in both files (by normalized signature match) whose bodies differ beyond pure type substitutions. Both bodies are normalized (MC types → PA equivalents, TRANSFORM patterns applied), then compared with
difflib. If similarity < 85% or any diff lines remain, the method is flagged. Shows up to 10 diff lines per method.
Verdict system:
| Verdict | Condition | Meaning |
|---|---|---|
| FAIL | Any MISSING_METHOD | Methods were lost during migration — investigate and restore |
| WARN | Any UNMIGRATED types or SEMANTIC_BODY_DIFF (but no missing methods) | Migration incomplete or has logic changes — manual review needed |
| PASS | None of the above | Migration looks clean — all methods present, types replaced, bodies match |
The 15% gap — what migration-diff CANNOT detect:
Migration-diff catches ~85% of migration issues automatically. The remaining ~15% requires human judgment:
- Algorithmic changes — If a method's logic was intentionally rewritten (e.g., switching from
forloop tostream()), migration-diff flags it as SEMANTIC_BODY_DIFF but can't tell if the new logic is equivalent - Logic reordering — If statements were reordered within a method body, the diff flags it even though it may be semantically equivalent
- Renamed local variables — Normalization only handles MC type names, not arbitrary variable renames
- Semantic equivalence —
player.posX→player.getX()changes the text even after normalization ifposXisn't in the classifications as a field transform - Intentional omissions — Some methods are deliberately not migrated (e.g., MC-specific registration hooks). Migration-diff flags these as MISSING_METHOD
Always manually review WARN and FAIL results. The verdict is a triage signal, not a final judgment.
Real workflow example:
# After migrating FactionController to core/
python tools/migration/analyze.py migration-diff \
--old src/main/java/noppes/npcs/controllers/FactionController.java \
--new core/src/main/java/noppes/npcs/controllers/FactionController.java
# Expected output for a well-migrated file:
# VERDICT: WARN [0 unmigrated, 0 missing, 2 body diffs]
# The body diffs are expected: loadFactions() changed from DataInputStream to INbt,
# and saveFactions() changed from CompressedStreamTools to PlatformServiceHolder.
# Manual review confirms these are correct migration changes → file is good.
# For a file with issues:
# VERDICT: FAIL [1 unmigrated, 2 missing, 0 body diffs]
# ! EntityPlayer still in new file → forgot to run Symbol-Swap
# - loadData(DataInputStream) missing → method used MC I/O, needs rewrite
# - getWorldInstance() missing → removed accidentally during migration
When to use:
- After every file migration → run as the final validation step before marking a file as "migrated"
- After a migration wave → batch-validate all files in the wave
- Before committing → catch accidental method deletions or leftover MC types
- Regression detection → re-run on previously migrated files after Symbol-Swap updates
Diagnostic Playbook — How to Solve Real Migration Problems
The diagnostic tools exist so you NEVER guess. Every migration decision — which files, which types, which methods, which strategy — should be backed by data from these tools.
Read the entire playbook. These aren't isolated patterns; real migrations chain multiple plays together. Understanding all of them lets you pick the right sequence for your specific situation.
PLAY 1: "Which files should I migrate next?"
The problem: You have 1,453 files with MC dependencies. You need to pick the next batch that gives maximum progress with minimum new PA interfaces.
The tool sequence:
# 1. Start with candidate files you're considering
python tools/migration/analyze.py query-file noppes/npcs/controllers/QuestController.java --readiness
python tools/migration/analyze.py query-file noppes/npcs/controllers/DialogController.java --readiness
python tools/migration/analyze.py query-file noppes/npcs/controllers/PlayerQuestController.java --readiness
# Real results:
# QuestController: 66.7% ready — blocked by CompressedStreamTools only
# DialogController: 66.7% ready — blocked by CompressedStreamTools only
# PlayerQuestController: 66.7% ready — blocked by CompressedStreamTools only
How to interpret: All three share the same blocker (CompressedStreamTools). That means fixing ONE type unblocks the ENTIRE batch. This is a high-leverage wave.
Next step — evaluate the blocker:
python tools/migration/analyze.py query-type CompressedStreamTools --top 5
# → 2 methods: readCompressed, writeCompressed
# → Used in 39 files total
# Decision: Only 2 methods → Static-Transform, not a PA interface
The decision rule:
- Readiness ≥ 60% → good candidate, few blockers
- Readiness 30-60% → viable but will need new PA interfaces or stubs
- Readiness < 30% → too many blockers, defer unless you're specifically targeting that subsystem
- Same blocker across multiple files → high leverage, fix the blocker and migrate them all
Contrast with the wrong approach (EntityNPCInterface.java):
python tools/migration/analyze.py query-file noppes/npcs/entity/EntityNPCInterface.java --readiness
# → 15.3% ready, 50 blocking types
# → This file NEVER migrates to core. It extends Entity. It stays in mc1710. Don't waste time.
PLAY 2: "I need to create a PA interface — what methods go in it?"
The problem: You've identified DamageSource as a blocker. You need to design IDamageSource. But should it have 3 methods or 30? How do you know what's enough?
Step 1 — Global surface scan (what does the whole codebase need?):
python tools/migration/analyze.py surface-miner DamageSource
# METHODS (8 unique):
# getEntity instance 7 ← most used, definitely include
# isMagicDamage instance 2
# isFireDamage instance 2
# isExplosion instance 2
# isProjectile instance 2
# getSourceOfDamage instance 2
# isUnblockable instance 2
# getDamageType instance 2
#
# STATIC CALLS (3 unique):
# causePlayerDamage static 6 ← factory method, needs design decision
# causeMobDamage static 5
# causeThrownDamage static 1
#
# FIELDS (1 unique):
# damageType 4 ← direct field access, must become a getter
Step 2 — Scoped scan (what does ONLY the target migration area need?):
python tools/migration/analyze.py type-usage DamageSource --scope noppes/npcs/controllers/
# → Maybe controllers/ only uses getEntity() and getDamageType()
# → You might create a minimal interface for now, expand later
Step 3 — Context check (how hard is this type to abstract?):
python tools/migration/analyze.py query-type DamageSource
# Key things to look for:
# Constructors: 0 → nobody creates new DamageSource instances in our code (good!)
# Casts: 1 → almost no casting (good!)
# instanceof: 0 → no runtime type checks (good!)
# Hierarchy: nobody extends DamageSource in our codebase (good!)
#
# Verdict: Simple wrappable type. 8 instance methods + 3 static factories.
Step 4 — If PA interface already exists, audit it:
python tools/migration/analyze.py surface-miner DamageSource \
--compare-existing platform-api/src/main/java/noppes/npcs/api/IDamageSource.java
# Shows [PA] vs [MISSING] per method
# → Any [MISSING] method with usage > 5 should be added
# → [MISSING] methods with usage 1-2 can wait
Design rules derived from diagnostics:
- Instance methods with usage ≥ 2 → add to interface
- Static methods → become static factory methods on the interface or on a companion factory class
- Field accesses → become getter methods (e.g.,
damageTypefield →getDamageType()) - High cast count → you need an
unwrap()orgetMCEntity()escape hatch method - High instanceof count → you may need subtype interfaces, not just one flat interface
- Classes extend this MC type → you CANNOT wrap it simply. It might need to stay in mc1710 or use a more complex delegation pattern
PLAY 3: "PA interface vs Static-Transform — which strategy?"
The problem: Not every MC type deserves a full PA interface. Some are better handled by regex replacement. How do you decide?
The diagnostic sequence:
python tools/migration/analyze.py query-type <MCType> --top 5
Decision matrix:
| Diagnostic Signal | Strategy | Why |
|---|---|---|
| 1-3 static methods, no instance calls, no fields | Static-Transform | Just rewrite the call sites with regex |
| Few method calls (<5 unique) across <10 files | Static-Transform | Not worth a whole interface |
| Many methods (5+) across many files (10+) | PA Interface | Too many call patterns for regex |
Direct field access (.posX, .inventory) |
PA Interface | Fields become getters; regex can't handle all contexts |
High constructor count (new MCType(...)) |
PA Interface with factory | Need PlatformService.create...() factory |
High cast count ((MCType) expr) |
PA Interface with unwrap() |
Casts become wrapper conversions |
| Type appears in method signatures | PA Interface | Methods need to accept/return the interface type |
Annotation type (@SideOnly, @Override) |
Static-Transform | Just swap annotation text |
Real examples from our codebase:
# CompressedStreamTools → Static-Transform (2 static methods, no state)
python tools/migration/analyze.py query-type CompressedStreamTools
# → 2 methods: readCompressed, writeCompressed. Used as utility calls only.
# → Strategy: Static-Transform to PlatformServiceHolder.get().readCompressedNBT(...)
# MathHelper → Static-Transform (pure static utility)
python tools/migration/analyze.py query-type MathHelper --methods-only --top 5
# → clamp_int, clamp_float, clamp_double, sqrt_double, floor_double
# → Strategy: Static-Transform to ValueUtil.clampInt(...) etc.
# Side/SideOnly → Static-Transform (annotations)
# → 800 imports but literally just annotation text replacement
# → Strategy: @SideOnly(Side.CLIENT) → @ClientOnly
# EntityPlayer → PA Interface (46 methods, 47 fields, 551 files, 180 casts)
python tools/migration/analyze.py query-type EntityPlayer
# → Massively used, has IPlayer already. Cannot be handled by regex.
# ResourceLocation → PA Interface (145 imports, 30 methods, used in constructors + as params)
python tools/migration/analyze.py query-type ResourceLocation --top 5
# → Appears in method signatures, passed to other MC methods. Needs interface.
PLAY 4: "I'm about to migrate a subsystem — give me the full intelligence briefing"
The problem: You're assigned to migrate the quest subsystem. You need to know EVERYTHING about its MC dependencies before writing a single line.
The full intelligence sequence:
# 1. File-by-file readiness scan
python tools/migration/analyze.py query-file noppes/npcs/quests/QuestItem.java --readiness
python tools/migration/analyze.py query-file noppes/npcs/quests/QuestKill.java --readiness
python tools/migration/analyze.py query-file noppes/npcs/quests/QuestDialog.java --readiness
python tools/migration/analyze.py query-file noppes/npcs/quests/QuestLocation.java --readiness
python tools/migration/analyze.py query-file noppes/npcs/quests/QuestManual.java --readiness
python tools/migration/analyze.py query-file noppes/npcs/controllers/QuestController.java --readiness
# 2. For EACH blocking type, get the usage picture scoped to your subsystem
python tools/migration/analyze.py type-usage NBTTagCompound --scope noppes/npcs/quests/
python tools/migration/analyze.py type-usage EntityPlayer --scope noppes/npcs/quests/
python tools/migration/analyze.py type-usage CompressedStreamTools --scope noppes/npcs/quests/
# 3. For types that need PA interfaces, get the minimal surface
python tools/migration/analyze.py surface-miner NBTTagCompound --compare-existing platform-api/src/main/java/noppes/npcs/api/INbt.java
# → Know exactly which INbt methods are needed vs already covered
# 4. Coverage baseline (before migration)
python tools/migration/analyze.py coverage-report
How to read the briefing and write your migration script:
The type-usage output for your scope tells you exactly what goes into your -Swaps parameter:
- Every method call on
NBTTagCompound→ these must exist onINbtafter migration - Every field access → these become getter/setter methods on the PA interface
- Constructor count → tells you how many
new NBTTagCompound()→NBT.compound()transforms - Cast count → tells you if you need wrapper conversion utilities
- instanceof count → tells you if runtime type checks need refactoring
Then you write the wave script with confidence — no guessing about what swaps to include, what stubs are needed, or what static transforms to apply.
PLAY 5: "Build failed after migration — how do I fix it systematically?"
The problem: You ran your migration wave script. Build-Report shows 47 errors. Where do you start?
Step 1 — Categorize errors (you already have Build-Report output):
MISSING_PACKAGE:net.minecraft.nbt → leftover MC import, run Nuke-Imports
MISSING_CLASS:CompressedStreamTools → need a stub or static transform
MISSING_METHOD:getCompoundTag → INbt doesn't have this method yet
TYPE_MISMATCH → incompatible types, wrong swap or missing conversion
Step 2 — Use diagnostics to understand each error category:
For MISSING_METHOD:getCompoundTag:
# Is this method supposed to be on INbt?
python tools/migration/analyze.py surface-miner NBTTagCompound --compare-existing platform-api/src/main/java/noppes/npcs/api/INbt.java
# → getCompoundTag shows as [MISSING] with 89 usages
# → It maps to getCompound() in INbt (different name!)
# → Fix: Add a Static-Transform: getCompoundTag → getCompound
For TYPE_MISMATCH:
# Find which files are trying to use the wrong type
python tools/migration/analyze.py type-usage EntityPlayer --scope core/src/main/java
# → If there are still EntityPlayer references in core/, Symbol-Swap missed some
# → If it's a cast issue, type-usage shows the exact cast count per file
The fix cycle uses both toolchains:
# ROUND 1: Fix what diagnostics revealed
Nuke-Imports -Directory 'core/src/main/java' -ForbiddenPrefixes @('net.minecraft.nbt.')
Static-Transform -Directory 'core/src/main/java' -Transforms @(
@('\.getCompoundTag\(', '.getCompound(')
)
Generate-Stubs -Stubs @(
@{ pkg='noppes.npcs.controllers'; name='SyncController'; type='class'; body='...' }
)
# ROUND 2: Build again
Build-Report -GradleTask ':core:compileJava'
# → Errors should drop from 47 to <10
Key insight: Don't fix errors one at a time. Use query-type and surface-miner to understand the PATTERN, then fix the entire category with one tool invocation.
PLAY 6: "After migration, did I break anything?"
The problem: Migration wave is done, build passes. But did you accidentally drop methods, change signatures, or leave MC types leaking through?
Validation checklist (run ALL of these):
# 1. Signature diff — catch unintended changes
python tools/migration/analyze.py diff-signatures \
--old mc1710/src/main/java/noppes/npcs/controllers/QuestController.java \
--new core/src/main/java/noppes/npcs/controllers/QuestController.java
# What you EXPECT to see:
# CHANGED: methods where NBTTagCompound → INbt, EntityPlayer → IPlayer (type migration)
# ADDED: possibly new overloads with PA types
# REMOVED: old MC-typed overloads replaced by PA-typed ones
#
# What you DON'T want to see:
# REMOVED methods that weren't replaced (accidental deletion)
# CHANGED return types that shouldn't have changed (over-zealous Symbol-Swap)
# Methods with wrong parameter types (mangled by regex)
# 2. Readiness check — core file should be 100% clean
python tools/migration/analyze.py query-file core/src/main/java/noppes/npcs/controllers/QuestController.java --readiness
# → Must be 100%. If not, MC types leaked through.
# 3. Coverage improvement — did the wave actually help?
python tools/migration/analyze.py coverage-report
# → Compare import coverage before vs after
Common post-migration problems and how diagnostics catch them:
| Problem | Diagnostic | Fix |
|---|---|---|
| MC type leaking in core file | query-file --readiness shows < 100% |
Run Symbol-Swap or Nuke-Imports on the missed type |
| Method accidentally deleted | diff-signatures shows unexpected REMOVED |
Restore from mc1710 original, re-apply migration |
| Return type mangled by regex | diff-signatures shows unexpected CHANGED |
Static-Transform was too greedy — refine regex |
| PA interface missing methods | surface-miner --compare-existing shows [MISSING] |
Add methods to PA interface |
PLAY 7: "What should I abstract next for maximum ROI?"
The problem: You have finite time. Which PA interfaces give you the most migration unlock per hour of work?
# 1. Get the top blockers
python tools/migration/analyze.py coverage-report --detailed
# Current top blockers:
# Side (405) / SideOnly (395) — handle with Static-Transform (10 min)
# GuiButton (218) — client-only, SKIP (never goes to core)
# Minecraft (180) — client-only, SKIP
# ResourceLocation (145) — needs PA interface
# MathHelper (98) — handle with Static-Transform (10 min)
# Block (84) — mixed, some uses might be abstractable
# TileEntity (78) — complex, defer
# 2. Eliminate the easy wins first
# Side/SideOnly: 800 imports eliminated by annotation swap (cost: 10 min, value: +12% coverage)
# MathHelper: 98 imports eliminated by static method swap (cost: 10 min, value: +1.5%)
# Total: 20 min work → coverage jumps from 37.1% to ~50.6%
# 3. For real PA interface candidates, check complexity
python tools/migration/analyze.py query-type ResourceLocation
# 145 imports, 30 methods called, 1 field, 3 constructors
# → Medium complexity PA interface. Worth doing.
python tools/migration/analyze.py query-type TileEntity
# 78 imports, 63 methods, 51 fields, 10 subclasses extend it
# → HIGH complexity. Many subclasses. Defer.
python tools/migration/analyze.py query-type AxisAlignedBB
# 59 imports, 68 methods, 28 fields
# → But query-type reveals: mostly used in entity code that stays in mc1710
# → SKIP for now — most users won't migrate to core anyway
ROI scoring formula:
ROI = (import_count × files_unblocked) / (unique_methods + field_count + constructor_complexity + cast_count)
- High import count + low method count = HIGH ROI (e.g., Side/SideOnly, MathHelper)
- High import count + high method count but simple delegation = MEDIUM ROI (e.g., ResourceLocation)
- High import count + subclasses + casts + field access = LOW ROI (e.g., TileEntity, AxisAlignedBB)
PLAY 8: "I need to understand how deeply a type is woven into a subsystem"
The problem: Before migrating controllers/, you need to know if EntityPlayer is used lightly (easy swap) or deeply (casts everywhere, field access, instanceof — hard migration).
python tools/migration/analyze.py type-usage EntityPlayer --scope noppes/npcs/controllers/
# The output tells you EVERYTHING:
# METHOD CALLS (11 unique, 20 total) — these become IPlayer method calls (easy)
# FIELD ACCESSES (20 unique, 29 total) — these need IPlayer getters (medium)
# CONSTRUCTORS: 0 — nobody creates EntityPlayer instances (good!)
# CASTS: 42 in 12 files — DANGER: this means lots of (EntityPlayer) casts
# INSTANCEOF: 5 in 3 files — some runtime type checking to handle
How to interpret each signal:
Casts: 42 occurrences — This is the hardest part. Each (EntityPlayer) someEntity becomes a wrapper conversion. You need:
- A
PlatformServiceHolder.get().asPlayer(entity)utility - Or
if (entity instanceof IPlayer)checks - The per-file breakdown tells you WHERE to focus:
CustomEffectController.javahas 11 casts — start there
Field accesses: 20 unique fields — Each player.inventory, player.posX, player.worldObj must become a method call on IPlayer. If IPlayer already has getInventory(), getX(), getWorld() — great. If not, you need to add them. Use extract-signatures on IPlayer to check:
python tools/migration/analyze.py extract-signatures platform-api/src/main/java/noppes/npcs/api/entity/IPlayer.java --public-only --json
# → Check if getInventory, getX, getWorld exist
instanceof: 5 occurrences — Each if (entity instanceof EntityPlayer) becomes if (entity instanceof IPlayer). Usually straightforward, but verify the cast pattern that typically follows.
PLAY 9: "The full diagnostic-to-migration pipeline"
This is the complete workflow for a real migration wave from start to finish.
PHASE 1: INTELLIGENCE GATHERING (5-10 min)
├── coverage-report --detailed → current baseline, top blockers
├── query-file --readiness × N → pick candidates (≥60% readiness)
├── type-usage <blocker> --scope → understand blockers in target area
├── surface-miner <blocker> → design PA interfaces needed
└── query-type <blocker> → decide PA interface vs Static-Transform
PHASE 2: WAVE SCRIPT WRITING (10-20 min)
├── Copy-Recursive → copy files to core
├── Nuke-Imports → strip MC imports
├── Symbol-Swap → replace types with PA types
├── Static-Transform → rewrite method calls
├── Generate-Stubs → placeholder missing deps
├── Dedupe-Imports → clean up
└── Build-Report → categorize errors
PHASE 3: FIX CYCLE (5-30 min depending on wave size)
├── For MISSING_METHOD errors:
│ └── surface-miner --compare-existing → find method name mappings
│ └── Static-Transform → rename method calls
├── For MISSING_CLASS errors:
│ └── query-type <missing> → understand what it is
│ └── Generate-Stubs OR pa interface → fix appropriately
├── For TYPE_MISMATCH errors:
│ └── type-usage <type> --scope core/ → find the exact locations
│ └── Manual fix or Static-Transform → correct the types
└── Build-Report again → verify error count dropped
PHASE 4: VALIDATION (5 min)
├── diff-signatures --old mc1710/... --new core/... → verify only expected changes
├── query-file core/.../<file> --readiness → must be 100%
└── coverage-report → confirm coverage improved
The key principle: NEVER skip Phase 1. Agents that jump straight to Phase 2 waste hours fixing avoidable errors. 10 minutes of diagnostics saves 2 hours of debugging.
PLAY 10: "After migrating a file — verify it's complete"
The problem: You've migrated QuestController.java from mc1710 to core. The build passes. But did you lose any methods? Are there MC types that slipped through Symbol-Swap? Did any method bodies change in unexpected ways?
The tool: migration-diff — the final validation step in any migration wave.
# Run migration-diff as the LAST step after build passes
python tools/migration/analyze.py migration-diff \
--old src/main/java/noppes/npcs/controllers/QuestController.java \
--new core/src/main/java/noppes/npcs/controllers/QuestController.java
How to interpret results:
PASS → File is clean. All methods present, no leftover MC types, bodies match after normalization. You can confidently mark this file as migrated.
WARN with UNMIGRATED types → Symbol-Swap missed some type replacements. Fix:
Symbol-Swap -Directory 'core/src/main/java' -Swaps @(
@('MissedType', 'IPAType', 'old.import', 'new.import')
)
Then re-run migration-diff to verify.
WARN with SEMANTIC_BODY_DIFF → Method bodies differ after normalization. Check each flagged method:
- If the diff is
new NBTTagCompound()→NBT.compound()style → expected, safe - If the diff shows logic changes (new conditions, different algorithm) → manual review required
- If the diff is just whitespace/formatting → safe, ignore
FAIL with MISSING_METHOD → Methods from the original file are gone. Check:
- Was the method intentionally excluded (MC-specific registration, client hooks)?
- Was it accidentally deleted during migration?
- Was the signature changed in a way that normalization didn't catch (e.g., different param count)?
Batch validation after a wave:
# Validate all files in a migration wave
for old_file in QuestController DialogController PlayerQuestController; do
echo "=== $old_file ==="
python tools/migration/analyze.py migration-diff \
--old "src/main/java/noppes/npcs/controllers/${old_file}.java" \
--new "core/src/main/java/noppes/npcs/controllers/${old_file}.java"
done
The complete Phase 4 with migration-diff:
PHASE 4: VALIDATION (5 min)
├── migration-diff --old mc1710/... --new core/... → comprehensive PASS/WARN/FAIL
├── diff-signatures --old mc1710/... --new core/... → detailed signature changes
├── query-file core/.../<file> --readiness → must be 100%
└── coverage-report → confirm coverage improved
migration-diff is the broadest check (types + methods + bodies). diff-signatures gives more detail on signature changes. query-file --readiness catches any remaining MC imports. Use all three for maximum confidence.
Diagnostic Output Formats
All subcommands support --json for piping into scripts or feeding into other tools.
JSON outputs are ideal for:
- Batch analysis scripts (
for file in ...; do python analyze.py query-file "$file" --readiness --json; done) - Feeding
surface-miner --jsonresults into Generate-Interface method list - Automated migration wave planning scripts
- CI/CD coverage tracking
Human-readable output is ideal for:
- Quick checks during migration work
- Copy-pasting into agent conversations for context
- Decision-making (the tabular format makes comparisons easy)
Current Codebase Statistics (from latest scan)
These numbers represent the full mc1710 codebase as of the last scan_codebase.py run:
| Metric | Value |
|---|---|
| Total Java files | 1,722 |
| Files with MC dependencies | 1,453 (84%) |
| Total MC imports | 6,723 |
| Unique MC types | 400 |
| PA abstraction coverage (types) | 11/400 (2.8%) |
| PA abstraction coverage (imports) | 2,494/6,723 (37.1%) |
| Classes extending MC types | 291 |
| Classes implementing MC interfaces | 61 |
| Most-imported type | NBTTagCompound (593 imports) |
| Most method-heavy type | NBTTagCompound (4,447 method calls) |
| Most field-heavy type | EntityLivingBase (1,113 field accesses) |
| Highest MC dependency file | EntityNPCInterface.java (59 imports) |
Currently abstracted types (11):
NBTTagCompound→INbt, NBTTagList→INbtList, Entity→IEntity, EntityLivingBase→IEntityLivingBase, EntityLiving→IEntityLiving, EntityPlayer→IPlayer, EntityPlayerMP→IPlayer, ItemStack→IItemStack, World→IWorld, WorldServer→IWorld, DamageSource→IDamageSource
Limitations & Known Gaps
-
No batch readiness query — Can't ask "show me the 20 easiest files to migrate" in one command. Workaround: script with
--json:# Find most-migratable files (use jq or python to sort by readiness) for f in $(python -c "import json; idx=json.load(open('tools/migration/mc_usage_index.json')); [print(f) for f in sorted(idx['files'])]"); do python tools/migration/analyze.py query-file "$f" --readiness --json 2>/dev/null done | python -c "import sys,json; ..." -
No cross-reference query — Can't ask "which files use BOTH EntityPlayer AND NBTTagCompound?" Workaround: use the
--jsonoutput and intersect file lists manually. -
Scoped surface-miner returns empty for fully-migrated modules — If core/ already replaced
EntityPlayerwithIPlayer, then--scope core/finds 0 EntityPlayer calls. This is correct behavior — scope tomc1710/or omit--scopefor full picture. -
Obfuscated method names — The scanner captures MCP names as-is (
func_150296_c). Cross-reference with MCP mappings to find the real name. -
No inheritance-aware method resolution — If
EntityPlayerMPinheritsgetHealth()fromEntityLivingBase, the scanner attributes the call to whichever variable type was used at the call site. Both types' method lists are independent.