Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
346a9a9
docs(advisor): add ADVISOR-DEV.md dev/test/rebase workflow (#77)
jay9297 Jun 17, 2026
a6bcca8
merge(advisor): #77 dev env verify + ADVISOR-DEV.md
jay9297 Jun 17, 2026
918aacd
feat(advisor): add Advisor tab UI scaffold wired into Build (#78)
jay9297 Jun 17, 2026
b804b07
merge(advisor): #78 tab scaffold (UI shell)
jay9297 Jun 17, 2026
745957b
feat(advisor): add Advisor.analyze pipeline, resist check, render + s…
jay9297 Jun 17, 2026
c2f4ac2
merge(advisor): #79 logic module + render pipeline + resist check
jay9297 Jun 17, 2026
3597d21
feat(advisor): add survivability checks (surplus, EHP, recovery, miti…
jay9297 Jun 18, 2026
59965d3
merge(advisor): #80 survivability checks
jay9297 Jun 18, 2026
e878978
feat(advisor): add skill/support/attribute/spirit checks (#81)
jay9297 Jun 18, 2026
d4b03a5
merge(advisor): #81 skill/support/attribute/spirit checks
jay9297 Jun 18, 2026
6a65a7b
feat(advisor): add passive-tree efficiency checks (adjacent notables,…
jay9297 Jun 18, 2026
9cc5f46
merge(advisor): #82 passive-tree efficiency checks
jay9297 Jun 18, 2026
75f81cf
feat(advisor): tab UX polish — grouping, filters, click-to-jump, live…
jay9297 Jun 18, 2026
5d4ef3e
merge(advisor): #83 tab UX polish
jay9297 Jun 18, 2026
449ef36
test(advisor): add headless smoke test; docs ADVISOR.md + upstream-hy…
jay9297 Jun 18, 2026
cdce47b
merge(advisor): #84 tests, docs & hygiene
jay9297 Jun 18, 2026
6c26b37
chore(advisor): add cspell:ignore directives for domain terms (maxhit…
jay9297 Jun 18, 2026
87879bc
fix(advisor): address three correctness bugs from AI review (#89)
jay9297 Jun 18, 2026
557c968
Merge branch 'dev' into feat/advisor-epic
jay9297 Jun 18, 2026
91d23cf
fix(advisor): address second-round AI review findings
claude Jun 18, 2026
be5b6e8
fix(advisor): address third-round AI review findings
claude Jun 18, 2026
53f14ae
fix(advisor): address fourth-round AI review comments
claude Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions ADVISOR-DEV.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Advisor — Developer notes

<!-- cspell:ignore pathofbuilding pathofbuildingcommunity headlessly maxhit deadend -->

Working notes for the **Advisor** feature (native, fully-offline deterministic
build-advisor tab). Tracking epic: [#85](https://github.com/jay9297/PathOfBuilding-PoE2/issues/85).

## Where development happens

This repository (`jay9297/PathOfBuilding-PoE2`) **is** the dev fork. All Advisor work lives
on the epic branch `feat/advisor-epic`; each sub-issue (#77–#84) is built on its own
`feat/advisor-NN-*` branch off the epic branch and merged back up. The epic branch is the
single unit reviewed against `dev`.

> The upstream community repo is the `origin` remote here; the fork is the `fork` remote.
> PRs target `fork/dev` — **never** upstream (see `CLAUDE.md` › Fork & PR rules).

## Running

- **Headless** (verified, CI path): the build loads under a plain LuaJIT interpreter via
`src/HeadlessWrapper.lua`, which stubs the rendering callbacks. This is how the test
suite boots and is the model for any scripted use — `loadBuildFromXML(...)` then read
`build.calcsTab.mainOutput`. The Advisor logic module (`src/Modules/Advisor.lua`) is
designed to be exercised entirely on this path.
- **GUI** (SimpleGraphic desktop runtime): launched via `src/Launch.lua`. Not available in
the headless/CI environment used for this work; verify UI changes on a desktop checkout.

## Tests

The suite uses [Busted](https://lunarmodules.github.io/busted/) with LuaJIT inside the
pre-built container image `ghcr.io/pathofbuildingcommunity/pathofbuilding-tests`.

`CLAUDE.md` documents the `docker compose` form. This dev machine has **no Docker** — only
Podman, and no compose provider — so use a plain `podman run` that mirrors
`docker-compose.yml` (image, `HOME=/tmp`, read-only `/workdir` mount, `busted --lua=luajit`
entrypoint):

```bash
# Full unit suite (what CI calls "unit"): excludes slow build-snapshot + data tags
podman run --rm -e HOME=/tmp -v ./:/workdir:ro -w /workdir \
--security-opt no-new-privileges:true \
--entrypoint busted ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest \
--lua=luajit --exclude-tags builds,data

# Just the Advisor specs (fast inner-loop while iterating)
podman run --rm -e HOME=/tmp -v ./:/workdir:ro -w /workdir \
--security-opt no-new-privileges:true \
--entrypoint busted ghcr.io/pathofbuildingcommunity/pathofbuilding-tests:latest \
--lua=luajit --filter="Advisor"

# Data-file validation (run after any src/Data change)
# ... --lua=luajit --tags data
```

On a machine with Docker, the documented `docker compose run --rm busted-tests ...` forms
in `CLAUDE.md` work identically.

**Baseline** on `feat/advisor-epic` (off `dev`): the full unit suite is
**green — 481 successes / 0 failures / 0 errors / 0 pending** (~9.5 min). Treat any new
failure as a regression.

## Rebasing on upstream

The fork's `dev` tracks the upstream community `dev` (the `origin` remote). To pick up an
upstream release:

```bash
git fetch origin
git switch dev && git merge origin/dev # fast-forward / merge upstream into fork dev
git push fork dev
git switch feat/advisor-epic && git rebase dev # replay the epic on the new base
```

The feature keeps its surface area small to make this cheap — see the touched-files list in
`ADVISOR.md` (added in #84).

## Upstream files touched

The Advisor feature is intentionally isolated to keep upstream rebases cheap. The only
modified upstream file is `src/Modules/Build.lua`, with three small additions:

| Location | Change |
|---|---|
| tab instantiation (~L519) | `self.advisorTab = new("AdvisorTab", self)` |
| mode-button row (~L347) | `modeAdvisor` button + `locked` + dynamic high-severity `label` badge |
| draw dispatch (~L1363) | `elseif self.viewMode == "ADVISOR" then self.advisorTab:Draw(...)` |

Everything else is new, self-contained files:

- `src/Classes/AdvisorTab.lua` — UI tab
- `src/Modules/Advisor.lua` — analysis logic
- `spec/Modules/Advisor_spec.lua` — unit tests
- `spec/System/TestAdvisorSmoke_spec.lua` — headless smoke test
- `ADVISOR.md`, `ADVISOR-DEV.md` — docs

Because the upstream surface is one file with three additive hunks, rebasing on an
upstream release rarely conflicts; if it does, re-apply the three hunks above.
71 changes: 71 additions & 0 deletions ADVISOR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Advisor

<!-- cspell:ignore maxhit deadend headlessly -->

A native, fully-offline **Advisor** tab for this PoB2 fork. It reads the live,
already-calculated build (`build.calcsTab.mainOutput`, `build.skillsTab`,
`build.spec`) and surfaces rule-based improvement findings. **No LLM, no network** —
pure deterministic Lua over PoB's own engine output, so every number it cites is the
same value PoB shows.

## Architecture

| File | Role |
|---|---|
| `src/Modules/Advisor.lua` | Pure logic: `Advisor.analyze(build) -> { finding, ... }`. No UI refs; unit-testable headlessly. |
| `src/Classes/AdvisorTab.lua` | UI only: renders findings grouped by category with filters, click-to-jump, and live refresh. |
| `spec/Modules/Advisor_spec.lua` | Unit tests for every check against crafted `mainOutput` / skill / tree fixtures. |
| `spec/System/TestAdvisorSmoke_spec.lua` | Headless smoke test over a real engine-calculated build. |

## Finding shape

```lua
{
id = "res.fire.uncapped", -- stable id
severity = "high", -- "high" | "med" | "low" | "info"
category = "Survivability", -- Survivability | Skills | Attributes | Spirit | Tree
title = "Fire Resistance is below cap",
detail = "Fire Res 58% (-17% under the cap).", -- cites exact numbers
fix = "Add ~17% Fire Resistance on gear, runes, or the tree.",
jump = { mode = "ITEMS" }, -- optional: tab the finding navigates to
}
```

Findings are sorted by severity (`high=3, med=2, low=1, info=0`) then category then title.

## Check catalogue

| id | flags | severity |
|---|---|---|
| `res.<elem>.uncapped` | Fire/Cold/Lightning/Chaos resistance below cap (chaos ignored under Chaos Inoculation) | high (elem), med (chaos) |
| `res.<elem>.surplus` | Elemental resistance wasted over the cap | info |
| `ehp.low` | Effective HP below a level-scaled heuristic floor | med |
| `recovery.none` | No meaningful life/ES regen or leech | med |
| `mitigation.none` | No meaningful armour / evasion / block / suppression layer | med |
| `maxhit.weakest` | Weakest damage type by maximum hit taken | info |
| `support.inapplicable.<name>` | Support gem whose skill-type tags do not match the active skill | high |
| `support.empty.<skill>` | Socket group with an active skill but no supports | info |
| `support.duplicate.<gameId>` | Same support gem socketed twice in a group | med |
| `attr.unmet.<Attr>.<gem>` | Gem Str/Dex/Int requirement exceeds the character's attribute | high |
| `spirit.over` | Spirit over-reserved (negative unreserved) | high |
| `spirit.unused` | Large unreserved Spirit pool | info |
| `tree.adjacent.<id>` | Unallocated notable directly adjacent to the allocated tree | info |
| `tree.deadend.<id>` | Allocated travel node that reaches no notable/keystone/socket | med |
| `tree.floating` | Allocated nodes not connected to the class start | med |

## Adding a new check

1. Append a `t_insert(Advisor.checks, function(build, out, findings) ... end)` block in
`src/Modules/Advisor.lua`. Each check appends 0..n findings using the shape above.
Read engine values from `out` (= `build.calcsTab.mainOutput`); guard every access
(`out.Foo and ...`) — a build mid-edit may be missing keys, and a crashing check must
never break the tab (`Advisor.analyze` pcall-wraps each check).
2. Add a unit test in `spec/Modules/Advisor_spec.lua` using `healthyOutput(overrides)` (or
a crafted skill/tree fixture) and `byId(findings, "<your.id>")`.
3. Document the new id in the catalogue table above.

No UI change is needed — `AdvisorTab` renders whatever `Advisor.analyze` returns.

## Running

See `ADVISOR-DEV.md` for the headless/test commands and the upstream rebase workflow.
Loading
Loading