Skip to content

Commit 485b54d

Browse files
committed
Implement typed Paragon payload schema and validation
1 parent 241588a commit 485b54d

26 files changed

Lines changed: 929 additions & 138 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,14 @@
88
*info_*.png
99
*info_log_parsed.txt
1010
.DS_Store
11+
.agents
1112
.codex
1213
.coverage
1314
.idea/
1415
.pytest_cache/
1516
.venv
1617
.vs/
18+
/skills-lock.json
1719
/tts/saapi
1820
/tts/x64
1921
__pycache__/

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ default_language_version:
44
minimum_prek_version: '0.3.0'
55
repos:
66
- repo: https://github.com/astral-sh/uv-pre-commit
7-
rev: 0.11.16
7+
rev: 0.11.21
88
hooks:
99
- id: uv-lock
1010
priority: &group1 4294967294
@@ -32,7 +32,7 @@ repos:
3232
priority: *group1
3333
files: \.(cpp|h)$
3434
- repo: https://github.com/pre-commit/pre-commit-hooks
35-
rev: v5.0.0
35+
rev: v6.0.0
3636
hooks:
3737
- id: check-added-large-files
3838
priority: &read-only 4294967295
@@ -62,7 +62,7 @@ repos:
6262
priority: *group1
6363
- id: trailing-whitespace
6464
- repo: https://github.com/astral-sh/ruff-pre-commit
65-
rev: v0.15.15
65+
rev: v0.15.17
6666
hooks:
6767
- id: ruff-format
6868
priority: *group1
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Typed Paragon Payload Profile Schema
2+
3+
Status: complete
4+
5+
## Problem Statement
6+
7+
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.
8+
9+
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.
10+
11+
## Solution
12+
13+
Make the profile's `Paragon` section a first-class Pydantic model instead of a free-form object.
14+
15+
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.
16+
17+
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.
18+
19+
## User Stories
20+
21+
1. As a profile author, I want invalid Paragon data to fail validation, so that broken profile files do not silently reach the overlay.
22+
1. As a profile author, I want clear validation errors for malformed Paragon data, so that I can fix profile YAML manually when needed.
23+
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.
24+
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.
25+
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.
26+
1. As a profile author, I want profile export to write `Paragon` as one object, so that new files follow the canonical schema.
27+
1. As a profile author, I want `ParagonBoardsList` to support multiple Paragon progression steps, so that leveling or progression states remain available.
28+
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.
29+
1. As a profile author, I want an empty `ParagonBoardsList` to fail validation, so that a stored Paragon payload always contains renderable board data.
30+
1. As a profile author, I want each Paragon board to require a name, so that the overlay can present readable board choices.
31+
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.
32+
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.
33+
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.
34+
1. As a profile author, I want only supported rotations to validate, so that the overlay never receives impossible board angles.
35+
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.
36+
1. As a profile author, I want unknown Paragon board keys to be rejected, so that source-specific data must be deliberately modeled.
37+
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.
38+
1. As a Mobalytics importer user, I want imported Paragon data to validate before saving, so that bad source extraction fails early.
39+
1. As a D4Builds importer user, I want reconstructed Paragon data to validate before saving, so that DOM parsing mistakes are caught.
40+
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.
41+
1. As a maintainer, I want the filter layer to expose typed Paragon payloads, so that downstream code has a clear contract.
42+
1. As a maintainer, I want tests around the profile schema, so that future Paragon changes cannot weaken validation accidentally.
43+
1. As a maintainer, I want tests around serialization aliases, so that generated YAML remains compatible with existing profile conventions.
44+
1. As a maintainer, I want legacy normalization covered by tests, so that compatibility behavior is intentional and documented.
45+
1. As a maintainer, I want importer return types to be precise, so that `dict[str, Any]` does not keep spreading through Paragon code.
46+
1. As a maintainer, I want profile model errors to identify the faulty Paragon field, so that support/debugging is faster.
47+
1. As a maintainer, I want the existing overlay UI behavior preserved, so that this schema fix does not become a UI rewrite.
48+
1. As a maintainer, I want no new concept of alternative Paragon builds inside one profile, so that the domain model stays simple.
49+
1. As a maintainer, I want optional payload metadata to remain optional, so that stripped or manually authored profiles can still be valid.
50+
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.
51+
52+
## Implementation Decisions
53+
54+
- Add a dedicated Paragon board model to the profile schema. It should model board name, glyph, rotation, node grid, and known optional source IDs.
55+
- 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.
56+
- The profile's `Paragon` field should become `ParagonPayloadModel | None`.
57+
- A profile may include at most one stored Paragon payload. Multiple Paragon payloads in one profile are not part of the supported domain model.
58+
- A Paragon payload may contain multiple Paragon progression steps. Each step is a list of Paragon boards.
59+
- Legacy `Paragon: [payload]` input should normalize to `Paragon: payload`.
60+
- Legacy `Paragon: []` input should normalize to no Paragon payload.
61+
- Legacy `Paragon: [payload1, payload2]` input should fail validation with a clear error.
62+
- Official `ParagonBoardsList` shape is a list of progression steps.
63+
- Legacy direct board-list input in `ParagonBoardsList` should normalize to one progression step.
64+
- Empty `ParagonBoardsList` should fail validation.
65+
- Board `Nodes` must contain exactly 441 boolean-compatible values.
66+
- Board `Nodes` may be all false.
67+
- Board `Rotation` should export as the current string format: ``, `90°`, `180°`, or `270°`.
68+
- Rotation input may accept integer or digit-only string forms, but validation should normalize to the canonical stored string.
69+
- Payload metadata fields such as source URL, generated timestamp, and generator should be optional strings.
70+
- Unknown fields should be forbidden on Paragon payloads and boards.
71+
- Known optional Maxroll metadata such as board ID and glyph ID should stay modeled.
72+
- Paragon import builders should return the typed Paragon payload model, not a raw dictionary.
73+
- The filter layer should store and expose typed Paragon payload models.
74+
- The Paragon overlay should consume typed model attributes rather than dictionary key lookups.
75+
- Profile serialization should keep existing public YAML aliases such as `Paragon`, `ParagonBoardsList`, `Name`, `Glyph`, `Rotation`, and `Nodes`.
76+
- Exported profiles should write the canonical one-payload shape, even when input used a legacy tolerated shape.
77+
- No ADR is needed for this change. The decision is a schema tightening with compatibility rules, not a hard-to-reverse architectural trade-off.
78+
79+
## Testing Decisions
80+
81+
- Tests should assert external behavior: profile validation, normalized model values, serialization output, importer-builder return behavior, and overlay-facing loaded data shape.
82+
- Add profile model tests following the existing config model test style.
83+
- Cover valid Paragon payload construction with canonical aliases.
84+
- Cover snake-case and alias behavior where it matters for existing model conventions.
85+
- Cover rejection of unknown payload fields.
86+
- Cover rejection of unknown board fields.
87+
- Cover missing required payload name.
88+
- Cover missing required board name.
89+
- Cover missing nodes.
90+
- Cover node count shorter than 441.
91+
- Cover node count longer than 441.
92+
- Cover all-false nodes as valid.
93+
- Cover valid rotations for 0, 90, 180, and 270 degrees.
94+
- Cover invalid rotations.
95+
- Cover rotation normalization from integer and string inputs if implemented.
96+
- Cover legacy `Paragon: []` normalization.
97+
- Cover legacy `Paragon: [payload]` normalization.
98+
- Cover legacy multi-payload list rejection.
99+
- Cover canonical `ParagonBoardsList` list-of-steps input.
100+
- Cover legacy direct board-list normalization to one progression step.
101+
- Cover serialization with aliases so saved YAML keeps existing field names.
102+
- Cover importer builder returning a typed Paragon payload model.
103+
- Cover the filter layer returning typed Paragon payloads after profile load where practical with existing filter tests.
104+
- Keep overlay tests focused at the seam where typed models are converted into overlay build rows; avoid testing tkinter rendering internals.
105+
- Prior art exists in the current profile model tests, importer tests, and filter profile-loading tests.
106+
107+
## Out of Scope
108+
109+
- Changing Paragon overlay UI behavior.
110+
- Changing board rendering, node layout, or rotation math.
111+
- Supporting multiple alternative Paragon payloads inside one profile.
112+
- Adding a migration command that rewrites profile files on disk.
113+
- Adding new Paragon importer sources.
114+
- Validating whether a board name, glyph name, board ID, or glyph ID exists in Diablo 4 game data.
115+
- Changing how item, sigil, tribute, or aspect profile sections are modeled.
116+
- Reworking profile editor UI to edit Paragon payloads manually.
117+
118+
## Further Notes
119+
120+
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.
121+
122+
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.
123+
124+
Verified with focused tests: `172 passed, 6 skipped`.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Type the Paragon payload schema
2+
3+
Status: complete
4+
5+
## Parent
6+
7+
.scratch/typed-paragon-payload/PRD.md
8+
9+
## What to build
10+
11+
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.
12+
13+
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.
14+
15+
## Acceptance criteria
16+
17+
- [x] Profile validation accepts one canonical Paragon payload with one or more Paragon progression steps.
18+
- [x] Profile validation rejects multiple Paragon payloads in one profile.
19+
- [x] Empty `Paragon` legacy list input normalizes to no Paragon payload.
20+
- [x] Single-item `Paragon` legacy list input normalizes to one Paragon payload.
21+
- [x] Direct board-list `ParagonBoardsList` input normalizes to one Paragon progression step.
22+
- [x] Empty `ParagonBoardsList` fails validation.
23+
- [x] Boards require a non-empty name.
24+
- [x] Boards require exactly 441 node values.
25+
- [x] All-false node grids are valid.
26+
- [x] Rotation accepts supported values and normalizes to the canonical stored string.
27+
- [x] Unsupported rotation values fail validation.
28+
- [x] Unknown payload fields fail validation.
29+
- [x] Unknown board fields fail validation, except deliberately modeled optional source IDs.
30+
- [x] Serialized profile output keeps existing public aliases such as `Paragon`, `ParagonBoardsList`, `Name`, `Glyph`, `Rotation`, and `Nodes`.
31+
- [x] Focused profile model tests cover valid, invalid, normalization, and serialization behavior.
32+
33+
## Blocked by
34+
35+
None - can start immediately.
36+
37+
## Comments
38+
39+
- Implemented the typed Paragon board/payload schema, legacy normalization, alias-preserving serialization, and validation coverage.
40+
- Verified with focused tests: `137 passed, 26 skipped`.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Return typed Paragon payloads from importers
2+
3+
Status: complete
4+
5+
## Parent
6+
7+
.scratch/typed-paragon-payload/PRD.md
8+
9+
## What to build
10+
11+
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.
12+
13+
## Acceptance criteria
14+
15+
- [x] Paragon payload construction returns the typed Paragon payload model instead of a raw mapping.
16+
- [x] Maxroll Paragon import preserves board IDs and glyph IDs where present.
17+
- [x] Mobalytics Paragon import still produces valid payloads from existing extracted board and node data.
18+
- [x] D4Builds Paragon import still produces valid payloads from reconstructed board data.
19+
- [x] Importer-generated profiles still serialize with the existing Paragon YAML aliases.
20+
- [x] Tests cover typed payload construction and at least one importer path using the builder.
21+
- [x] Existing importer behavior unrelated to Paragon remains unchanged.
22+
23+
## Blocked by
24+
25+
- .scratch/typed-paragon-payload/issues/01-type-paragon-payload-schema.md
26+
27+
## Comments
28+
29+
- Paragon payload construction now returns `ParagonPayloadModel`, and Maxroll/Mobalytics/D4Builds paths are covered by regression tests.
30+
- Verified with focused tests: `137 passed, 26 skipped`.
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Carry typed Paragon payloads through filter and overlay
2+
3+
Status: complete
4+
5+
## Parent
6+
7+
.scratch/typed-paragon-payload/PRD.md
8+
9+
## What to build
10+
11+
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.
12+
13+
## Acceptance criteria
14+
15+
- [x] Profile loading stores typed Paragon payloads for profiles that contain Paragon data.
16+
- [x] The filter layer exposes typed Paragon payloads to Paragon consumers.
17+
- [x] The Paragon overlay reads payload names, progression steps, boards, rotations, glyphs, and nodes from typed model attributes.
18+
- [x] Existing overlay behavior is preserved, including newest progression step first.
19+
- [x] Legacy shapes accepted by the profile model still reach the overlay as canonical typed payloads.
20+
- [x] No tkinter rendering behavior is intentionally changed.
21+
- [x] Tests cover the overlay-facing build-row seam using typed Paragon payloads.
22+
23+
## Blocked by
24+
25+
- .scratch/typed-paragon-payload/issues/01-type-paragon-payload-schema.md
26+
27+
## Comments
28+
29+
- The filter layer now exposes typed Paragon payloads and the overlay builds rows from model attributes.
30+
- Verified with focused tests: `137 passed, 26 skipped`.
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Regression coverage for end-to-end profile behavior
2+
3+
Status: complete
4+
5+
## Parent
6+
7+
.scratch/typed-paragon-payload/PRD.md
8+
9+
## What to build
10+
11+
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.
12+
13+
## Acceptance criteria
14+
15+
- [x] Tests verify a profile with canonical Paragon data can be validated, serialized, loaded, and exposed for overlay use.
16+
- [x] Tests verify legacy tolerated Paragon shapes become canonical typed payloads before overlay use.
17+
- [x] Tests verify invalid Paragon payloads fail before overlay use.
18+
- [x] Tests verify importer-generated Paragon data remains compatible with profile serialization.
19+
- [x] Any stale test expectations around `dict[str, object] | list[dict[str, object]]` are removed or updated.
20+
- [x] Targeted tests for config models, importer payload construction, filter loading, and overlay build-row creation pass.
21+
22+
## Blocked by
23+
24+
- .scratch/typed-paragon-payload/issues/01-type-paragon-payload-schema.md
25+
- .scratch/typed-paragon-payload/issues/02-return-typed-paragon-payloads-from-importers.md
26+
- .scratch/typed-paragon-payload/issues/03-carry-typed-paragon-payloads-through-filter-and-overlay.md
27+
28+
## Comments
29+
30+
- Added regression coverage across schema, serialization, importer, filter, and overlay seams.
31+
- Verified with focused tests: `137 passed, 26 skipped`.

AGENTS.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,3 +221,17 @@ All UI coordinates in `src/config/data.py` are defined at **3840×2160** (UHD) a
221221
- User data directory: `~/.d4lf/` (profiles, params.ini, logs).
222222
- Version is defined in `src/__init__.py` as `__version__`.
223223
- Existing comments should be kept in place unless the code they relate to is removed
224+
225+
## Agent skills
226+
227+
### Issue tracker
228+
229+
Issues and PRDs are tracked as local markdown files under `.scratch/`. See `docs/agents/issue-tracker.md`.
230+
231+
### Triage labels
232+
233+
Triage labels use the default canonical strings. See `docs/agents/triage-labels.md`.
234+
235+
### Domain docs
236+
237+
This repo uses a single-context domain docs layout. See `docs/agents/domain.md`.

CONTEXT.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Domain Context
2+
3+
## Glossary
4+
5+
### Profile
6+
7+
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.
8+
9+
### Paragon payload
10+
11+
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.
12+
13+
### Paragon progression step
14+
15+
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.

0 commit comments

Comments
 (0)