From 375349e56e401f149d5cee5cfd3391c87fcd72eb Mon Sep 17 00:00:00 2001 From: matt Date: Thu, 4 Sep 2025 19:28:48 -0700 Subject: [PATCH] =?UTF-8?q?release:=202.2.6=20=E2=80=93=20refresh=20bracke?= =?UTF-8?q?t=20list=20JSONs;=20finalize=20brackets=20compliance=20docs=20a?= =?UTF-8?q?nd=20UI=20polish?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 21 + README.md | Bin 50950 -> 52654 bytes code/deck_builder/brackets_compliance.py | 38 +- code/tests/test_brackets_compliance.py | 30 ++ code/web/routes/build.py | 10 +- .../templates/build/_compliance_panel.html | 27 +- config/brackets.yml | 7 + config/card_lists/extra_turns.json | 57 ++- config/card_lists/game_changers.json | 69 ++- config/card_lists/mass_land_denial.json | 80 +++- config/card_lists/tutors_nonland.json | 411 +++++++++++++++++- 11 files changed, 734 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 771b1d9..c7fddb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,8 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning ## [Unreleased] +## [2.2.6] - 2025-09-04 + ### Added - Bracket policy enforcement: global pool-level prune for disallowed categories when limits are 0 (e.g., Game Changers in Brackets 1–2). Applies to both Web and headless runs. - Inline enforcement UI: violations surface before the summary; Continue/Rerun disabled until you replace or remove flagged cards. Alternatives are role-consistent and exclude commander/locked/in-deck cards. @@ -20,10 +22,29 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning ### Changed - Spells and creatures phases apply bracket-aware pre-filters to reduce violations proactively. - Compliance detection for Game Changers falls back to in-code constants when `config/card_lists/game_changers.json` is empty. +- Data refresh: updated static lists used by bracket compliance/enforcement with current card names and metadata: + - `config/card_lists/extra_turns.json` + - `config/card_lists/game_changers.json` + - `config/card_lists/mass_land_denial.json` + - `config/card_lists/tutors_nonland.json` + Each list includes `list_version: "manual-2025-09-04"` and `generated_at`. ### Fixed - Summary/export mismatch in headless JSON runs where disallowed cards could be pruned from exports but appear in summaries; global prune ensures consistent state across phases and reports. +### Notes +- These lists underpin the bracket enforcement feature introduced in 2.2.5; shipping them as a follow-up release ensures consistent results across Web and headless runs. + +## [2.2.5] - 2025-09-03 + +### Added +- Bracket WARN thresholds: `config/brackets.yml` supports optional `_warn` keys (e.g., `tutors_nonland_warn`, `extra_turns_warn`). Compliance now returns PASS/WARN/FAIL; low brackets (1–2) conservatively WARN on presence of tutors/extra_turns when thresholds aren’t provided. +- Web UI compliance polish: the panel auto-opens on non-compliance (WARN/FAIL) and shows a colored overall status chip (green/WARN amber/red). WARN items now render as tiles with a subtle amber style and a WARN badge; tiles and enforcement actions remain FAIL-only. +- Tests: added coverage to ensure WARN thresholds from YAML are applied and that fallback WARN behavior appears for low brackets. + +### Changed +- Web: flagged metadata now includes WARN categories with a `severity` field to support softer UI rendering for advisory cases. + ## [2.2.4] - 2025-09-02 ### Added diff --git a/README.md b/README.md index 41601ba5945bee78a295a1d615878d356f29be11..7e39eae179e79efc0fea4c5599e8cc03489279e5 100644 GIT binary patch delta 1323 zcmaJ>O>0w85S?@ zgbQ)uLR?69B6RH{3xA3`SN;OenVZ|BqEa5c_s-0jnR90H`fcvZhuqgIB^wqBwzgL| z@<1NRnjKxp+b4S`R=W~NM;a1|!6}tQVp+!5#al=Eczq^2dN#Pn_?GQVshD*ngsl%- z3=jGR`0BVt_;ad)!TWMDs+=dzW9TwWgL`jc5I0Lipfjtdl1I9Bx#I+eAg!U2PEFygYT1MeKjmqC))YZq;`H` z4HP5eT;l0zaTp&9m1CYkXlsLa#Y=}7 str: return s.casefold() -def _status_for(count: int, limit: Optional[int]) -> str: +def _status_for(count: int, limit: Optional[int], warn: Optional[int] = None) -> str: + # Unlimited hard limit -> always PASS (no WARN semantics without a cap) if limit is None: return "PASS" - return "PASS" if count <= int(limit) else "FAIL" + if count > int(limit): + return "FAIL" + # Soft guidance: if warn threshold provided and met, surface WARN + try: + if warn is not None and int(warn) > 0 and count >= int(warn): + return "WARN" + except Exception: + pass + return "PASS" def evaluate_deck( @@ -155,7 +164,13 @@ def evaluate_deck( flagged_names_disp = sorted({deck_canon_to_display.get(cn, cn) for cn in flagged_set}) c = len(flagged_set) lim = limits.get(key) - status = _status_for(c, lim) + # Optional warn thresholds live alongside limits as "_warn" + try: + warn_key = f"{key}_warn" + warn_val = limits.get(warn_key) + except Exception: + warn_val = None + status = _status_for(c, lim, warn=warn_val) cat: CategoryFinding = { "count": c, "limit": lim, @@ -166,12 +181,27 @@ def evaluate_deck( categories[key] = cat if status == "FAIL": messages.append(f"{key.replace('_',' ').title()}: {c} exceeds limit {lim}") + elif status == "WARN": + try: + if warn_val is not None: + messages.append(f"{key.replace('_',' ').title()}: {c} present (discouraged for this bracket)") + except Exception: + pass + # Conservative fallback: for low brackets (levels 1–2), tutors/extra-turns should WARN when present + # even if a warn threshold was not provided in YAML. + if status == "PASS" and level in (1, 2) and key in ("tutors_nonland", "extra_turns"): + try: + if (warn_val is None) and (lim is not None) and c > 0 and c <= int(lim): + categories[key]["status"] = "WARN" + messages.append(f"{key.replace('_',' ').title()}: {c} present (discouraged for this bracket)") + except Exception: + pass # Two-card combos detection combos = detect_combos(deck_cards.keys(), combos_path=combos_path) cheap_early_pairs = [p for p in combos if p.cheap_early] c_limit = limits.get("two_card_combos") - combos_status = _status_for(len(cheap_early_pairs), c_limit) + combos_status = _status_for(len(cheap_early_pairs), c_limit, warn=None) categories["two_card_combos"] = { "count": len(cheap_early_pairs), "limit": c_limit, diff --git a/code/tests/test_brackets_compliance.py b/code/tests/test_brackets_compliance.py index f5c7a34..1108daf 100644 --- a/code/tests/test_brackets_compliance.py +++ b/code/tests/test_brackets_compliance.py @@ -51,3 +51,33 @@ def test_two_card_combination_detection_respects_cheap_early(): rep2 = evaluate_deck(deck, commander_name=None, bracket="optimized") assert rep2["categories"]["two_card_combos"]["limit"] is None assert rep2["overall"] == "PASS" + + +def test_warn_thresholds_in_yaml_are_applied(): + # Exhibition: tutors_nonland_warn=1 -> WARN when a single tutor present (hard limit 3) + deck1 = { + # Use a non-"Game Changer" tutor to avoid hard fail in Exhibition + "Solve the Equation": _mk_card(["Bracket:TutorNonland"]), + "Cultivate": _mk_card([]), + } + rep1 = evaluate_deck(deck1, commander_name=None, bracket="exhibition") + assert rep1["level"] == 1 + assert rep1["categories"]["tutors_nonland"]["status"] == "WARN" + assert rep1["overall"] == "WARN" + + # Core: extra_turns_warn=1 -> WARN at 1, PASS at 0, FAIL above hard limit 3 + deck2 = { + "Time Warp": _mk_card(["Bracket:ExtraTurn"]), + "Explore": _mk_card([]), + } + rep2 = evaluate_deck(deck2, commander_name=None, bracket="core") + assert rep2["level"] == 2 + assert rep2["categories"]["extra_turns"]["limit"] == 3 + assert rep2["categories"]["extra_turns"]["status"] in {"WARN", "PASS"} + # With two extra turns, still <= limit, but should at least WARN + deck3 = { + "Time Warp": _mk_card(["Bracket:ExtraTurn"]), + "Temporal Manipulation": _mk_card(["Bracket:ExtraTurn"]), + } + rep3 = evaluate_deck(deck3, commander_name=None, bracket="core") + assert rep3["categories"]["extra_turns"]["status"] == "WARN" diff --git a/code/web/routes/build.py b/code/web/routes/build.py index 399158e..d3f8146 100644 --- a/code/web/routes/build.py +++ b/code/web/routes/build.py @@ -2273,12 +2273,12 @@ async def build_compliance_panel(request: Request) -> HTMLResponse: seen_lower: set[str] = set() for key, cat in cats.items(): try: - lim = cat.get('limit') - cnt = int(cat.get('count', 0) or 0) - if lim is None or cnt <= int(lim): + status = str(cat.get('status') or '').upper() + # Only surface tiles for WARN and FAIL + if status not in {"WARN", "FAIL"}: continue # For two-card combos, split pairs into individual cards and skip commander - if key == 'two_card_combos': + if key == 'two_card_combos' and status == 'FAIL': # Prefer the structured combos list to ensure we only expand counted pairs pairs = [] try: @@ -2316,6 +2316,7 @@ async def build_compliance_panel(request: Request) -> HTMLResponse: 'category': labels.get(key, key.replace('_',' ').title()), 'role': role, 'owned': (nm_l in owned_lower), + 'severity': status, }) continue # Default handling for list/tag categories @@ -2332,6 +2333,7 @@ async def build_compliance_panel(request: Request) -> HTMLResponse: 'category': labels.get(key, key.replace('_',' ').title()), 'role': role, 'owned': (nm_l in owned_lower), + 'severity': status, }) except Exception: continue diff --git a/code/web/templates/build/_compliance_panel.html b/code/web/templates/build/_compliance_panel.html index 1ef1db9..96890d7 100644 --- a/code/web/templates/build/_compliance_panel.html +++ b/code/web/templates/build/_compliance_panel.html @@ -1,7 +1,18 @@ {% if compliance %} -
+{% set non_compliant = compliance.overall is defined and (compliance.overall|string|lower != 'pass') %} +
Bracket compliance -
Overall: {{ compliance.overall }} (Bracket: {{ compliance.bracket|title }}{{ ' #' ~ compliance.level if compliance.level is defined }})
+ {% set ov = compliance.overall|string|lower %} +
Overall: + {% if ov == 'fail' %} + FAIL + {% elif ov == 'warn' %} + WARN + {% else %} + PASS + {% endif %} + (Bracket: {{ compliance.bracket|title }}{{ ' #' ~ compliance.level if compliance.level is defined }}) +
{% if compliance.messages and compliance.messages|length > 0 %}