Files

106 KiB
Raw Permalink Blame History

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

  1. Tools are METHODS. You FEED them PARAMS. You NEVER edit their source. Unless they are broken or the core logic needs modification.
  2. Every tool follows one principle: NEVER DELETE. ONLY ADD AND TRANSFORM.
  3. Build failures after migration are EXPECTED. Do NOT panic. Do NOT delete files. Run Build-Report and fix categorically.
  4. Always load tools from .ps1 files 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 IFoo from 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 root applies to all files unless overridden
  • Each file entry can specify its own root (for mixed batches: PA interfaces + core + mc1710)

Explicit path override:

  • If path is 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 --force to overwrite existing files
  • Directories auto-created (mkdir -p)

Validation:

  • Warns if explicit path doesn't match the package declaration (detects typos)
  • Errors if content field missing
  • Errors if no package declaration found (and no explicit path)
  • Errors if no public class/interface/enum/@interface found (and no explicit path)

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:

  1. PHASE 1: Validate all edit specs structurally (no I/O)
  2. PHASE 2: Read all files, resolve all targets, compute all results in memory
  3. 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 -Sources files are ALWAYS copied regardless of whitelist.
  • Initial -Sources files 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):

  1. Remove ALL old imports first
  2. Replace symbols in code body
  3. 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 interface
  • MISSING_PACKAGE:pkg.name -- leftover MC import, run Nuke-Imports or Symbol-Swap
  • MISSING_METHOD:methodName -- method signature changed or missing
  • MISSING_VARIABLE:varName -- field not migrated
  • MISSING_SYMBOL -- general "cannot find symbol" (multi-line Gradle output)
  • TYPE_MISMATCH -- incompatible types, usually needs a cast or wrapper
  • BAD_ARGS -- wrong argument types to a method
  • ABSTRACT_VIOLATION -- class doesn't implement required abstract methods
  • UNCHECKED_EXCEPTION -- missing throws/try-catch
  • OTHER -- 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, $2 etc. 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:

  1. Files copied from mc1710 to core with all allowed transitive deps
  2. MC imports stripped
  3. Types replaced with PA equivalents
  4. Constructor/static method patterns rewritten
  5. Missing deps stubbed
  6. Import duplicates cleaned
  7. 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:

  1. IMessageService.java is created in platform-api/
  2. Every ChatComponentText and ChatComponentTranslation reference in core/ is replaced with IMessageService
  3. Old MC imports removed, new PA import added -- in every file that had the reference
  4. 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:

  1. First build report shows categorized errors
  2. MISSING_PACKAGE errors fixed by nuking leftover MC imports
  3. MISSING_CLASS errors fixed by generating stubs
  4. Second build report shows remaining errors (should be much fewer)
  5. 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)

  1. DO NOT paste PowerShell inline through bash with $1/$2 in strings. Bash eats them. Write a .ps1 file and use -File.

  2. DO NOT run Copy-Recursive without AllowPaths. It will copy the entire codebase via transitive imports (tested: 1,360 files from one seed file).

  3. DO NOT panic on build failures after migration. Build failures are EXPECTED. Run Build-Report. Fix categorically.

  4. DO NOT delete files from core/ when the build fails. EVER. Add stubs. Fix imports. Transform code. NEVER delete.

  5. DO NOT edit the tool source files. They are stable functions. You provide params.

  6. DO NOT run Symbol-Swap before Nuke-Imports. Nuke first to remove the worst offenders, then Swap for the semantic replacements.

  7. DO NOT forget Dedupe-Imports at the end. Two MC types mapping to the same PA type (e.g., NBTTagCompound and NBTTagString both to INbt) 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) and mc_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.py is 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: IPlayer vs NONE

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-existing uses 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-existing to see what methods are actually needed
  • Auditing an existing PA interface → run with --compare-existing to find gaps and dead methods
  • Scoped analysis → --scope core/src/main/java shows 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):

  1. Method calls — each method, call count, file list with per-file counts
  2. Static calls — same format
  3. Field accesses — each field, access count, file list
  4. Constructors — count + file list with per-file breakdown
  5. Casts — count + file list with per-file breakdown
  6. 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/instanceof
  • surface-miner → interface-design focused (method names only), no casts/instanceof/constructors
  • type-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:

  1. 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.

  2. 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, so loadData(NBTTagCompound nbt) in old correctly matches loadData(INbt nbt) in new.

  3. 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 for loop to stream()), 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 if posX isn'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., damageType field → getDamageType())
  • High cast count → you need an unwrap() or getMCEntity() 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 on INbt after 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.java has 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 --json results 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

  1. 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; ..."
    
  2. No cross-reference query — Can't ask "which files use BOTH EntityPlayer AND NBTTagCompound?" Workaround: use the --json output and intersect file lists manually.

  3. Scoped surface-miner returns empty for fully-migrated modules — If core/ already replaced EntityPlayer with IPlayer, then --scope core/ finds 0 EntityPlayer calls. This is correct behavior — scope to mc1710/ or omit --scope for full picture.

  4. Obfuscated method names — The scanner captures MCP names as-is (func_150296_c). Cross-reference with MCP mappings to find the real name.

  5. No inheritance-aware method resolution — If EntityPlayerMP inherits getHealth() from EntityLivingBase, the scanner attributes the call to whichever variable type was used at the call site. Both types' method lists are independent.