Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
cec879c
chore(backlog): T-011 done
BrandanBurgess Jul 16, 2026
d0a1e4f
docs(agent): orchestrator state snapshot for session handoff
BrandanBurgess Jul 16, 2026
613cad2
feat(screens): whiteboard page with toolbar, view menu, record/save (…
BrandanBurgess Jul 16, 2026
b5d7160
merge: T-030 whiteboard page into integration
BrandanBurgess Jul 16, 2026
24ccc96
chore(backlog): T-030 done, T-031 + T-033 doing
BrandanBurgess Jul 16, 2026
23e21ff
feat(screens): patterns page with libraries, chips, search, details (…
BrandanBurgess Jul 16, 2026
880efb4
merge: T-031 patterns page into integration
BrandanBurgess Jul 16, 2026
733ac3f
feat(screens): roster page with CRUD, sliders, double-exposure warnin…
BrandanBurgess Jul 16, 2026
598c97a
fix(infra): seed database in e2e boot path so fresh environments pass…
BrandanBurgess Jul 16, 2026
b165909
merge: T-031 seed-on-boot fix into integration
BrandanBurgess Jul 16, 2026
09c9fe2
merge: reconcile main squash history into integration
BrandanBurgess Jul 16, 2026
ea0930f
Merge remote-tracking branch 'origin/main' into integration
BrandanBurgess Jul 16, 2026
7fd5a3a
merge: T-033 roster page into integration (resolve nav shell, schemas…
BrandanBurgess Jul 16, 2026
add1c38
chore(backlog): T-031 done, T-033 pr, T-032 + T-034 doing
BrandanBurgess Jul 16, 2026
e8926a7
Merge remote-tracking branch 'origin/main' into integration
BrandanBurgess Jul 16, 2026
f331829
chore(backlog): T-033 done
BrandanBurgess Jul 16, 2026
c634850
feat(screens): formations page with keystones, keycards, rondo map (T…
BrandanBurgess Jul 16, 2026
877ad0d
merge: T-032 formations page into integration
BrandanBurgess Jul 16, 2026
35a07a2
chore(e2e): drop duplicated nav assertion left by merge
BrandanBurgess Jul 16, 2026
ce9695c
feat(screens): identity page with reference teams, pass-risk, cult co…
BrandanBurgess Jul 16, 2026
62c8b24
Merge remote-tracking branch 'origin/main' into integration
BrandanBurgess Jul 16, 2026
02cf1c3
merge: T-034 identity page into integration (all five nav entries live)
BrandanBurgess Jul 16, 2026
f8fde64
Merge remote-tracking branch 'origin/main' into integration
BrandanBurgess Jul 16, 2026
4d7ff05
chore(backlog): T-032 + T-034 done, screens phase complete
BrandanBurgess Jul 16, 2026
d1ce7c1
chore(backlog): T-040 + T-041 doing
BrandanBurgess Jul 16, 2026
abb4823
feat(collab): role gating suite, API enforcement audit (T-040)
BrandanBurgess Jul 16, 2026
817df6d
merge: T-040 role gating suite into integration
BrandanBurgess Jul 16, 2026
8732131
chore(backlog): founder decisions recorded, add T-012 + T-043
BrandanBurgess Jul 16, 2026
0151d7b
feat(collab): playstyle suggestion flow (T-041)
BrandanBurgess Jul 16, 2026
27c9211
Merge remote-tracking branch 'origin/main' into integration
BrandanBurgess Jul 16, 2026
2131cc4
merge: T-041 suggestion flow into integration
BrandanBurgess Jul 16, 2026
0e4cf0c
chore(backlog): T-041 pr, T-012 doing
BrandanBurgess Jul 16, 2026
302040d
Merge remote-tracking branch 'origin/main' into integration
BrandanBurgess Jul 16, 2026
7f69ea2
chore(backlog): T-041 done
BrandanBurgess Jul 16, 2026
ea71eae
feat(platform): role-scoped join codes, head coach member management …
BrandanBurgess Jul 16, 2026
0282683
merge: T-043 team management into integration
BrandanBurgess Jul 16, 2026
7116ee6
feat(content): identity age_hint column, migration, Bible backfill (T…
BrandanBurgess Jul 16, 2026
335e147
Merge remote-tracking branch 'origin/main' into integration
BrandanBurgess Jul 16, 2026
c3f273c
merge: T-012 identity age_hint into integration
BrandanBurgess Jul 16, 2026
0fc6f18
merge-fix: re-point T-012 migration to 0005 after T-043's 0004
BrandanBurgess Jul 16, 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
4 changes: 4 additions & 0 deletions backend/app/models/formations.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,10 @@ class Identity(Base):
signature_pattern_codes: Mapped[list] = mapped_column(JSON, nullable=False, default=list)
keystone_roles_json: Mapped[dict | list | None] = mapped_column(JSON, nullable=True)
youth_takeaway: Mapped[str] = mapped_column(Text, nullable=False)
# Bible 8.2.4 / doc 03 amendment (T-012, founder decision 2026-07-16):
# every identity carries an age-suitability hint alongside its youth
# takeaway, same rule and shape as library_items.age_hint.
age_hint: Mapped[str] = mapped_column(String(60), nullable=False)
block: Mapped[str | None] = mapped_column(String(10), nullable=True) # high|mid|low
# style archetypes only (Bible 5.7 row): encouraged, tolerated,
# discouraged, tempo_rule.
Expand Down
1 change: 1 addition & 0 deletions backend/app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ class IdentityOut(BaseModel):
# rather than a fixed model (mirrors LibraryItemOut.extras above).
keystone_roles: list | None = Field(default=None, validation_alias="keystone_roles_json")
youth_takeaway: str
age_hint: str
block: Literal["high", "mid", "low"] | None
# style archetypes only (Bible 5.7): encouraged/tolerated/discouraged/tempo_rule.
pass_risk: dict | None = Field(default=None, validation_alias="pass_risk_json")
Expand Down
46 changes: 46 additions & 0 deletions backend/migrations/versions/0005_identity_age_hint.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""identities.age_hint (founder decision 2026-07-16, T-012): doc 03 section
5's identities schema had no age_hint column when T-010 shipped, so Bible
8.2.4's "every identity carries a youth takeaway line and an age-suitability
hint" rule was only honoured for library_items (patterns/deliveries/
rotations), not identities. The founder has since decided to amend the
schema rather than leave identity cards without an age hint.

Added nullable=False with server_default="" (rather than a bare add, doc
03's identities table already has rows in any dev DB that ran T-010's
seed): SQLite's batch-recreate for an ALTER TABLE ADD COLUMN NOT NULL needs
a value for existing rows, and scripts/seed.py's upsert-by-code re-run
overwrites every row's placeholder with its real Bible-backed value
immediately afterward, so the empty string never surfaces to a user who
runs `make seed` after upgrading.

Built off 0003 (players.flank, T-033) in parallel with T-043's 0004
(role-scoped join codes); T-043 merged first, so this migration was
re-pointed onto 0004 at integration merge (orchestrator resolution).

Revision ID: 0005
Revises: 0004
Create Date: 2026-07-16 00:00:00.000000

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

revision: str = "0005"
down_revision: Union[str, None] = "0004"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
with op.batch_alter_table("identities") as batch_op:
batch_op.add_column(
sa.Column("age_hint", sa.String(length=60), nullable=False, server_default="")
)


def downgrade() -> None:
with op.batch_alter_table("identities") as batch_op:
batch_op.drop_column("age_hint")
16 changes: 16 additions & 0 deletions backend/tests/test_identity_routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def _seed_identities() -> None:
signature_pattern_codes=["A1"],
keystone_roles_json=[{"role": "single_pivot", "note": "The pivot."}],
youth_takeaway="A youth takeaway line.",
age_hint="U13+",
block="high",
pass_risk_json=None,
shape_render="animated",
Expand All @@ -93,6 +94,7 @@ def _seed_identities() -> None:
signature_pattern_codes=[],
keystone_roles_json=[{"role": "stopper_cb", "note": "The stopper."}],
youth_takeaway="Another youth takeaway.",
age_hint="U11+",
block="mid",
pass_risk_json=None,
shape_render="static",
Expand All @@ -113,6 +115,7 @@ def _seed_identities() -> None:
signature_pattern_codes=["B5"],
keystone_roles_json=["single_pivot", "false_9"],
youth_takeaway="A style youth takeaway.",
age_hint="U13+",
block="high",
pass_risk_json={
"encouraged": ["Short circulation"],
Expand All @@ -138,6 +141,7 @@ def _seed_identities() -> None:
signature_pattern_codes=[],
keystone_roles_json=None,
youth_takeaway="A cult corner youth takeaway.",
age_hint="U9+",
block=None,
pass_risk_json=None,
shape_render="details_only",
Expand Down Expand Up @@ -203,3 +207,15 @@ def test_players_can_browse_identities_too(client: TestClient) -> None:
def test_identity_route_requires_authentication(client: TestClient) -> None:
_seed_identities()
assert client.get("/api/identities").status_code == 401


def test_every_identity_carries_a_non_empty_age_hint(client: TestClient) -> None:
"""T-012 (Bible 8.2.4 / doc 03 amendment): every identity, of every
kind, must serve a non-empty age_hint through the route, same rule
LibraryItemOut already enforces for patterns/deliveries/rotations."""
_seed_identities()
coach = _coach_with_team()
identities = coach.get("/api/identities").json()
assert len(identities) == 4
for item in identities:
assert item["age_hint"], f"{item['code']}: missing age_hint"
4 changes: 4 additions & 0 deletions backend/tests/test_seed_content.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,10 @@ def test_identity_entries_carry_the_bible_youth_and_traceability_fields(fname: s
assert _word_count(item["tag_line"]) <= 25, f"{fname} {code}: tag_line over 25 words"
assert item["core_idea"], f"{fname} {code}: missing core_idea"
assert item["youth_takeaway"], f"{fname} {code}: missing youth_takeaway"
# T-012 (Bible 8.2.4, founder decision 2026-07-16): every identity
# carries an age-suitability hint alongside its youth takeaway,
# same rule library_items already enforces (age_hint check above).
assert item["age_hint"], f"{fname} {code}: missing age_hint"
assert item["source_ref"].startswith("bible:"), f"{fname} {code}: bad source_ref"


Expand Down
6 changes: 6 additions & 0 deletions e2e/identity.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ test.describe("identity: five-part Section 6 template, pass-risk, cult corner",
await expect(panel.getByTestId("identity-detail-youth-takeaway")).toContainText(
"Positions before players"
);
// T-012: Bible 8.2.4's age-suitability hint, added after Youth takeaway.
await expect(panel.getByTestId("identity-detail-age-hint")).toContainText("U13+");
// A reference team's own pass_risk_json is null: the block never renders.
await expect(panel.getByTestId("identity-detail-pass-risk")).toHaveCount(0);
// Order in the DOM matches the template's own order.
Expand All @@ -185,6 +187,7 @@ test.describe("identity: five-part Section 6 template, pass-risk, cult corner",
"identity-detail-signature-patterns",
"identity-detail-keystone-roles",
"identity-detail-youth-takeaway",
"identity-detail-age-hint",
]);
await page.getByTestId("identity-details-close").click();
await expect(page.getByTestId("identity-details-panel")).toHaveCount(0);
Expand All @@ -206,6 +209,7 @@ test.describe("identity: five-part Section 6 template, pass-risk, cult corner",
await page.getByTestId("identity-details-toggle").click();
await expect(page.getByTestId("identity-detail-formation")).toContainText("4-2-3-1");
await expect(page.getByTestId("identity-detail-youth-takeaway")).toBeVisible();
await expect(page.getByTestId("identity-detail-age-hint")).toContainText("U13+");

await assertCleanPage(page, issues);
});
Expand All @@ -229,6 +233,7 @@ test.describe("identity: five-part Section 6 template, pass-risk, cult corner",
// Style archetypes' core_idea has no "Formation:" leading sentence, so
// the template does not fabricate a Formation & shape row for them.
await expect(page.getByTestId("identity-detail-formation")).toHaveCount(0);
await expect(page.getByTestId("identity-detail-age-hint")).toContainText("U13+");

await assertCleanPage(page, issues);
});
Expand All @@ -251,6 +256,7 @@ test.describe("identity: five-part Section 6 template, pass-risk, cult corner",
await expect(page.getByTestId("identity-detail-keystone-roles")).toHaveCount(0);
await expect(page.getByTestId("identity-detail-pass-risk")).toHaveCount(0);
await expect(page.getByTestId("identity-detail-signature-patterns")).toHaveCount(0);
await expect(page.getByTestId("identity-detail-age-hint")).toContainText("U11+");

await assertCleanPage(page, issues);
});
Expand Down
1 change: 1 addition & 0 deletions frontend/src/identityApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export interface IdentityOutWire {
signature_pattern_codes: string[];
keystone_roles: KeystoneRoleWire[] | null;
youth_takeaway: string;
age_hint: string;
block: "high" | "mid" | "low" | null;
pass_risk: PassRiskWire | null;
shape_render: "animated" | "static" | "details_only";
Expand Down
13 changes: 9 additions & 4 deletions frontend/src/pages/IdentityPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
// render Details only (CLAUDE.md rule 6: no designed surface, no
// invented one). Details follows the Section 6 five-part template
// (Formation & shape, Core idea, Signature patterns, Keystone roles,
// Youth takeaway); style archetypes additionally show the pass-risk
// block (Bible 5.7) between Keystone roles and Youth takeaway. Copy rule
// (doc 03 section 7): identities curate, never lock.
// Youth takeaway), plus an Age hint row (Bible 8.2.4, T-012); style
// archetypes additionally show the pass-risk block (Bible 5.7) between
// Keystone roles and Youth takeaway. Copy rule (doc 03 section 7):
// identities curate, never lock.

import { useEffect, useMemo, useState } from "react";
import type { Orientation } from "../board/coords";
Expand Down Expand Up @@ -150,7 +151,7 @@ export function IdentityPage({ orientation }: IdentityPageProps) {
className="app-page-info"
aria-hidden="true"
data-testid="identity-info"
title="Identities curate, never lock. Pull up Browse identities to explore the great teams and style archetypes: pick one, and its signature idea plays on the board, with the formation, keystone roles, and youth takeaway in Details."
title="Identities curate, never lock. Pull up Browse identities to explore the great teams and style archetypes: pick one, and its signature idea plays on the board, with the formation, keystone roles, youth takeaway, and age hint in Details."
>
i
</span>
Expand Down Expand Up @@ -286,6 +287,10 @@ export function IdentityPage({ orientation }: IdentityPageProps) {
<p className="patterns-details-kicker">Youth takeaway</p>
<p>{selection.youth_takeaway}</p>
</div>
<div className="identity-detail-row" data-testid="identity-detail-age-hint">
<p className="patterns-details-kicker">Age hint</p>
<p>{selection.age_hint}</p>
</div>
</div>
</div>
)}
Expand Down
2 changes: 1 addition & 1 deletion scripts/validate_seeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
SYNERGY_REQUIRED_FIELDS = ["code", "name", "why_it_works", "source_ref", "content_version"]
CLASH_REQUIRED_FIELDS = ["code", "name", "trigger_expression", "warning_copy", "source_ref", "content_version"]
IDENTITY_REQUIRED_FIELDS = [
"code", "name", "tag_line", "core_idea", "youth_takeaway", "shape_render",
"code", "name", "tag_line", "core_idea", "youth_takeaway", "age_hint", "shape_render",
"source_ref", "content_version",
]

Expand Down
6 changes: 6 additions & 0 deletions seeds/identities_archetypes.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"single_pivot", "advanced_8", "false_9", "inside_forward", "touchline_winger"
],
"youth_takeaway": "Teach the team to occupy zones and form triangles, and the free man appears from structure, not from individual talent.",
"age_hint": "U13+",
"block": "high",
"pass_risk_json": {
"encouraged": ["Short circulation", "line-breakers into feet", "F7 slide-rule through balls", "prepared B3 switches", "F1 cutbacks"],
Expand All @@ -40,6 +41,7 @@
"overlapping_fb", "anchor_destroyer", "classic_10", "wide_forward", "runner_in_behind"
],
"youth_takeaway": "A team does not need the ball to be dangerous: know exactly who sprints and where the instant it is won back.",
"age_hint": "U11+",
"block": "mid",
"pass_risk_json": {
"encouraged": ["First pass forward", "F8 ball over the top", "F7 slide-rule through balls", "F2 early crosses"],
Expand All @@ -65,6 +67,7 @@
"box_to_box_8", "false_9", "inside_forward"
],
"youth_takeaway": "Effort can be a system, not just a vibe: pressing has choreography, with curved runs and triggers, that can be trained like a passing drill.",
"age_hint": "U13+",
"block": "high",
"pass_risk_json": {
"encouraged": ["Vertical ground passes", "F3 driven crosses", "quick F7 through balls after regains"],
Expand All @@ -90,6 +93,7 @@
"anchor_destroyer", "box_to_box_8", "target_man", "runner_in_behind"
],
"youth_takeaway": "Defending deep is a plan, not a surrender: never send a block out without an outlet to break with.",
"age_hint": "U11+",
"block": "low",
"pass_risk_json": {
"encouraged": ["F13 channel outlets", "long clearances to the target", "F8 over the top when sprung"],
Expand All @@ -115,6 +119,7 @@
"box_to_box_8", "touchline_winger", "target_man", "second_striker"
],
"youth_takeaway": "Winning the loose ball after a long pass is a team job that can be organised and drilled, not left to chance.",
"age_hint": "U9+",
"block": "mid",
"pass_risk_json": {
"encouraged": ["F13 channel balls", "long diagonals to the target", "F4 floated crosses", "forward throw-ins"],
Expand All @@ -140,6 +145,7 @@
"anchor_destroyer", "classic_10", "second_striker", "target_man"
],
"youth_takeaway": "Teams do not have to pick a single lane: the sanest identity for most youth sides is one that can do both.",
"age_hint": "U13+",
"block": "mid",
"pass_risk_json": {
"encouraged": ["The possession menu when controlling the game", "the counter menu on transition"],
Expand Down
6 changes: 6 additions & 0 deletions seeds/identities_cult_corner.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"signature_pattern_codes": [],
"keystone_roles_json": null,
"youth_takeaway": "A complete defensive plan, drilled without compromise, can beat teams with far more individual talent.",
"age_hint": "U11+",
"block": "low",
"pass_risk_json": null,
"shape_render": "details_only",
Expand All @@ -29,6 +30,7 @@
"signature_pattern_codes": ["C2"],
"keystone_roles_json": null,
"youth_takeaway": "A restart can be a weapon: rehearse the throw-in with the same care as any other delivery.",
"age_hint": "U9+",
"block": null,
"pass_risk_json": null,
"shape_render": "details_only",
Expand All @@ -46,6 +48,7 @@
"signature_pattern_codes": ["C2"],
"keystone_roles_json": null,
"youth_takeaway": "Organisation without the ball is a repeatable advantage, not a lesser style of play.",
"age_hint": "U9+",
"block": "low",
"pass_risk_json": null,
"shape_render": "details_only",
Expand All @@ -63,6 +66,7 @@
"signature_pattern_codes": ["C2"],
"keystone_roles_json": null,
"youth_takeaway": "Commitment to a simple plan, played without hesitation, can unsettle a more talented opponent.",
"age_hint": "U9+",
"block": null,
"pass_risk_json": null,
"shape_render": "details_only",
Expand All @@ -80,6 +84,7 @@
"signature_pattern_codes": ["B5"],
"keystone_roles_json": null,
"youth_takeaway": "A youth-development pathway can produce a team that outplays clubs with far larger squads.",
"age_hint": "U13+",
"block": "high",
"pass_risk_json": null,
"shape_render": "details_only",
Expand All @@ -97,6 +102,7 @@
"signature_pattern_codes": ["D1"],
"keystone_roles_json": null,
"youth_takeaway": "A clear defensive job for every player can be a title-winning identity on its own.",
"age_hint": "U11+",
"block": "high",
"pass_risk_json": null,
"shape_render": "details_only",
Expand Down
Loading
Loading