Skip to content

feat(tactics): Tactics Lab, phases, superiority engine, personnel panel (T-100 epic) - #19

Merged
BrandanBurgess merged 101 commits into
mainfrom
integration
Aug 9, 2026
Merged

feat(tactics): Tactics Lab, phases, superiority engine, personnel panel (T-100 epic)#19
BrandanBurgess merged 101 commits into
mainfrom
integration

Conversation

@BrandanBurgess

Copy link
Copy Markdown
Owner

Completes Epic T-100 (Tactics Lab) per docs/source/06_Tactical_Depth_Spec.md. The Formations page stops being a shape browser and becomes a tactics lab.

What a coach can now do

Pick a base shape and watch it morph into what it actually looks like with the ball (3-2-5, 2-3-5, box midfield) and without it (4-4-2 mid block, 5-4-1 low block). Drop an opponent shape on top and read where they are spare, where they are short, and which route connects them. Play a named rotation and see the shape reorganise, with its cost given equal weight to its benefit. Assign real roster players into slots, pick an archetype for each, and get told which combinations balance.

Every number on screen is computed from coordinates, not authored.

Tickets

Ticket What
T-101 Schema, migration 0006: eight new tables, rondo_zones L/R corridor data migration
T-102 Seeds: 42 position archetypes across ten slot families, 19 combinations, 17 unit balance rules
T-103 Seeds: 33 formation phases, 14 rotation systems, 36 rondo zones, 15 matchups, 10 reference systems
T-104 Superiority engine: mirrorOpponent, pointInPolygon/Circle, countZone, findFreeMen, gridOccupancy, classifyRestDefence, buildRead
T-105 Phase morph playback bound by slot, opponent token layer, reusing the existing animation player
T-106 Formations page rebuild: phase segment, opposition toggle, live rondo counts, rotation player, grid overlay
T-107 Personnel panel: slot assignment, archetype picker, ranked suggestions, live unit balance, footedness notes
T-108 API: 9 routes, 403 per coach-only route
T-110 Slot-to-unit crosswalk and the coach-only balance endpoint
T-111 Migration 0007: delete the orphan counterpress rondo zone row
T-112 Counterpress ring, restoring the Rondo Map to six zones
T-109 Epic hardening, port collision fix, demo path extension, full DoD audit

Definition of done (doc 06 section 6)

  • make verify green: lint, typecheck, pytest, vitest, e2e at both viewports, em-dash scan, seed validator
  • No em dash in any seed file, blurb, coaching point, warning or label
  • Every new team-world query goes through app/scoped.py; cross-team read tests return nothing
  • Coach-only data returns 403 for a player token, one test per route
  • Engine unit tests written before implementation: mirror involution across the full integer grid, point-in-polygon boundary cases, recompute benchmark (median 0.018ms against a 2ms budget)
  • Every seeded row has source_ref and content_version
  • Every rotation and combination has a non-empty cost line, validator-enforced
  • Phase positions_json slot sets match their base formation exactly, validator-enforced
  • Playwright journeys at both viewports; demo path extended with a tactics-lab beat

Known gaps, tracked and deliberately not fixed here

The T-109 audit found three points where the DoD is not literally met. All are filed rather than papered over:

  • T-114: the ten reference systems have no UI surface. They are seeded and validated but unreachable, and scripts/seed.py loads only items so the file-level disclaimer never reaches the database.
  • T-115: doc 06 asks for a zone-count fixture per formation pair (15); what ships is one per formation (6). Also update_team_formation bulk-deletes slot rows with a raw query, bypassing app/scoped.py (guarded by an upstream scope.get(), and it mirrors a pre-existing idiom in roster.py).
  • T-113: the personnel panel does not persist to team_formations. The API exists; no ticket wired the UI.

Open founder question: the 5-4-1 evaluates only its back line, because its flat midfield four matches no doc 06 unit.

Infrastructure

make verify now derives a stable per-worktree port pair from cksum of the checkout path, so parallel worktrees stop silently sharing one dev server. Explicit POP_WEB_PORT / POP_API_PORT overrides still win.

🤖 Generated with Claude Code

BrandanBurgess and others added 30 commits July 16, 2026 02:39
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…T-030)

Whiteboard page (Brief step 16, PNG 01-05/14/34): toolbar, view menu
(zones + thresholds), record/replay, and save into My Patterns, all
persisted through the scoped query layer (backend/app/routers/whiteboard.py)
into the boards and saved_patterns rows (doc 03 4.2/4.3). Delete is
coach-only, enforced server-side (403 for players), and every saved
pattern is author-stamped from the caller's own membership, never a
client-supplied field.

Fixes a real reload bug: the view menu's settings popover floated above
the toolbar and, left open, could visually sit on top of a token (e.g.
the ball's default spot, or any token near the toolbar on a phone-width
board). A drag or click landing there hit the popover instead of the
token, and for a checkbox's label that silently flipped a zone toggle
the user never touched, which then persisted to the boards row on
reload. Fixed by closing the view menu when recording starts (a
deliberate mode switch) and, for phone-width boards where the popover
has no gap in the pitch it can occupy without covering some token,
pinning it to the viewport's top edge instead of floating over the
pitch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Systematic permission test suite covering every Brief section 3 table
row in both roles, API and UI: backend/tests/test_permissions.py names
each test after its table row (whiteboard, delete-a-pattern, library/
formations/identity, roster/fit-warnings, plus the additive-only and
player-token-gets-403 principles), and e2e/permissions.spec.ts sweeps a
player-role visit across all five pages asserting coach-only controls
and data are absent from the DOM, not merely hidden.

Audit result: no enforcement gaps found. Whiteboard delete, roster CRUD,
and fit_warnings exclusion were already API-enforced from their own
tickets (T-030, T-033); this suite pins that behavior explicitly against
the table rather than leaving it implicit across five separate files.

Two table rows (Suggest own playstyle, Sessions) have no API surface yet
in this codebase state, only their models: marked as named, skipped
tests with a reason, for T-041/T-042 to turn into real assertions rather
than silently omitted.

One known ambiguity is pinned, not changed, per this ticket's own
instruction: the API returns join_code to any team member while the UI
shows it to coaches only (docs/agent/STATE.md open founder question 2).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Player submits a free-text playstyle suggestion on their own roster
profile, sees it pending; coach reviews from a team-wide queue with a
gold badge and an Approve/Dismiss card; approve merges the text into
players.playstyle_note (visible both roles), dismiss clears it with no
merge. New backend/app/routers/suggestions.py enforces every direction
at the API: only a player may submit, and only against their own linked
roster row; only a coach may list the pending queue, approve, or
dismiss (403 otherwise). No new migration: playstyle_suggestions already
exists in the 0002 schema migration.

Player-row linkage (needed for "own profile") did not exist yet before
this ticket; app/routers/roster.py adds a minimal, UI-free claim: on a
player's own GET /api/roster, an unclaimed row whose name uniquely
matches their display_name becomes theirs.
BrandanBurgess and others added 29 commits August 7, 2026 15:46
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Doc 06 sections 2.4, 4 and 5.1. The playback and rendering machinery a
Formations page rebuild (T-106) sits on top of. No page consumes it yet.

Phase morph. A third builder in the board engine that produces the same
`Playback` object the declarative-spec and raw-keyframe builders already
produce, driven by the same PlayerController on the same FrameLoop clock.
One player, one timing model, one coordinate model, three builders. The
two content formats stay separate and untouched; a morph is neither of
them, it is a computed transition between two seeded position sets with
no steps, no ball waypoints and no route badges.

Binding is by SLOT. Not by array index, not by position code, not by
coordinate. Slot identity is the only thing that survives a phase change,
so it is what the animation is keyed on: the coach watches their left
back walk into midfield. Tests bind a deliberately shuffled to-phase, a
pair where two slots share a position code and one changes code entirely,
the case where every slot moves at once, and the case where a slot's
coordinates are identical in both phases and must not jitter by an ulp on
any of 120 sampled frames.

Opponent layer. Opponents are seeded in their own frame and crossed into
ours by mirrorOpponent from the T-104 superiority engine and by nothing
else. There is one mirror in the codebase and this calls it. Shape-level
round trip is asserted over the full 101 by 101 integer grid. Their
tokens render as side "away", the colour the board already defines for
recorded opponents. Token ids are namespaced so both elevens can carry a
slot called "LB" and still be two tokens on one board.

Landscape model coords throughout; orientation appears nowhere in either
module, and a portrait test proves the consequence rather than asserting
the absence. 600ms, deterministic, no clock read and no randomness in the
logic.

PatternPreviewBoard is extended, not forked: one new optional prop,
showRestart, defaulting to true so every existing caller is unchanged. A
phase morph is a transition into a state the coach then reads, not
something to replay.

Benchmarked per doc 06 section 4 rather than assumed. Build median
0.008ms, a full second of 60fps playback with 22 tokens median 0.051ms,
single frame sample 0.0005ms.

Verify: POP_WEB_PORT=6273 POP_API_PORT=9100 make verify, exit 0.
213 vitest, 277 pytest, 100 e2e both viewports, em-dash scan clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Doc 06 sections 5.1, 5.2 and 5.4, on top of T-032's board-first shell.
Board, floating meta bar and page-level swipe-up sheet are unchanged, and
so are the keystone pulse, the keycard, the details panel and the
searchable formation browser. What is new is the meta bar's five controls
and what they put on the same board.

- Phase segment (Base / With the ball / Without the ball / Rest defence),
  morphing over 600ms through T-105's morphToPhase: one Playback, one
  clock, destination tokens. Variant chips reach the second and third
  seeded shape of a phase. A caption strip under the board names the
  resulting shape and its trigger.
- Opposition toggle with an opponent formation picker and an opponent
  phase picker defaulted to their out-of-possession variant. Their shape
  is passed in their own frame as seeded and mirrored once, inside the
  board engine.
- Live rondo counts from T-104's countZone. A ratio on screen is either
  COMPUTED from the two shapes on the pitch (verdict-coloured,
  data-source="computed") or the SEEDED no-opposition fallback (muted,
  dashed, data-source="seeded"). The ratio is out of the zone's display
  name entirely so the two can never be confused.
- Rotations list filtered to the formation, playing each one on the board
  and opening a card whose risk line has equal visual weight to the gain:
  same grid track, same padding, same type scale, same colour, asserted
  in the journey.
- Positional grid overlay, off by default, drawing the 5 by 5 grid and
  tinting any band gridOccupancy raises a check about. Breach copy is a
  question, never an error.

Phone portrait: the meta bar collapses to an icon row and every control
opens a bottom sheet; count chips shrink to the ratio and the read moves
into the tapped zone card. The bar stops floating on phone, because six
controls over a 7:10 board sit on top of the striker.

The counterpress ring is no longer drawn. Doc 06 section 5.1 renders it
only when a ball is placed, there is no ball on this page, and
formation_phases has no ball column, so its seeded polygon (a bound on
half the pitch) would have produced a meaningless ratio.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… ring

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ball fallback

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…112)

Doc 06 section 0 approved a six zone Rondo Map on all six formations.
T-106 shipped five: it stopped drawing counterpress_ring because section
5.1 gates the ring on "a ball is placed or a phase with a defined ball
position is active", formation_phases has no ball column, and the seeded
polygon bounds half the pitch. Correct call on what it was shown. It was
never shown section 2.3, which defines the ring as a circle of radius 18
centred on the ball OR, with no ball placed, on the centroid of our three
most advanced players. That fallback is unconditional, so the ring is
always renderable. Section 2.3 defines the object and gives it a centre
for exactly this case; section 5.1's clause assumes a ball affordance the
page does not have. Resolved toward 2.3.

Wire: RondoZoneOut and the formations router now carry canonical_rondo,
zone_kind and radius, added to the model by T-101 and seeded by T-103 but
never put on the wire. ZONE_ORDER's last key was still the pre-0007
`counterpress`, which sorted the ring last by accident rather than by
intent; it now names counterpress_ring.

Render: the ring is a circle, never its seeded polygon. Centre comes from
ringCentre() and recomputes with the phase, so it travels with the shape,
which is the teaching point. Radius comes off the seeded column. Drawn in
a 0 0 100 100 viewBox with preserveAspectRatio none, which is normalized
render space, so the centre maps through modelToRender and the radius is
the same number on both axes in either orientation. On screen that is an
ellipse whose aspect is the pitch's, and that is the correct shape: it is
the same locus pointInCircle counts inside, so the drawing and the count
never disagree. The e2e measures this rather than asserting it.

Counting: the ring goes through T-104's countZone as a circle zone, so
there is no second containment test. It stays OUT of buildRead, whose
spare/short read only means something across zones that partition; the
ring overlaps by design. Its chip follows the same rule as every other
zone, seeded and muted with opposition off, computed and verdict coloured
with opposition on.

Hit testing: only the ring's LINE takes a tap (pointer-events: stroke on a
transparent 22px band). Its interior is not hit tested at all, so every
tap inside it falls through to the polygon zone underneath.

splitRondoName is gone. rondoDisplayName keeps the ratio out of the zone
title; the fallback chip reads canonical_rondo off the wire.

T-106's two five-zone assertions are corrected rather than loosened: five
polygons plus one circle, and the ring's seeded polygon is still asserted
absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…110)

Closes the spec gap T-108 flagged: doc 06 calls unit-balance warnings the
core mechanic but never says how a formation's eleven slots map to the
seven unit_balance_rules units.

Per the founder's approved decision (2026-08-07):
  - seeds/formations.json now carries slot_family on all 66 slots, seeded
    per formation rather than derived from position_code, which cannot
    tell a back three's cb_wide from its cb_central nor a six from an
    eight when both are CM.
  - app/units.py holds the fixed slot_family-to-unit crosswalk, the
    per-formation unit membership resolver, and the evaluator, which
    reuses the role_clashes evaluation shape and carries the seeded
    warning_copy through verbatim.
  - POST /api/formations/{code}/balance is the coach-only surface. A
    player token gets 403.

A unit the formation does not contain is never evaluated: a 4-3-3
resolves midfield_three, front_three, back_line and two wide units, and
double_pivot and strike_pair are reported in units_not_evaluated rather
than scored empty and made to trip every requires_duty rule. Nothing
assigned is a first-class silent 200, and requires_duty rules wait for a
complete unit so a coach is not shouted at mid-click.

Validator gains three checks: slot_family required on every formation
position, drawn from doc 06's ten families, and naming a family some
archetype actually belongs to. All 33 formation_phases rows still carry
their base formation's slot set, position codes and coordinates
unchanged.

Left undefined rather than invented: the 5-4-1's flat midfield four and
lone striker match no doc 06 unit, so that shape evaluates its back line
only. box_midfield stays reachable from no family, matching T-102
seeding it no rules.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ance, footedness (T-107)

Adds the Formations page's personnel panel (doc 06 sections 2.6, 2.7,
5.3, 5.4) as a second sheet segment alongside "Browse formations": for
each of the eleven slots, a player picker from the team roster, an
archetype picker, the top-three ranked suggestions with the API's own
cited reason, and a coach-only footedness note computed per doc 06
section 2.7. A live unit balance read (POST /formations/{code}/balance)
renders inline underneath, reading the seeded warning_copy verbatim.

Backend: adds slot_family (optional, additive) to FormationPositionOut
and populates it in GET /formations, the one field the panel needs to
call the already-shipped /archetypes and /archetypes/suggest routes.

Suggestions, unit balance, and footedness notes are gated on role_on_team
=== "coach" both in which requests fire and in the DOM (absent, not
hidden, for a player token); T-106's "player role reaches every control
with no failed request" tripwire now also walks into the personnel panel
to prove it. Empty roster is a first-class state: the panel still ranks
archetypes and evaluates balance with nobody assigned. Playwright
journeys at both viewports cover the empty-roster state, assignment, an
archetype pick via a suggestion tap, the top-three cited reasons, a unit
balance note appearing as archetypes change, and a footedness note.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…(T-109)

Wire POP_WEB_PORT/POP_API_PORT into make verify/e2e/dev by deriving a
stable per-worktree default from the checkout path (cksum of CURDIR,
folded into 0-999), so parallel ticket worktrees no longer share one dev
server and silently test the wrong worktree's code. Explicit env vars
still win via ?=, preserving the manual-override contract every ticket
in this epic relied on. Extend the Brief section 6 demo path with a
Tactics Lab beat: the coach morphs to the in-possession phase, turns the
opposition on, and reads a computed superiority, immediately after the
existing seeded-rondo beat it deliberately contrasts with. Fix the stale
`counterpress`/single `flank_corridor` inline comment on
RondoZone.zone_key, which migration 0007 (T-111) obsoleted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…on (T-070)

Derives transparent-background brand assets from the source logo PNG via a
committed, re-runnable Pillow script (scripts/build_logo_assets.py) since
the source sits on a solid red field that also appears inside the leaf,
the shield ring, and every closed wordmark letterform (P, R, A, O). A
plain border flood fill leaves those as opaque red islands, so the script
keys background by colour distance plus connected-component size instead:
large enclosed red regions (leaf, ring) stay opaque, small ones (trapped
letter counters) go transparent alongside the outer field.

- Nav rail: app-brand-dot placeholder replaced with the shield+stars mark
  (shield-mark-96/144.png) in both AppShell.tsx and App.tsx's pre-auth
  MinimalShell, decorative (alt="", aria-hidden) so "Patterns of Play"
  stays the h1's accessible name for App.test.tsx.
- Sign-in: full lockup (shield + stars + grass + wordmark) added above the
  AuthForms card via a new .auth-lockup class; TeamOnboarding's
  create-team/join-code screens deliberately left text-only (brand moment
  belongs once, at the true entry point, and phone vertical space is
  tight).
- favicon-16/32/48.png plus a flattened (opaque, iOS-safe) apple-touch-icon
  wired into index.html; title stays "Patterns of Play".

All derivatives are RGBA PNGs, no baked theme colours in any component;
the one exception (apple-touch-icon's navy backing) is a raster icon
pixel, not a themed surface.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three review defects on the T-070 logo integration:

1. logo-lockup.png's flat wordmark band was white text with no backing
   once the red field was keyed out, near-invisible on the board theme's
   light background. Dropped it from the crop entirely (the shield's own
   arched "PATTERNS OF PLAY" lettering, white on navy, already carries the
   name and reads on any ground); the sign-in img is now decorative
   (alt="", aria-hidden) since MinimalShell's h1 already exposes the
   page's accessible name.

2. favicon-16.png was a muddy downsample of the full crest: the star arc
   turned to noise and the shield silhouette didn't survive. Added a
   stars-free shield-only crop plus a build_favicon_mini flattening pass
   (shield navy + the leaf's own connected component only, dropping the
   gold border, the ring, and the arc lettering that can't survive
   resampling to 16px) so the favicon is designed for 16px instead of
   downsampled to it. 32px keeps the full engraved detail, which is
   legible at that size. Dropped favicon-48.png: nothing in the app
   requests it (no manifest, no browserconfig tile). Added a root
   favicon.ico (built from the same flattened mark) since Chrome requests
   /favicon.ico by habit regardless of <link rel="icon">, and that request
   404ed with nothing at that path.

3. shield-mark-144.png was unused: sizes="28px" never selects a 144w
   candidate over the 96w one that already covers a 3x device. Dropped
   the file and the srcSet/sizes attributes in both AppShell.tsx and
   App.tsx's MinimalShell.

scripts/build_logo_assets.py grows the row-window and connected-component
helpers needed for the two new crops; still re-run by hand, still not a
runtime dependency.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The pitch default theme becomes the red brand theme (shield navy chrome,
brand red accent, shield gold advisories), and dark and board are restyled
to the same brand. Founder palette directive of 2026-08-07, which
supersedes the design README's gold-accent token table.

Board decoupled from chrome first, because the rest was not safe without
it. The board had no colours of its own: PitchMarkings filled with
var(--pitch-turf, transparent), a token nothing defined, so the pitch only
looked green because Board.css painted the wrapper with the chrome's
--bg-stripe. Home mapped to --accent and away to --red, so a red accent
collapsed the two teams. Confirmed lanes read --accent and blocked lanes
--red, so a red accent made "this pass is on" and "this pass is blocked"
the same colour.

There is now a real board token layer (--pitch-turf, --pitch-stripe,
--pitch-line, --token-face, --team-home, --team-away, --ball, --lane-*,
--intercept, --mark, --zone, --keystone, --route-badge*), defined per
theme with literal values and no chrome fallback. --bg-stripe is deleted
so there is no chrome token left to mistake for turf. The board keeps the
design README's football language in all three themes: green turf with
mown stripes, gold "the pass is on", red "blocked / opposition / marking".

Chrome resolves the accent-red vs status-red ambiguity by family and by
form: --accent (brand red) is the only interactive colour and the only red
fill, --warn (shield gold) carries advisories and read-only emphasis (fit
warnings, clash notes, SENT pill, receipts, verdict chips, labels), and
--red is failure only, rendered as text, a 1px outline, or a faint tint,
never a fill on a control. The recording toggle keeps the accent but gains
a ring and a pulse so colour alone never distinguishes it from the
permanently active select tool.

Contrast is computed, not eyeballed: every text pair clears 4.5:1 and
every graphical pair that carries meaning clears 3:1, in all three themes.

Left behind so the layers cannot be recoupled:
- scripts/check_palette.py, run by make check-copy: both layers complete
  in every theme, no board token equal to a chrome colour, no football
  selector reading --accent/--glow/--red/--warn, AA on every pair, three
  distinct themes.
- e2e/palette.spec.ts, both viewports: overrides --accent/--glow/--red at
  the document root and asserts the turf, both teams and a confirmed lane
  are unchanged, plus the brand-red/gold/crimson split on the roster and
  the coach-only fit warning absent from a player's DOM.
Three comments referenced frontend/src/styles/tokens.test.ts, which was
never created. The static guard is scripts/check_palette.py, wired into
make check-copy. A stale pointer to a nonexistent guard is the same
failure mode this epic exists to fix in the design README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR #18 squashed T-100, T-101 and T-104 onto main as one commit, which is not in integration's history, so git saw a conflict on every file those tickets touched that later tickets also changed. Integration is strictly ahead: main's only unique commit is that squash, and its content is already here. Resolved every conflict to integration's side; the resulting tree is identical to integration before the merge.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@BrandanBurgess
BrandanBurgess merged commit 2a24459 into main Aug 9, 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.

1 participant