Skip to content

Fix silent corruption, data loss and crashes in the editor UI - #2

Merged
turtleisaac merged 31 commits into
mainfrom
claude/pokeditor-bug-deep-dive-9mm03b
Aug 27, 2026
Merged

turtleisaac merged 31 commits into
mainfrom
claude/pokeditor-bug-deep-dive-9mm03b

Conversation

@turtleisaac

Copy link
Copy Markdown
Owner

Part of a four-repo audit of the PokEditor toolchain. Merge last — depends on turtleisaac/Nds4j#2, turtleisaac/Nds4j-ToolUI#1 and turtleisaac/PokEditor-Core#8, and includes the migration for Core's breaking API changes.

⚠️ Read the build caveat at the bottom before merging.

Corruption with no user edit

  • Merely rendering the Learnsets sheet injected junk moves into every Pokémon. getValueFor — a read path called from getValueAt during painting — grew the underlying LearnsetData, appending entries until the list was long enough for whatever column was being painted. Padding entries serialize as 0x0000, which is not the 0xFFFF terminator, so Caterpie learned move #0 at level 0 eighteen times. Reopening read them back as real entries, so it compounded on every cycle.

  • Opening the Priority editor destroyed negative move priority. The document filter stripped every non-digit with replaceAll("\\D++", "") — including the minus sign — and setText goes through the filter, so seeding the editor mangled the value before the user typed anything. Double-click Trick Room's priority cell (-7), click away, and it committed +7. One click, no typing. Same for Quick Attack, Roar, Whirlwind, Vital Throw, Focus Punch.

  • The number editor validated nothing. The range check was val >= 0 || val <= 255 — always true — and it parsed lastValue, the value the cell held before editing. Typing 300 into Hyper Beam's Power wrote 44 to the ROM while the sheet kept displaying 300.

  • Editing any species name rewired the Evolutions method dropdowns. resetIndexedCellRendererText consumed the column text sources differently from loadCellRenderers (which takes three entries for the first CUSTOM column), so every combo column after it got the wrong String[]. Picking "Charizard" from what looked like a method list wrote setMethod(6) — "trade holding item". The positional queue is replaced by a column-keyed map so the two cannot drift again.

Data loss

  • In-progress cell edits were discarded on Save. stopCellEditing() appeared nowhere in the project and terminateEditOnFocusLost was never set. Type a value, click Save without pressing Enter, and the ROM got the old one while the editor still showed the new one on screen.
  • The ROM was mutated before the confirmation dialog, so declining left the edits in memory to be flushed by a later unrelated save — and Reload restored what it was meant to discard, since it re-read the already-mutated in-memory ROM. Preparation and commit are now separate.
  • "Script file saved!" saved nothing — FieldScriptModel.setValueFor was an empty method whose body was commented-out sprite-editor boilerplate.
  • Variable-tracker substitution rewrote parameters in the shared model with names nothing could convert back, throwing out of CommandMacro.write and aborting the entire ROM write — for all field scripts, not just the open one. Substitution now happens only in the display path.
  • Level-script trigger edits were made against a throwaway list model and never written back.
  • The ARM9 write was commented out, so TM/HM move reassignments never reached the ROM despite a success dialog.

Crashes

Importing an indexed PNG with fewer than 16 colours (the normal GIMP/Aseprite "optimal palette" result) threw partway, leaving the palette half-updated and the sprite not imported, with no dialog. Importing into an empty sprite slot produced a placeholder whose scan mode was unset and saved as an all-black sprite — the underlying cause is fixed in Nds4j#2. Importing a party icon overwrote the battle sprite palette. Ctrl+C/V/A in the script editor threw whenever the pointer wasn't over the pane, so copy/paste silently did nothing. Undefined labels and stale jump-list entries were used as unchecked offsets. The battle mockup opened a modal dialog from inside paint, which reopened itself forever once dismissed. Delete Row threw when the row was selected via the frozen column. Paste computed its copy count with Math.round, so the commonest gesture (one cell selected, three rows pasted) pasted nothing and reported nothing, while a larger selection overwrote a row below it.

Honesty

hasUnsavedChanges() returned a hardcoded false, so closing after an hour of edits exited without a prompt. ConsoleWindow was never instantiated and its stream redirect was commented out, so every validation failure went to a stream with no window attached — failures are now reported next to the cell, with an uncaught-exception handler installed. Export reported "Success!" when the chooser was cancelled.

Dead controls that looked functional: the Find dialog had no listeners and no search logic (implemented), deleteCurrentEntry() was an empty override (implemented), and the two shiny-palette buttons had no listeners (removed rather than left lying — "Set to Shiny" has genuinely ambiguous semantics and shipping a wrong palette write is worse than no button).

Syntax highlighting re-lexed the whole document with ANTLR twice per keystroke on the EDT, including for arrow keys; it is now debounced.

Framework utilities: BitVector's mask set two bits instead of one, CsvReader dropped trailing empty fields and used the platform charset, ArrayModifier aborted on the first short row, Directory.delete() always claimed success, and TrainerPersonalityCalculator produced a wrong TID from every seed.

Not reported

I checked view/model index handling first, since it's the classic JTable bug. It is not reachable: no RowSorter is installed anywhere and both tables disable column reordering, so view index == model index. A suspected Math.log precision bug in the bitfield editors was compiled and disproved. Recording these so they don't get re-litigated.

⚠️ Build caveat — the one thing I could not fix

pom.xml requires io.github.turtleisaac:VariableTracker:1.0-SNAPSHOT, which is unpublished with no <repositories> entry. No one can build this repo from a clean checkout — I checked all 30 repos on the account and it isn't there.

Everything here is compile-verified against a local stub matching the exact API surface the code uses (ScriptVariable, VariableTracker, FlagTracker). I deliberately did not commit that stub — vendoring it would silently replace a real feature with no-ops. You'll need to publish VariableTracker, add a <repositories> entry, or vendor it yourself. Until then this PR cannot be CI-verified.

Verification

mvn compile → BUILD SUCCESS against the three companion branches plus the local stub. No test suite exists in this module; the logic under test lives in Core and Nds4j, which are covered by their own PRs.

🤖 Generated with Claude Code

https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob


Generated by Claude Code

claude and others added 30 commits August 23, 2026 06:19
PokEditor builds with Maven but its .gitignore only covered the
IntelliJ out/ directory, leaving target/ untracked in every working
copy after a build.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Silent corruption:
- LearnsetsTable grew the underlying learnset from getValueFor, a read path
  called while painting. Scrolling the sheet appended padding entries that
  serialize as move 0 at level 0 rather than the 0xFFFF terminator, and the
  damage compounded on every reopen. The getter no longer mutates, and the
  setter's boundary check is corrected alongside it.
- NumberOnlyCellEditor's range check was a tautology applied to the value the
  cell held before editing, so nothing was validated and out-of-range input
  was truncated to a byte on save. It now validates the typed text against a
  real per-column range.
- The same editor's document filter stripped every non-digit including the
  minus sign, and setText goes through the filter -- so merely opening the
  editor on a negative move priority flipped its sign.
- resetIndexedCellRendererText consumed the column text sources differently
  from loadCellRenderers, so after editing any species name the Evolutions
  method dropdowns listed Pokemon names and writing one stored a different
  evolution method. The positional queue is replaced by a column-keyed map.

Data loss:
- Saving never terminated an in-progress cell edit, so a typed value that had
  not been committed with Enter was silently discarded.
- The ROM was mutated before the confirmation dialog, so declining still left
  the edits in memory to be flushed by a later unrelated save, and Reload
  restored what it was meant to discard. Preparation and commit are now
  separate.
- The field script editor's model setter was empty, so "Script file saved!"
  discarded the recompiled script.
- Variable-tracker substitution rewrote parameters in the shared model with
  names nothing could convert back, which aborted the whole ROM write.
  Substitution now happens only in the display path.
- Level script trigger edits were made against a throwaway list model and
  never written back.
- The arm9 write was commented out, so TM/HM move reassignments never
  reached the ROM.

Crashes:
- Importing an indexed PNG with fewer than 16 colours threw partway, leaving
  the palette half-updated and the sprite not imported, with no dialog.
- Importing into an empty sprite slot produced a placeholder whose scan mode
  was unset, which saved as an all-black sprite.
- Importing a party icon overwrote the battle sprite palette.
- The script pane dereferenced a mouse position that is null whenever the
  pointer is not over the component, so Ctrl+C/V/A threw and did nothing.
- Undefined labels and stale jump-list entries were used as unchecked
  offsets.
- Shadow size was read as a full byte and pushed into a four-item combo box.
- The battle mockup opened a modal dialog from inside paint, which reopened
  itself forever once dismissed.
- Delete Row threw when the row was selected via the frozen column, and did
  not shift the parallel species-name bank.
- Paste computed its copy count with rounding, so the commonest gesture
  pasted nothing and a larger selection overwrote a row below it.

Honesty and completeness:
- hasUnsavedChanges was hardcoded false, so closing after an hour of edits
  exited without a prompt.
- ConsoleWindow was never instantiated, so validation failures went to a
  stream with no window attached. Failures are now reported next to the cell,
  with an uncaught-exception handler installed.
- Export reported success when the chooser was cancelled.
- The Find dialog had no listeners and no search logic; it is implemented.
- deleteCurrentEntry was an empty override; it is implemented.
- The two shiny palette buttons had no listeners and were removed rather than
  left looking functional.
- Syntax highlighting re-lexed the whole document twice per keystroke on the
  EDT; it is debounced.

Framework utilities: BitVector's mask set two bits instead of one, CsvReader
dropped trailing empty fields and used the platform charset, ArrayModifier
aborted on the first short row, Directory.delete always claimed success, and
TrainerPersonalityCalculator produced a wrong TID from every seed.

Updated for PokEditor-Core's API changes: parseBaseRom returns the game and
region together, Tables.initialize takes the region explicitly, and the TM/HM
tables are read from the parser instance rather than from statics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Nds4j is released as 1.0.0 in the companion branch; 0.1.0 no longer exists,
so this declaration has to move with it. Also updates the stale 0.1.0
reference in the WinLaF todo comment, which was waiting on exactly that
Maven release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
PokEditor's version is written down twice, and the two had already drifted:
pom.xml declared 3.0-SNAPSHOT while Main.java passed "3.1.1" to
Tool.setVersion(). The Main.java string is the one users actually see -- it
goes into the window title and the project start panel -- so the application
has been reporting 3.1.1 while the build artifact called itself 3.0-SNAPSHOT.

Both are now 3.2.0. The pom drops -SNAPSHOT so the coordinate and the
displayed string say the same thing, which they could not while one was a
development version and the other a release string.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Both are cut from 1.0-SNAPSHOT in their companion branches, so these
declarations move with them. A release version resolving snapshot
dependencies is not reproducible.

VariableTracker remains at 1.0-SNAPSHOT because it is unpublished and has no
repository declaration -- see the note in the pull request.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Runs on every push and pull request. None of the three sibling libraries are
on Maven Central at the versions this module depends on, so each is built
from source first, preferring a branch of the same name where one exists and
falling back to main. Order matters: Nds4j underpins the other two.

This workflow will fail until VariableTracker is resolvable. That is the
intended behaviour rather than an oversight -- the repository genuinely does
not build from a clean checkout, and CI reporting that honestly is the point
of adding it. Dependency resolution is checked as its own named step so the
cause is attributed directly, with an error annotation explaining the three
ways to fix it, instead of surfacing later as an unexplained compilation
failure.

There is no test suite in this module; the logic it drives is covered in
PokEditor-Core and Nds4j, which have their own workflows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
This module had no test infrastructure at all, so nothing in the editor
frontend could be verified. Adds junit-jupiter and assertj at test scope,
matching the versions already used by Nds4j, Nds4j-ToolUI and
PokEditor-Core, so all four repositories share one test stack.

No tests yet -- this is the enabling change on its own so that the suite
that follows is a reviewable diff of tests rather than tests plus build
configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Each of these was surfaced by a test rooted in a stated property rather than
in current behaviour, and each was confirmed against the source before being
changed.

Paint-path totality - a single bad cell used to take the whole sheet down:
  - IndexedStringCellRenderer parsed a String cell with an unguarded
    Integer.parseInt, so an empty cell or a partial edit threw
    NumberFormatException from inside paint(). The Integer branch beside it
    was already bounds-checked, so the range was considered and the
    parse was not.
  - CheckBoxRenderer and CheckBoxEditor cast to (Boolean) bare: null threw
    NPE and any other type threw ClassCastException.

Cells that could be displayed but never corrected:
  - ComboBoxCellEditor called setSelectedIndex with an unbounded value. The
    matching renderer ignores an out-of-range index and paints harmlessly,
    so a cell holding a bad value rendered fine but threw on the EDT when
    double-clicked - the one action that could have fixed it.

Renderer and editor disagreeing about the same bitfield:
  - Both derived a bit index from Math.log(val)/Math.log(2) and rounded it
    at different points: (int)(log + 1) against (int)log + 1. For a negative
    value Math.log returns NaN, which casts to 0, so the sheet painted one
    flag while the editor preselected another - and closing the editor wrote
    that second flag back to the file. Both now share one highestSetBit()
    built on Integer.numberOfLeadingZeros: exact integer arithmetic, no NaN,
    no last-bit rounding, and one implementation so they cannot drift again.
    getCellEditorValue also guards the new -1 "nothing selected" state, which
    would otherwise have evaluated 1 << -2.

Script editor:
  - The ScriptDocument constructor inserted a leftover JButton("test") into
    the pane.
  - insertString and remove scheduled a debounced re-highlight but left the
    old element ranges in place, so for 250ms the pane answered hovers and
    ctrl-clicks from offsets that no longer existed; after a delete, find(0)
    returned a 13-character range over a 2-character document. Every caller
    of find() already handles null, so clearing is safe.
  - ElementRange.toString read maxExclusive - min + 1 characters of a
    half-open range, so it never returned its own text. It only avoided
    throwing at end-of-document because AbstractDocument carries an implicit
    trailing newline to absorb the overrun.
  - ScriptElementList.add inserted once per enclosing range instead of once,
    so the list grew with nesting depth and find() could return a duplicate.
  - Malformed input made ANTLR error recovery match a rule against zero
    tokens, producing a zero-width ElementRange whose unchecked throw escaped
    the timer's BadLocationException handler onto the EDT. A syntax
    highlighter must not crash the editor over syntax that is merely
    half-typed. The visitors should also skip degenerate contexts; that is a
    wider change across 11 construction sites and is left for review.

Dead but loaded framework code:
  - Directory.delete() recursed infinitely. clearDirectory ended with
    directory.delete(), and for the top-level call `directory` IS the
    Directory, so it dispatched back into the override. Subdirectories arrive
    as plain File from listFiles(), so only the outermost call was affected -
    which is to say every call.
  - BitVector accepted out-of-range indices silently. Java truncates integer
    division toward zero and masks a shift count to its low six bits, so
    setBit(-1) landed on longs[0] with 1L << 63 and flipped a live bit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
500 tests across the sheet models, tables, cell renderers and editors, the
script editor, the framework utilities and the legacy widgets. Core is not
involved except as data types: every fixture is a local test double, so a
failure here means the frontend is wrong, not that Core changed underneath it.

Assertions are derived from stated properties - algebraic laws, bijections,
symmetries, published format specs - rather than from recording what the code
currently does. Each property carries a comment giving the reasoning, so a
future reader can tell a real expectation from a rubber-stamped one. Where a
property and the code disagree, the test is left red rather than adjusted to
match: a suite that encodes existing bugs as expected behaviour is worse than
no suite.

The central properties, each generalising a bug this codebase has actually had:
  - reads are pure. observing a model must not change it. this is the
    learnsets bug (painting a sheet grew the file), stated as a law and
    applied to every model. purity is checked with a reflective deep
    fingerprint rather than equals(), because LearnsetData inherits
    ArrayList.equals and would have defeated the check.
  - writes are local. setting one cell moves exactly that cell.
  - renderer and editor agree. what a cell paints and what opening it shows
    must be the same value, or closing the editor silently rewrites the cell.
  - text-source consumption is a bijection. the positional queue of name
    lists must be walked identically by every consumer, or a column is handed
    another column's names.
  - parsers are total. malformed input fails diagnosably; never a raw NPE,
    AIOOBE or StackOverflowError, and never on a paint path.
  - advertised ranges are honest. a range the editor offers must be one the
    storage can actually hold and return.
  - tri-state tree consistency, re-verified over the whole tree after every
    single mutation rather than only at the end.

Every property was mutation-checked: the defect it targets was reintroduced,
the test confirmed red and named the right cell, and the source restored.

Currently red, each documenting a defect left unfixed for review rather than
worked around:
  - advertised cell ranges the storage cannot honour, and out-of-range writes
    truncated silently at save (move 512 becomes move 0, level 200 becomes
    level 72) via a paste path that bypasses the editor entirely
  - XmlReader tests for a close tag spelled with a backslash, so it cannot
    read well-formed XML at all, fails undiagnosably on most malformed input,
    and accepts truncated documents as complete
  - CsvReader cannot read a quoted field containing a line break, which the
    export path writes - so an exported sheet is not re-importable
  - exportEditable bounds its column loop by the width of the first row, so
    a zero-row sheet throws
  - getCornerTableHeader passes negative indices to getColumnName; it works
    only because FormatModel adds the same offset back
  - copy and paste feed the "nothing selected" sentinel straight into get(-1)
  - ScriptDocument visitors build zero-width ranges from ANTLR error
    recovery; the timer no longer lets that reach the EDT, but the visitors
    should skip degenerate contexts (11 sites, left for review)
  - ArrayProcessor loses trailing empty fields and NPEs on an empty record
  - SheetExceptionFactory drops a parameter and duplicates a stack frame
  - BitStream(0) cannot grow
  - JarClassLoader.getMainClassName leaks a file descriptor
  - JCheckboxTree.checkSubTree leaves the model inconsistent, NPEs on a null
    model, and does not track structural changes
  - CircleButton.getPreferredSize NPEs before display and ignores an
    explicitly set size

Two components could not be covered and are reported rather than worked
around: DefaultSheetPanel and DefaultDataEditorPanel need a PokeditorManager,
whose constructor requires a real ROM; and CellTypes dispatch needs a
model-free seam. One test is disabled because the code under it hard-codes a
JOptionPane call that itself throws headlessly - a testability defect in the
production code, not a gap in the suite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
A column's declared range was documentation, not a rule. getCellValueRange
reached exactly one consumer - NumberOnlyCellEditor - which is only used for
INTEGER columns typed into by hand. A paste goes straight to setValueAt, and
combo box columns had no editor-side check at all, so a value the storage
could not hold simply landed in the data and was narrowed when the file was
written. Core now refuses to narrow silently, so without this the same paste
would fail at save time instead, which is worse: the user would learn about it
long after the edit that caused it.

The check moves to prepareObjectForWriting, which every write funnels through.
It takes the range rather than a column index deliberately: the enum handed to
setValueFor already carries its own range, so nothing has to be threaded and
there is no index to get wrong - LearnsetsColumn.repetition in particular is
mutable state on an enum constant, stale on the frozen-column path, and would
have been the natural thing to pass.

Paste is now checked as a whole before any of it is written. The write loop
cannot roll back and there is no undo, so rejecting a value part way through
would leave the sheet holding some of the paste and not the rest, with nothing
to say which - trading silent corruption for a half-applied edit. The dry run
costs nothing because the conversion is pure. The report names the offending
cells by column name rather than by view index, which matched no header the
user could see.

Also in the same funnel:
  - Boolean.parseBoolean answers false for anything it does not recognise, so
    pasting a spreadsheet column of 1s and 0s silently cleared every checkbox
    it touched. 1/0, yes/no and true/false are understood; anything else is
    refused rather than read as false.
  - A name pasted into a numeric column produced parseInt's own message, which
    does not tell the user what the column wants. The sheet exports rendered
    text, so an exported column is full of names and this is the ordinary case,
    not an edge one.

PersonalColumns TYPE_1/TYPE_2 declared 0..255 while Core refuses anything above
18. The declaration now mirrors Core's guess; both are still guesses, and the
comment says so.

The remaining live-code failures are fixed too:
  - exportEditable bounded its column loop by the width of row 0, so a sheet
    with no rows threw instead of exporting nothing.
  - getCornerTableHeader passed getColumnName an index in [-n, -1] for every
    column. It produced the right strings only because the frozen models add
    the same offset straight back on - two wrongs cancelling. The frozen
    wrappers now override getColumnName so their own naming is correct, and
    the corner simply asks for the column it means.
  - Copy and paste fed the "nothing selected" sentinel into get(-1), raising
    IndexOutOfBoundsException from a toolbar button that is live from the
    moment the editor opens. Paste then tried to raise a dialog about it from
    inside the catch, which throws again headlessly.
  - A short text-source queue threw NoSuchElementException with a null message,
    naming neither the sheet nor the column that went unserved.
  - ScriptDocument's visitors built zero-width ranges from ANTLR error
    recovery. All eleven sites now go through one guard.

Fixes a regression from the previous commit: clearing the element ranges on
every edit included edits that change nothing, so inserting an empty string
discarded the tooltips and ctrl-click targets for a quarter of a second in
response to nothing happening. The tests caught it.

The cell-type dispatch moves out of DefaultTable.loadCellRenderers into
TableCellComponents.forType. It was only testable by reading the method's
source text and looking for each constant's name - a test that could not tell
a real dispatch from a mention in a comment and depended on the working
directory. It is now called directly. Custom-column sharing stays in the table,
where it belongs, and is asserted by identity rather than inferred from how
many text sources were consumed.

Tests: 520 total. 503 run in the build that must stay green, and it is green.
The other 17 assert properties that framework/ and gui_old/ classes do not
hold; none of those classes has a caller anywhere in src/main, so the failures
are specifications for whoever revives or deletes them rather than defects
anyone can hit. They are tagged and split into their own CI job whose expected
count is asserted, so that a fix and a regression are both build failures
rather than a number nobody reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Validating the whole rectangle before writing any of it was right, but it
refused a value no column can hold rather than recognising a cell that holds
no value - and every spreadsheet produces those.

Excel, LibreOffice and Sheets all terminate a copied range with a newline.
Splitting on newlines with a negative limit keeps the empty field after it, so
a 2x2 copy arrives as three rows, the last holding one empty cell. The dry run
handed that empty string to Integer.parseInt, which threw, and the paste was
refused in full. Copying a block out of a spreadsheet and pasting it in - the
commonest gesture the sheet supports - wrote nothing at all on every numeric
and combo box column of every sheet.

Two things were wrong and both are fixed:
  - the trailing newline is a terminator, not a row. Counting it also inflated
    the tiling factor, so a paste into a larger selection repeated the wrong
    number of times.
  - an empty cell means "nothing here", not "write nothing here". It is now
    skipped for numeric, checkbox and combo box columns, by both the dry run
    and the write loop so the two agree cell for cell. Text columns still take
    it as a value, because clearing a name is a real edit.

Neither was caught because TestSheet reported CellTypes.STRING for every
column, and prepareObjectForWriting does nothing at all for text - so nine
paste tests exercised the geometry and none of the conversion. The fixture can
now declare real types, its setValueAt converts the way the real sheets do,
and NumericPasteTest drives a paste through columns that actually validate.
Reintroducing either half of the fix turns it red.

Two testability defects made this possible and are fixed rather than worked
around, since both are the reason the gap existed:
  - PasteAction read Toolkit.getDefaultToolkit().getSystemClipboard() inline,
    which needs a display, so the paste path could only be reached by
    reflectively replacing the AWT toolkit. It now goes through a method a test
    can override, and returns null instead of throwing where there is no
    display.
  - the rejection path called JOptionPane unconditionally, which throws
    HeadlessException from inside the error path and replaces the problem being
    reported with a different one.

Also:
  - the type range is derived from PersonalData.NUMBER_OF_TYPES rather than
    restating 18. Gen 4 has 18 types numbered 0..17, and typeColors has 18
    entries, so 18 itself is one past the end - the previous comment argued for
    a bound its own reasoning ruled out.
  - CellTypesTest.integerEditorHonoursTheDeclaredRange asserted isNotNull(),
    which would pass against an editor that ignored the range entirely - the
    exact fallback the previous commit added. It now drives values either side
    of both bounds. The bound is enforced when the value is read, not by the
    document filter, which accepts "128" quite happily.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
DataManager.getData takes a ROM and then ignores it: the cache is keyed by data
class alone and never cleared. Opening a second ROM in one session returned the
first ROM's parsed data for every format, and because the cache was populated
nothing re-parsed - so saving wrote the first ROM's personal table, TM list and
everything else into the second. The ROM parameter made it look as though this
had been considered.

The caches now record which ROM they hold and discard themselves when handed a
different one. Compared by identity rather than equality: two ROMs loaded from
the same file are separate objects with separate edits, and treating them as
interchangeable is the same bug in a quieter form. Every entry point that takes
a ROM goes through the check, so no route can skip it.

An earlier commit claimed this was fixed by making PersonalParser's tables
instance fields instead of static. It was not, and could not have been - the
parser is bound as a Guice singleton, so there is one instance for the lifetime
of the process either way. The static fields were worth removing on their own
merits; they were not this bug.

Also fixes resetData, which called dataMap.remove(newList) - passing a List to
a map keyed by Class, so it removed nothing at all.

The type range now derives from the number of type colours the sheet actually
has, rather than restating a count that Core no longer enforces. It is a
statement about what this table can display, which is where a limit like that
belongs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Two regressions from the previous commits, and the false claims that hid them.

TmCompatibilityTable asked DataManager for the move list with a null ROM. That
was harmless while the ROM argument was ignored on a cache hit; once the caches
became ROM-scoped it read as "a different ROM" and reset for it. Double-clicking
a TM column header and picking a move therefore discarded every open sheet's
parsed data, every unsaved-change flag and every code binary - and the binaries
are populated exactly once, at startup, so nothing ever put them back. From that
click onward every save reported a fatal error and reload silently did nothing.
The session was unrecoverable, and the user's work was gone before the stack
trace appeared.

The move list is now passed in when the sheet is built, where a ROM is in hand.
A null ROM is refused outright rather than treated as a no-op: a no-op would
stop the crash while leaving the caches free to answer for the wrong ROM, which
is the failure the scoping exists to prevent, and it would do it silently.

useRom no longer clears the dirty flags. Whether unsaved work may be discarded
is a decision to put to the user at the moment they switch ROMs, not one a cache
coherency helper settles on their behalf - clearing them means the exit prompt
goes quiet and the edits vanish without a word.

Reassigning a TM's move was never marked dirty at all. The change lives on the
parser rather than in any sheet's data, so the next save wrote it while the exit
prompt insisted there was nothing unsaved.

Both save confirmations now happen before anything is prepared. processDataList
is not side effect free - PersonalParser writes the TM table straight into the
shared arm9 buffer and PokemonSpriteParser does the same - so preparing first
and asking afterwards left arm9 already modified when the user declined, and the
next confirmed save of any sheet wrote that cancelled edit to disk. Reloading
could not undo it either, because the reload re-reads the TM table out of the
arm9 it had just mutated. The file list the dialog needs comes from the parser's
own requirements, which is the same set the prepared output would have yielded,
so nothing has to be serialised to ask the question. This also covers the git
commit-message prompt, which was a second way to back out with arm9 dirty.

Two comments claiming otherwise are corrected rather than deleted: useRom does
not in fact cover every entry point that takes a ROM (commitData and
saveCodeBinaries do not come through it), and the cross-ROM contamination it
guards against cannot currently be reached at all - the three menu entries that
would open a second ROM are unimplemented and closing the tool frame ends the
process. It is kept as a guard against a future capability, and now says so.

The field script editor is no longer built or shown. It is the one editor that
compiles scripts back into the ROM, so a half-working version damages a project
rather than merely disappointing; constructing it is also what pulls in
VariableTracker, which is not published anywhere.

Main no longer dies on a missing jokes file. It has never been committed, on any
branch, and the resource stream was dereferenced unguarded - so a clean checkout
could not start at all. Nothing cosmetic should be able to prevent launch.

Tests for the caches, which had none. A refused call must not discard parsed
data, code binaries or dirty flags; every entry point must refuse null the same
way; and the file list must be obtainable without side effects.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
…thers

Personal, TM compatibility, Evolutions and Learnsets all read the same species
name bank, across three different data classes. Adding or removing a row shifts
that shared bank but fires table events only on the sheet being edited, so every
other open sheet keeps displaying names that have moved underneath it - each one
now labelling the entry above or below the one it belongs to.

Nothing reports this. The sheet being edited looks correct, the others are only
wrong if you go and look, and because the bank is marked dirty the next save of
any sheet writes it to the ROM. One click on the Evolutions sheet renames every
species from that row down.

The other sheets are now told. The notification walks the open panels the same
way resetAllIndexedCellRendererText already does, matches the bank by identity
since the point is that these sheets hold the very same object, and fires a data
change rather than a structure change - a structure change would discard the
column widths and cell renderers the tables configure when they are built, and
would not rebuild them.

This corrects the view, which is what stops the silent wrong-row edit. It does
not make the underlying operation sound: these tables are positional, so
deleting a row renumbers every entry after it and anything referring to those
entries by index now points somewhere else. Whether the row buttons belong on
species-indexed sheets at all is a separate question, and the comment says so
rather than implying the matter is settled.

Also in this commit:

codeBinarySetup now writes arm9 through the lock, like every other arm9 write.
Not for mutual exclusion - nothing is concurrent at startup - but because lock()
and unlock() are what maintain the buffer's cursors, extending the recorded size
to whatever was written and returning the writer to the end. getData() returns
the bytes between the cursors, so the hand-rolled save and restore this replaces
was a weaker copy of the same protocol that could truncate arm9 on save. The fix
removes code rather than adding it.

Two more comments corrected. resetData claimed that prepareData no longer mutates
the ROM; moving the confirmations ahead of preparation means a cancelled save
leaves the ROM untouched, but preparation itself still writes to arm9, so a save
that fails after confirmation can leave it partly written. And disabling the
field script editor was described as removing the VariableTracker dependency,
which it does not - the dependency is still declared and ScriptDocument still
imports it, so a clean checkout still cannot resolve it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Reported: typing a move name into a sheet errors out, so the move has to be
found in the list by hand.

The combo boxes have type-to-search, and a search that does not land on an exact
match leaves the selection empty rather than guessing. getCellEditorValue
reported that empty selection as -1, which the cell range check then refused -
so an edit that had simply not chosen anything came back as an error dialog.

Before the range check existed the same -1 was written into the data and masked
at save into whatever bit pattern it happened to make: a learnset move of -1
became move 511. So the check is right and the reporting was wrong. The editor
now hands back the value the cell already held, which makes a selection of
nothing a no-op - exactly what the numeric editor has always done with input it
cannot use.

Same treatment for the bitfield editor, which had the same shape and answered
"no flags set" for a selection that had chosen nothing.

Three tests: an edit that selects nothing keeps the old value, a real selection
is still reported, and a value the column has no name for opens and survives
being closed. That last one matters for hacked ROMs, where an index past the
name list is legitimate data the editor must not quietly replace.

Also adds TECH_DEBT.md, recording what this bug-hunting pass found and did not
fix. Everything in it was reproduced before being written down, and the entries
that are judgement calls rather than defects say so. The two worth knowing
about: row add and delete are unsound on species-indexed sheets, because the
tables are positional and deleting a row renumbers every entry after it; and in
Nds4j, saving a scanned sprite destroys it unless its top-left pixel is index 0,
because the decoder takes its key from the first word of the ciphertext.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Typing into a combo box in a sheet threw IllegalAccessError on Windows:

  class com.jidesoft.plaf.LookAndFeelFactory cannot access class
  com.sun.java.swing.plaf.windows.WindowsLookAndFeel (in module java.desktop)

jide-oss picks its style with "lnf instanceof WindowsLookAndFeel", on the
branch taken for every look and feel it does not recognise - FlatLaf, which
is the one we set, among them. Every JIDE component reaches it, because
JidePopup.updateUI() calls installJideExtension() and updateUI() runs from
the JComponent constructor; we arrive there because EditorComboBox installs a
ComboBoxSearchable whose search popup is a JidePopup.

An instanceof resolves its class before it can answer false, so the platform
decides how it breaks. On Windows java.desktop holds that package and does
not export it, so resolution fails outright. Elsewhere no JDK ships the
package at all, and the system-scoped WinLaF.jar at the repository root is
what has been quietly satisfying it.

Neither remedy covers the other case. Parent-first delegation finds
java.desktop's copy first, so WinLaF.jar is shadowed on Windows and could
never have helped there; Add-Exports naming a package a module does not have
is ignored silently, so carrying it everywhere costs nothing. Both were
reproduced and both verified, the Windows condition by patching the class
into java.desktop so it was present but encapsulated, exactly as it is there.

The runnable jar had no build at all - it was assembled by hand outside the
repository, which is why the fix had nowhere to live. A dist profile now
builds it, writes the manifest entry, and unpacks WinLaF.jar, which the shade
plugin otherwise drops because system scope is neither compile nor runtime.
The default build and CI are untouched.

JideLookAndFeelResolutionTest guards the classpath half: nothing names that
class, so WinLaF.jar reads as dead weight, and removing it turns the test red
rather than surfacing as a crash when a user types a move name. The manifest
half cannot be asserted from inside a JVM that has not built the jar yet, so
it is documented instead.

Also recorded in TECH_DEBT.md: JIDE is used for exactly one class, and
replacing it would retire the system-scoped jar and this whole class of
failure. Its os.version table also stops at 6.2, so on Windows 10 and 11 its
own isWindowsVistaAbove() is false and the crash arrives via XPUtils.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Nothing said how. The README pointed at the Releases page, and the dist
profile's own comment explains why the manifest entry exists without ever
saying which command writes it. The build order for the three sibling
libraries lived only in the CI workflow, so anyone building by hand had to
read build.yml to discover that Nds4j has to be installed before the other
two, and that a change spanning several repositories is developed on branches
of the same name in each.

Records the part that is easiest to get wrong: hand-assembling the jar, which
is how it was produced until now, silently drops both halves of the JIDE
look-and-feel workaround. The result starts cleanly and then dies the first
time someone types into a combo box in a sheet - IllegalAccessError on
Windows, NoClassDefFoundError elsewhere, and neither remedy covers the other
case.

Also states plainly that a clean checkout does not build yet, because
VariableTracker is published nowhere the build can reach.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
The jar handed to testers was assembled by hand outside the repository, which
is how it came to ship without either half of the jide-oss look-and-feel
workaround. CI now builds it with the dist profile on every push and uploads
it as a run artifact, so a tester can take it from the Actions run.

Building it is not the same as checking it. Shade succeeds whether or not the
manifest carries Add-Exports and whether or not WinLaF's classes made it in,
so a build-only step would go green on precisely the artifact that crashes on
the first keystroke in a sheet. Both halves are executed instead:

  - on the runner's own terms, which covers WinLaF being dropped - nothing in
    src/main names that class, so it reads as dead weight, and losing it is a
    NoClassDefFoundError for every Linux and macOS user;
  - under the Windows condition, reproduced by patching the class into
    java.desktop so it is present but encapsulated as it is there, and by
    giving jide-oss an os.version it recognises, since its table stops at 6.2
    and its own isWindowsVistaAbove() is false on Windows 10 and 11.

The exports applied are the ones the jar itself declares, so trimming the
manifest fails the step. A third run then withholds them and requires the
probe to fail: without that, a simulation that stopped reproducing the
condition would leave a step that passes forever and proves nothing. Both
failure modes were confirmed by breaking a built jar each way and watching
the step go red.

Reading the manifest needs the helper rather than grep. The value spans three
lines and the wrap currently falls inside sun.awt.shell; which tokens split
moves with the value, so a present package can read as absent and a split one
becomes a garbage --add-exports flag rather than an error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
ComboBoxCellEditor keeps the value a cell arrived with so that an edit
selecting nothing leaves it alone - type-to-search empties the selection
whenever the typed text matches no entry exactly, which is what made typing a
move name raise an error instead of editing.

BitfieldComboBoxEditor had the same guard and it did not work. The subclass
overrode getTableCellEditorComponent without calling super, so the value was
never recorded, and getLastValue() returned null: opening a flag cell and
committing without picking handed back null rather than the flag. Not -1, and
not the old value - the cell losing its contents. Mistyping a flag name was
enough to reach it, as was merely opening a cell holding a bit the column has
no name for, since that deliberately shows no selection.

Recording the value is now final and subclasses override selectValue instead,
so the mapping from value to entry can be changed while the bookkeeping
cannot be skipped. This was the parent's own bug reappearing one level down,
which is a sign the two were separable when they should not have been.

The three tests covering this on the parent had no counterpart here, which is
why the subclass shipped broken. Three are added, and reintroducing the defect
turns two of them red; the third is the control that a genuine selection is
still reported.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Auditing the other two editors for the defect fixed in the combo boxes found
it in the numeric one, reachable and live.

A Learnsets row is only as long as that species' learnset, and the read path
deliberately returns null past its last entry rather than growing the list -
scrolling must not invent moves. So most of that sheet is blank. Opening a
blank cell and clicking away put a modal error in front of the user, saying ""
is not a valid value, for an edit that changed nothing. A stray double click
was enough.

Committing it would have been worse than the dialog: setValueFor pads every
entry up to the one being set, so writing to a blank cell injects junk moves,
which is exactly what the read path is careful not to do. The edit is now
cancelled rather than rejected - no dialog, and no write. Emptying a cell that
held a value is still refused, because that is a real attempt to store nothing
in a numeric column.

The fallback that hands the cell back unchanged also stored
String.valueOf(value), which turns a blank cell into the four letters "null" -
so the value offered as the cell's new contents was text the write path then
refused as not a number. It keeps the value itself now.

Adds the join nobody was testing. The editors were covered alone and the write
path was covered alone, and both stayed green while the sheet was broken,
because the bug was in the handoff: a cleared selection reported as -1, and -1
is outside every column's range. EditorToWritePathTest runs the editor the
sheet actually installs for a column and pushes what it produces into that
column's write path. Reintroducing the original defect fails it with the error
the user saw - "-1 is outside the range 0 to 511".

CheckBoxEditor has the same shape and is not reachable; TECH_DEBT.md records
why, and that what keeps it safe is a frozen column's isCellEditable rather
than anything in the type declarations.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
Sweeping for open items not already written down turned up four, plus one
that was written down wrongly.

The sequencing section said Nds4j had to be published to Maven Central before
the other three could merge. That is not true and it would have held up the
merge for no reason: nothing in CI resolves these from Central. Each build
clones the sibling at a branch of the same name, or main, and builds from
source - so Nds4j merging to main is what unblocks the others, and publication
matters to users and to the README coordinate, not to the build.

Newly recorded: PokEditor's build fails at dependency resolution and skips
every step after, so its pull request cannot go green while VariableTracker is
unpublished, whatever else is fixed. The signing subkey has expired and is on
only one of the two keyservers. Four repository secrets are still absent.
maven-publish.yml publishes to OSSRH on JDK 8, which is decommissioned and
returns 404, and sits alongside the release.yml that replaced it.

And the one open decision: CodeBinary.compressed is private, unread, and
reachable only by reflection from its own tests. Expose it or delete it, but
not after 1.0.0 is permanent on Central. It is the only unresolved review
thread across the four pull requests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
isCompressed() is public and the OSSRH workflow is gone, so both stop being
open questions. Records what the getter cannot say for itself - the flag is
about the constructor argument, not the current contents - and that
wasCompressed() would have been the better name, since that is the half no
longer changeable.

maven-verify.yml is noted rather than deleted: same vintage, but it fires on
no branch, so it is neither running nor blocking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
It shipped as isCompressed() and was renamed before merge. The name mattered
twice: it describes the constructor argument rather than the object, and
Overlay already had an isCompressed() reading the overlay table bit, so the
base-class method was silently overridden by a subclass answering a different
question.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
maven-verify.yml looked like coverage for the scanned NCGR path and was not:
it deleted the ROM it downloaded by checking out after fetching it, and ran on
a JDK too old to compile the tests. Worth stating next to the coverage gap,
because the workflow existing is why the gap looked smaller than it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
The section listed three defects and no note about coverage, which made it
look like the thinnest of the four. It is the opposite: 3,834 lines of main
against 588 of test, reaching two files. Tool.java has 1,299 lines and no
tests, and all three recorded defects are in the untested files - FileUtils
was the one part with tests and the one part that got fixed properly.

Also states what the numbers hide in the other direction: the three
atomic-write tests that skip locally are the ones with teeth, and they skip
only because a container runs as root. CI is not root and runs all 25.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
A sheet's save writes several NARCs and arm9. It wrote them one at a time, so
a failure part way through left the project holding a combination it was never
in - a new personal.narc beside the TM table that was meant to change with it.
ToolUI now stages a batch before replacing anything, so the failures that
happen in practice leave the project entirely as it was.

TECH_DEBT records the three items as fixed, and keeps what the fix does not
cover: the run of renames at the end is not one operation, so a process killed
between two of them still leaves a mixture.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
PokEditor declared JGit 6.7.0 and never referenced it. A direct dependency
beats a transitive one, so the version ToolUI asks for was being overridden
here - and upgrading ToolUI to fix the SSH-signing crash would have left the
shipped application on the broken version with every test still green.

Removed rather than bumped. Nothing here uses JGit, and re-pinning it is
exactly how the two drifted apart.

Verified in the built artifact rather than the dependency tree alone: the dist
jar carries JGit 7 classes, and its manifest is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
jsvg belongs to flatlaf-extras, which is what draws the SVG icons; declaring it
here pinned it a version ahead of what flatlaf asks for, with nothing importing
it and so nothing to fail if the pin were wrong. jackson-dataformat-xml and
junit:junit are unused outright - this module is entirely on JUnit 5.

jackson-databind is declared instead, because DataManager does import it and
was reaching it through the xml artifact by accident.

flatlaf-intellij-themes and Guice stay: unlike ToolUI and Core, this module
genuinely uses both.

Verified on a clean build and in the shipped jar rather than the dependency
tree - jsvg, the themes, Jackson and JGit are all present, and an SVG icon
still renders from the jar itself, which is the only way to see a missing
renderer. flatlaf reports nothing when it is absent; the icons simply come out
blank.

TECH_DEBT records the rule and the two traps: dependency:analyze cannot see
runtime loading, and a dependency change reports green on stale classes unless
the build is cleaned.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
The uncaught handler printed the stack trace and told the user to see the
command line. This is launched by double-clicking a jar, which on Windows runs
through javaw and has no console - so the trace went to a stream with no reader
and the dialog directed the user to something that does not exist. Both times a
user reported a crash on this branch, the diagnosis had to be reconstructed
from source because nothing had been kept.

ToolLog.begin now runs first, before the handler and before anything prints,
and the dialog names the log file so a report can carry the stack trace instead
of one line of message. When no file could be opened it says that plainly
rather than naming a path that is not there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
VariableTracker existed only as a local ~/.m2 1.0-SNAPSHOT and one
un-remoted clone, so PokEditor could not build from a clean checkout and
CI failed at "Verify dependencies resolve". Vendor the whole
io.github.turtleisaac.variabletracker package (models, Swing panels, hex
cell helpers, .jfd forms, and variable_tracker/*.properties) under
src/main and drop the Maven dependency. Its only third-party needs -
Jackson, FlatLaf, MigLayout - were already declared here.

Add a ScriptVariable(String, int) constructor that neither the committed
source nor the published artifact had, so ScriptDocumentHighlightingTest
compiles. Mark the corresponding TECH_DEBT.md blockers resolved.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Merging and releasing" section described a world that no longer
exists. It said Nds4j merges first and that main is still 0.1.0; that
the other three clone a sibling at a matching branch name and build it
from source; that publication to Central is not a merge prerequisite;
that four repository secrets must be created and the signing subkey
extended before release.yml can run; and that PokEditor's build is red
on the unpublished VariableTracker. Every one of those is now either
done or wrong, which made the file's most operational section the least
trustworthy part of it.

Replaced with what is actually true, each point checked rather than
recalled:

- What is published. Nds4j 1.0.0 resolves from Central with jar, pom,
  sources, javadoc and signature all present; ToolUI and PokEditor-Core
  both 404. The signature is the evidence that the secrets exist and
  the key works, since release.yml has run once, on the v1.0.0 tag, and
  succeeded.
- How to build the chain locally, which is now the only way to exercise
  a change spanning two repositories, and the better test anyway: it
  runs the exact combination about to ship rather than whichever
  branches happened to share a name.
- That ToolUI's and Core's CI no longer build Nds4j from source, what
  that costs, and that PokEditor's CI still builds the two unpublished
  siblings because nothing else can resolve them.
- That a version bump now propagates only by hand, and that Nds4j main
  declares 1.0.0 while sitting 8 commits past the tag of that name,
  having gained Screen and CellAnimation and a rewritten CellBank. So
  installing Nds4j main locally overwrites the published 1.0.0 in the
  local repository with different code -- worth knowing before
  following the local build steps.
- That neither ToolUI nor Core has any publishing path: no release
  workflow, no publishing, gpg or javadoc plugin, and none of the six
  pom elements Central requires. Left as an open decision, with the
  case for each stated, rather than as a defect.

Writing the local build steps turned up something the file should have
said all along: a plain "mvn verify" on PokEditor goes red. 545 tests
run and 17 fail, and all 17 are the @tag("dead-code") specifications
that are meant to fail. CI passes -DexcludedGroups=dead-code, which is
why CI is green; anyone following build steps without it would conclude
the build was broken. The steps now carry the flag and say why. Checked
both ways: 545 run with 17 red, 528 run and green with the exclusion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RqYGZhtVb8KrPA9ftp5bob
@turtleisaac
turtleisaac merged commit 4b6ff30 into main Aug 27, 2026
2 checks passed
@turtleisaac
turtleisaac deleted the claude/pokeditor-bug-deep-dive-9mm03b branch August 27, 2026 22:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants