Skip to content

fix(export): address adversarial review findings in PR #60 PoE2 build export#76

Merged
jay9297 merged 3 commits into
feature/poe2-build-export-improvedfrom
claude/pr-60-adversarial-review-5kq95l
Jun 14, 2026
Merged

fix(export): address adversarial review findings in PR #60 PoE2 build export#76
jay9297 merged 3 commits into
feature/poe2-build-export-improvedfrom
claude/pr-60-adversarial-review-5kq95l

Conversation

@jay9297

@jay9297 jay9297 commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes all issues identified in the adversarial review of PR #60. Changes are scoped strictly to the export feature branch (feature/poe2-build-export-improved).

Fixes applied

CRITICAL

  • nodeNotes lost on undo/tree-copy (PassiveSpec.lua): Added nodeNotes = copyTable(self.nodeNotes) to CreateUndoState and self.nodeNotes = state.nodeNotes or {} in RestoreUndoState. Without this, every Ctrl+Z after setting a node note silently erased it, and copying a passive tree lost all notes.
  • io.popen('ls -d ...') crash + injection (BuildExportPoE2.lua): Replaced with os.rename(path, path) — a POSIX-guaranteed no-op when the path exists. No shell is spawned, no path content is interpolated into a command string. Eliminates both the Windows crash risk and the shell injection vector.

HIGH

  • warnedNoStringId module global (BuildExportPoE2.lua): Removed the per-session warn-once guard. The warning is now returned as a third value from buildPassivesBuildTableExportWriteFile, so every call site gets fresh state. ImportTab.DoPoE2Export now displays the warning as a colorCodes.WARNING status instead of silently succeeding.
  • Non-deterministic JSON output (BuildExportPoE2.lua): table.sort(order) added in buildPassives before emitting; buildItems now iterates a sorted copy of SlotMap keys. Eliminates pairs()-over-hash-table ordering surprises.

MEDIUM

  • PresetNextLevels anyHas checks only levelMax (BuildExportPoE2.lua): Changed to levelMin or levelMax and guarded the maxLvl update with entry.levelMax and — prevents double-seeding an entry that already has only levelMin set.
  • autoBracket gives hi < lo for n > 99 (BuildExportPoE2.lua): Changed hi formula to use m_max(lo, m_floor(i/n * 99)), guaranteeing hi >= lo for any n.
  • SkillsTab clampLvl fallback uses global math.floor (SkillsTab.lua): Added local m_floor = math.floor at module top; fallback now uses the local alias consistently with the rest of the file.
  • self.build.modFlag vs self.modFlag (SkillsTab.lua): Level-bracket edit callbacks now correctly set self.modFlag (tab-level dirty flag) instead of self.build.modFlag.

LOW

  • Note popup fires on unallocated nodes (PassiveTreeView.lua): Gated both the Shift+Right-Click popup and the tooltip hint on hoverNode.alloc / node.alloc. The tooltip now shows existing note content prominently when present, with the edit hint below.
  • Misleading gemIdFor comment (BuildExportPoE2.lua): Replaced the factually incorrect "typo in gameId" claim with an accurate description of why gemId is preferred over gemData.gameId.

Test plan

  • Run full suite: docker compose run --rm busted-tests
  • Verify new PresetNextLevels test case (levelMin-only entry) passes
  • Verify new Export warning-return test passes
  • Manually: allocate a passive node, Shift+Right-Click to add a note, press Ctrl+Z — note should survive undo
  • Manually: copy a passive tree — notes should be preserved in the copy
  • Manually: export a build and confirm the status label shows the warning when tree.lua lacks stringIds

https://claude.ai/code/session_01EvGnnyxkoMawLmWWqCTxxX


Generated by Claude Code

CRITICAL
- PassiveSpec: include nodeNotes in CreateUndoState/RestoreUndoState so
  Ctrl+Z and tree copy no longer silently erase per-node notes
- BuildExportPoE2: replace io.popen('ls -d ...') in dirExists with
  os.rename(path,path) — eliminates the Windows crash risk and the shell
  injection vector from unescaped path content

HIGH
- BuildExportPoE2: remove warnedNoStringId module-level global; surface
  the "no stringId in tree.lua" warning as a third return value from
  Export/WriteFile so each call site gets fresh state and the UI can
  display it prominently (ImportTab now shows colorCodes.WARNING status)
- BuildExportPoE2: sort `order` before emitting in buildPassives and sort
  SlotMap keys before iterating in buildItems to produce deterministic
  JSON output regardless of LuaJIT hash table iteration order

MEDIUM
- BuildExportPoE2: PresetNextLevels anyHas now checks `levelMin or
  levelMax` (was levelMax-only), preventing double-seeding of an entry
  that already has only levelMin set
- BuildExportPoE2: autoBracket uses m_max(lo, ...) for hi so hi >= lo
  is guaranteed even for n > 99 sets
- SkillsTab: add local m_floor alias; fix clampLvl fallback to use it
  instead of the global math.floor; fix level-bracket callbacks to set
  self.modFlag instead of self.build.modFlag

LOW
- PassiveTreeView: gate the per-node note popup and tooltip on
  hoverNode.alloc — notes only make sense on allocated nodes; tooltip
  shows existing note content when present
- BuildExportPoE2: replace factually-incorrect "typo in gameId" comment
  on gemIdFor with an accurate description

Tests: add cases for levelMin-only anyHas, Export warning return value

https://claude.ai/code/session_01EvGnnyxkoMawLmWWqCTxxX
@github-actions

Copy link
Copy Markdown

🔄 Cycle 1, attempt 1/3

src/Classes/ImportTab.lua:405path:gsub("%^", "^") replaces ^ with ^, a no-op. safePath is always identical to path. If the intent is to escape caret characters for the color-code renderer (^x33FF77 prefix), the replacement must be "^^".

src/Modules/BuildExportPoE2.lua (dirExists)err:find("No such file") and err:find("does not exist") are locale-sensitive; on non-English libc locales ENOENT carries a different message, causing the function to return true for a missing directory and silently attempt writes into a non-existent path.

Missing test — No test exercises the passiveWarning-non-nil path (the sawMissingStringId and not sawStringId branch in buildPassives); the new test only validates the nil case.

✅ Agent succeeded (cycle 1, attempt 1)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST_CHANGES. See review comment above.

- ImportTab: fix safePath gsub no-op ('^' -> '^^' to actually escape
  caret characters for PoB's color-code renderer)
- BuildExportPoE2: make dirExists locale-independent by checking only
  ok == true from os.rename; error strings are locale-sensitive and
  cannot reliably distinguish ENOENT from EACCES
- Tests: add Export test covering the passiveWarning non-nil path
  (allocated node with no stringId triggers the degraded-export warning)

https://claude.ai/code/session_01EvGnnyxkoMawLmWWqCTxxX
@github-actions

Copy link
Copy Markdown

🔄 Cycle 1, attempt 1/3
SkillsTab.lua:1459,1468self.modFlag = true is correct here; Build.lua:1253,1374 explicitly reads skillsTab.modFlag to track unsaved state and the rest of the file uses the same pattern. The original self.build.modFlag was the bug.

PassiveSpec.lua:2396copyTable(self.nodeNotes) is safe; nodeNotes is always initialized to {} at line 104 of the constructor.

BuildExportPoE2.lua:68 — The m_max(lo, ...) guard in autoBracket fixes a real underflow (e.g., n≥100, i=1 gives m_floor(1/100*99)=0 < lo=1), but no test covers this edge case.

✅ Agent succeeded (cycle 1, attempt 1)

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: COMMENT. See review comment above.

Adds an Export test that places 100 passive specs with one allocated
node in the first spec, then asserts the emitted level_interval has
lo <= hi. Regresses the underflow that existed before the m_max fix
(n=100, i=1 gave hi=floor(1/100*99)=0 < lo=1).

https://claude.ai/code/session_01EvGnnyxkoMawLmWWqCTxxX
@github-actions

Copy link
Copy Markdown

🔄 Cycle 1, attempt 1/3
All clean. self.modFlag on SkillsTab is properly wired into build:ResetModFlags() and the unsaved check at Build.lua:1374.

✅ Agent succeeded (cycle 1, attempt 1)

@jay9297
jay9297 marked this pull request as ready for review June 14, 2026 00:16
@jay9297
jay9297 merged commit 4186552 into feature/poe2-build-export-improved Jun 14, 2026
1 check passed
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