Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
*info_*.png
*info_log_parsed.txt
.DS_Store
.agents
.codex
.coverage
.idea/
.pytest_cache/
.venv
.vs/
/skills-lock.json
/tts/saapi
/tts/x64
__pycache__/
Expand Down
6 changes: 3 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ default_language_version:
minimum_prek_version: '0.3.0'
repos:
- repo: https://github.com/astral-sh/uv-pre-commit
rev: 0.11.16
rev: 0.11.21
Comment thread
chrisoro marked this conversation as resolved.
hooks:
- id: uv-lock
priority: &group1 4294967294
Expand Down Expand Up @@ -32,7 +32,7 @@ repos:
priority: *group1
files: \.(cpp|h)$
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
rev: v6.0.0
hooks:
- id: check-added-large-files
priority: &read-only 4294967295
Expand Down Expand Up @@ -62,7 +62,7 @@ repos:
priority: *group1
- id: trailing-whitespace
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.15
rev: v0.15.17
hooks:
- id: ruff-format
priority: *group1
Expand Down
124 changes: 124 additions & 0 deletions .scratch/typed-paragon-payload/PRD.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Typed Paragon Payload Profile Schema

Status: complete

## Problem Statement

Paragon payload data in a profile is currently typed as an open-ended mapping or list of mappings. That lets malformed Paragon overlay data pass profile validation and pushes schema assumptions into importer and overlay code. A profile can currently contain arbitrary Paragon data, even though the application expects a specific shape: one Paragon payload with a build name, optional metadata, and one or more Paragon progression steps made of 21x21 board node grids.

This weak typing makes the Paragon overlay fragile. Importers can write incomplete or malformed payloads, profile loading can accept data the overlay cannot render, and future changes do not have a clear schema contract.

## Solution

Make the profile's `Paragon` section a first-class Pydantic model instead of a free-form object.

The profile schema should represent exactly one stored Paragon payload per profile. That payload can contain multiple Paragon progression steps for the same imported build. Legacy single-payload list shapes should still be accepted as migration tolerance, but profile export should write one payload object, not a list.

Paragon import builders should return the typed Paragon payload model so malformed importer output fails before profile save. Profile loading should keep typed Paragon payloads through the filter layer. The Paragon overlay should consume typed model attributes instead of raw dictionary keys.

## User Stories

1. As a profile author, I want invalid Paragon data to fail validation, so that broken profile files do not silently reach the overlay.
1. As a profile author, I want clear validation errors for malformed Paragon data, so that I can fix profile YAML manually when needed.
1. As a profile author, I want a profile to contain at most one Paragon payload, so that the profile schema matches the intended domain model.
1. As a profile author with older data, I want a legacy single-item `Paragon` list to keep loading, so that existing profiles do not break unnecessarily.
1. As a profile author with bad older data, I want a multi-item `Paragon` list to be rejected, so that ambiguous multiple-payload profiles are not treated as supported.
1. As a profile author, I want profile export to write `Paragon` as one object, so that new files follow the canonical schema.
1. As a profile author, I want `ParagonBoardsList` to support multiple Paragon progression steps, so that leveling or progression states remain available.
1. As a profile author with older data, I want a direct board list in `ParagonBoardsList` to normalize into one progression step, so that simple old payloads still load.
1. As a profile author, I want an empty `ParagonBoardsList` to fail validation, so that a stored Paragon payload always contains renderable board data.
1. As a profile author, I want each Paragon board to require a name, so that the overlay can present readable board choices.
1. As a profile author, I want each Paragon board to require exactly 441 node values, so that every board represents the expected 21x21 grid.
1. As a profile author, I want all-false node grids to be valid, so that a board can exist before selected nodes are present.
1. As a profile author, I want board rotation to accept common input forms and normalize to the current stored string format, so that profile YAML stays compatible.
1. As a profile author, I want only supported rotations to validate, so that the overlay never receives impossible board angles.
1. As a profile author, I want unknown Paragon payload keys to be rejected, so that typos and unsupported metadata do not become accidental schema.
1. As a profile author, I want unknown Paragon board keys to be rejected, so that source-specific data must be deliberately modeled.
1. As a Maxroll importer user, I want imported board IDs and glyph IDs to remain supported, so that useful source metadata is not lost.
1. As a Mobalytics importer user, I want imported Paragon data to validate before saving, so that bad source extraction fails early.
1. As a D4Builds importer user, I want reconstructed Paragon data to validate before saving, so that DOM parsing mistakes are caught.
1. As a Paragon overlay user, I want the overlay to read typed Paragon payloads, so that runtime rendering no longer depends on arbitrary dictionaries.
1. As a maintainer, I want the filter layer to expose typed Paragon payloads, so that downstream code has a clear contract.
1. As a maintainer, I want tests around the profile schema, so that future Paragon changes cannot weaken validation accidentally.
1. As a maintainer, I want tests around serialization aliases, so that generated YAML remains compatible with existing profile conventions.
1. As a maintainer, I want legacy normalization covered by tests, so that compatibility behavior is intentional and documented.
1. As a maintainer, I want importer return types to be precise, so that `dict[str, Any]` does not keep spreading through Paragon code.
1. As a maintainer, I want profile model errors to identify the faulty Paragon field, so that support/debugging is faster.
1. As a maintainer, I want the existing overlay UI behavior preserved, so that this schema fix does not become a UI rewrite.
1. As a maintainer, I want no new concept of alternative Paragon builds inside one profile, so that the domain model stays simple.
1. As a maintainer, I want optional payload metadata to remain optional, so that stripped or manually authored profiles can still be valid.
1. As a maintainer, I want generated payload metadata to continue being written by importers, so that saved profiles still show source and generator context.

## Implementation Decisions

- Add a dedicated Paragon board model to the profile schema. It should model board name, glyph, rotation, node grid, and known optional source IDs.
- Add a dedicated Paragon payload model to the profile schema. It should model payload name, optional source metadata, and the payload's Paragon progression steps.
- The profile's `Paragon` field should become `ParagonPayloadModel | None`.
- A profile may include at most one stored Paragon payload. Multiple Paragon payloads in one profile are not part of the supported domain model.
- A Paragon payload may contain multiple Paragon progression steps. Each step is a list of Paragon boards.
- Legacy `Paragon: [payload]` input should normalize to `Paragon: payload`.
- Legacy `Paragon: []` input should normalize to no Paragon payload.
- Legacy `Paragon: [payload1, payload2]` input should fail validation with a clear error.
- Official `ParagonBoardsList` shape is a list of progression steps.
- Legacy direct board-list input in `ParagonBoardsList` should normalize to one progression step.
- Empty `ParagonBoardsList` should fail validation.
- Board `Nodes` must contain exactly 441 boolean-compatible values.
- Board `Nodes` may be all false.
- Board `Rotation` should export as the current string format: `0°`, `90°`, `180°`, or `270°`.
- Rotation input may accept integer or digit-only string forms, but validation should normalize to the canonical stored string.
- Payload metadata fields such as source URL, generated timestamp, and generator should be optional strings.
- Unknown fields should be forbidden on Paragon payloads and boards.
- Known optional Maxroll metadata such as board ID and glyph ID should stay modeled.
- Paragon import builders should return the typed Paragon payload model, not a raw dictionary.
- The filter layer should store and expose typed Paragon payload models.
- The Paragon overlay should consume typed model attributes rather than dictionary key lookups.
- Profile serialization should keep existing public YAML aliases such as `Paragon`, `ParagonBoardsList`, `Name`, `Glyph`, `Rotation`, and `Nodes`.
- Exported profiles should write the canonical one-payload shape, even when input used a legacy tolerated shape.
- No ADR is needed for this change. The decision is a schema tightening with compatibility rules, not a hard-to-reverse architectural trade-off.

## Testing Decisions

- Tests should assert external behavior: profile validation, normalized model values, serialization output, importer-builder return behavior, and overlay-facing loaded data shape.
- Add profile model tests following the existing config model test style.
- Cover valid Paragon payload construction with canonical aliases.
- Cover snake-case and alias behavior where it matters for existing model conventions.
- Cover rejection of unknown payload fields.
- Cover rejection of unknown board fields.
- Cover missing required payload name.
- Cover missing required board name.
- Cover missing nodes.
- Cover node count shorter than 441.
- Cover node count longer than 441.
- Cover all-false nodes as valid.
- Cover valid rotations for 0, 90, 180, and 270 degrees.
- Cover invalid rotations.
- Cover rotation normalization from integer and string inputs if implemented.
- Cover legacy `Paragon: []` normalization.
- Cover legacy `Paragon: [payload]` normalization.
- Cover legacy multi-payload list rejection.
- Cover canonical `ParagonBoardsList` list-of-steps input.
- Cover legacy direct board-list normalization to one progression step.
- Cover serialization with aliases so saved YAML keeps existing field names.
- Cover importer builder returning a typed Paragon payload model.
- Cover the filter layer returning typed Paragon payloads after profile load where practical with existing filter tests.
- Keep overlay tests focused at the seam where typed models are converted into overlay build rows; avoid testing tkinter rendering internals.
- Prior art exists in the current profile model tests, importer tests, and filter profile-loading tests.

## Out of Scope

- Changing Paragon overlay UI behavior.
- Changing board rendering, node layout, or rotation math.
- Supporting multiple alternative Paragon payloads inside one profile.
- Adding a migration command that rewrites profile files on disk.
- Adding new Paragon importer sources.
- Validating whether a board name, glyph name, board ID, or glyph ID exists in Diablo 4 game data.
- Changing how item, sigil, tribute, or aspect profile sections are modeled.
- Reworking profile editor UI to edit Paragon payloads manually.

## Further Notes

The domain glossary defines a profile as a user-defined loot filtering configuration for one Diablo 4 build. A profile may include at most one stored Paragon payload. A Paragon payload represents one imported Paragon build and may contain multiple Paragon progression steps.

This PRD deliberately preserves current YAML names and the current importer payload concept while replacing the permissive `dict[str, object] | list[dict[str, object]]` schema with a strict model.

Verified with focused tests: `172 passed, 6 skipped`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Type the Paragon payload schema

Status: complete

## Parent

.scratch/typed-paragon-payload/PRD.md

## What to build

Make the profile's `Paragon` section a first-class typed schema. A profile should contain at most one Paragon payload, and that payload should contain one or more Paragon progression steps made of boards with valid 21x21 node grids.

The schema should reject arbitrary payload and board keys, preserve current YAML aliases, validate rotations and node counts, and normalize the tolerated legacy shapes into the canonical one-payload format.

## Acceptance criteria

- [x] Profile validation accepts one canonical Paragon payload with one or more Paragon progression steps.
- [x] Profile validation rejects multiple Paragon payloads in one profile.
- [x] Empty `Paragon` legacy list input normalizes to no Paragon payload.
- [x] Single-item `Paragon` legacy list input normalizes to one Paragon payload.
- [x] Direct board-list `ParagonBoardsList` input normalizes to one Paragon progression step.
- [x] Empty `ParagonBoardsList` fails validation.
- [x] Boards require a non-empty name.
- [x] Boards require exactly 441 node values.
- [x] All-false node grids are valid.
- [x] Rotation accepts supported values and normalizes to the canonical stored string.
- [x] Unsupported rotation values fail validation.
- [x] Unknown payload fields fail validation.
- [x] Unknown board fields fail validation, except deliberately modeled optional source IDs.
- [x] Serialized profile output keeps existing public aliases such as `Paragon`, `ParagonBoardsList`, `Name`, `Glyph`, `Rotation`, and `Nodes`.
- [x] Focused profile model tests cover valid, invalid, normalization, and serialization behavior.

## Blocked by

None - can start immediately.

## Comments

- Implemented the typed Paragon board/payload schema, legacy normalization, alias-preserving serialization, and validation coverage.
- Verified with focused tests: `137 passed, 26 skipped`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Return typed Paragon payloads from importers

Status: complete

## Parent

.scratch/typed-paragon-payload/PRD.md

## What to build

Change Paragon importer payload construction so imported Paragon data becomes a typed Paragon payload before it is assigned to a profile. Maxroll, Mobalytics, and D4Builds imports should still save the same user-facing YAML shape, but malformed extracted payloads should fail at the importer/profile boundary instead of reaching the overlay.

## Acceptance criteria

- [x] Paragon payload construction returns the typed Paragon payload model instead of a raw mapping.
- [x] Maxroll Paragon import preserves board IDs and glyph IDs where present.
- [x] Mobalytics Paragon import still produces valid payloads from existing extracted board and node data.
- [x] D4Builds Paragon import still produces valid payloads from reconstructed board data.
- [x] Importer-generated profiles still serialize with the existing Paragon YAML aliases.
- [x] Tests cover typed payload construction and at least one importer path using the builder.
- [x] Existing importer behavior unrelated to Paragon remains unchanged.

## Blocked by

- .scratch/typed-paragon-payload/issues/01-type-paragon-payload-schema.md

## Comments

- Paragon payload construction now returns `ParagonPayloadModel`, and Maxroll/Mobalytics/D4Builds paths are covered by regression tests.
- Verified with focused tests: `137 passed, 26 skipped`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Carry typed Paragon payloads through filter and overlay

Status: complete

## Parent

.scratch/typed-paragon-payload/PRD.md

## What to build

Keep typed Paragon payloads after profile validation and pass them through profile loading into the Paragon overlay. The overlay should build its selectable Paragon build rows from model attributes rather than raw dictionary keys, while preserving current overlay behavior and user-facing labels.

## Acceptance criteria

- [x] Profile loading stores typed Paragon payloads for profiles that contain Paragon data.
- [x] The filter layer exposes typed Paragon payloads to Paragon consumers.
- [x] The Paragon overlay reads payload names, progression steps, boards, rotations, glyphs, and nodes from typed model attributes.
- [x] Existing overlay behavior is preserved, including newest progression step first.
- [x] Legacy shapes accepted by the profile model still reach the overlay as canonical typed payloads.
- [x] No tkinter rendering behavior is intentionally changed.
- [x] Tests cover the overlay-facing build-row seam using typed Paragon payloads.

## Blocked by

- .scratch/typed-paragon-payload/issues/01-type-paragon-payload-schema.md

## Comments

- The filter layer now exposes typed Paragon payloads and the overlay builds rows from model attributes.
- Verified with focused tests: `137 passed, 26 skipped`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Regression coverage for end-to-end profile behavior

Status: complete

## Parent

.scratch/typed-paragon-payload/PRD.md

## What to build

Add final regression coverage across the profile, importer, filter, and overlay seams so the typed Paragon payload contract stays intact end to end. This issue should remove or update any remaining tests or assumptions that treat Paragon data as arbitrary dictionaries or lists.

## Acceptance criteria

- [x] Tests verify a profile with canonical Paragon data can be validated, serialized, loaded, and exposed for overlay use.
- [x] Tests verify legacy tolerated Paragon shapes become canonical typed payloads before overlay use.
- [x] Tests verify invalid Paragon payloads fail before overlay use.
- [x] Tests verify importer-generated Paragon data remains compatible with profile serialization.
- [x] Any stale test expectations around `dict[str, object] | list[dict[str, object]]` are removed or updated.
- [x] Targeted tests for config models, importer payload construction, filter loading, and overlay build-row creation pass.

## Blocked by

- .scratch/typed-paragon-payload/issues/01-type-paragon-payload-schema.md
- .scratch/typed-paragon-payload/issues/02-return-typed-paragon-payloads-from-importers.md
- .scratch/typed-paragon-payload/issues/03-carry-typed-paragon-payloads-through-filter-and-overlay.md

## Comments

- Added regression coverage across schema, serialization, importer, filter, and overlay seams.
- Verified with focused tests: `137 passed, 26 skipped`.
14 changes: 14 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,17 @@ All UI coordinates in `src/config/data.py` are defined at **3840×2160** (UHD) a
- User data directory: `~/.d4lf/` (profiles, params.ini, logs).
- Version is defined in `src/__init__.py` as `__version__`.
- Existing comments should be kept in place unless the code they relate to is removed

## Agent skills

### Issue tracker

Issues and PRDs are tracked as local markdown files under `.scratch/`. See `docs/agents/issue-tracker.md`.

### Triage labels

Triage labels use the default canonical strings. See `docs/agents/triage-labels.md`.

### Domain docs

This repo uses a single-context domain docs layout. See `docs/agents/domain.md`.
15 changes: 15 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Domain Context

## Glossary

### Profile

A user-defined loot filtering configuration for one Diablo 4 build. A profile may include at most one stored Paragon payload for the Paragon overlay.

### Paragon payload

The stored Paragon overlay data attached to a profile. It represents one imported Paragon build, not a collection of alternative builds. A payload may contain multiple progression steps for that build.

### Paragon progression step

One board-state snapshot within a Paragon payload. Each step contains the boards and active nodes for a point in the imported build's progression.
Loading