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
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ v1 is done when a seeded benchmark board can be created, a qualified athlete can

## Current Repository Baseline

As of this packet, `apps/wodsmith-start/src/lib/competitions/capabilities.ts` already defines `competitionCan`, `leaderboardVariant`, selectability helpers, result-entry helpers, and a registry for `in-person` and `online`. The existing test suite intentionally expects `benchmark` to be unknown and fail closed. The first benchmark task updates that baseline deliberately.
As of M0a, `apps/wodsmith-start/src/lib/competitions/capabilities.ts` defines `competitionCan`, `leaderboardVariant`, selectability helpers, result-entry helpers, and registered entries for `in-person`, `online`, and `benchmark`. Benchmark declares `videoSubmissions` and `perpetual`, remains hidden from the generic create picker, and still requires M1 schema/seed work before real benchmark rows can exist.
17 changes: 14 additions & 3 deletions ai/research/hillerfit-benchmark-leaderboard/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,30 @@ Acceptance criteria:
- Benchmark-capable registry entry can be added without reintroducing literal online/in-person checks at those chokepoints.
- `scoringAlgorithm === "online"` branches remain untouched.

## M1 Schema And PDF-Derived Seed
## M1a Schema Primitives

Add benchmark type, benchmark tables, shared columns, validation schemas, and the training-PDF-derived seed.
Add benchmark persistence primitives, shared columns, and validation schemas. This slice does not seed PDF-derived benchmark rows.

Acceptance criteria:

- `competitionType: "benchmark"` is supported in TS and registry without SQL enum migration.
- `benchmark_batteries`, `benchmark_tests`, and `benchmark_tier_thresholds` are defined and exported.
- `trackWorkouts.benchmarkTestId`, `trackWorkouts.benchmarkCategory`, and `scores.benchmarkVariant` are migrated.
- Categories JSON validates on write, publish, and read; `testCount` is treated as a validated cache.
- `absolute_tier` config validation requires `absoluteTier.batteryId`, while the scoring dispatcher fails closed until M2 implements the engine.
- Generic create/edit forms do not expose `benchmark` as a selectable type or render in-person-only venue fields for stored benchmark rows.
- No task creates HillerFit-branded pages, routes, product navigation, marketing copy, logos, or theme treatments.

## M1b PDF-Derived Seed And Receipt

Create the first generic benchmark battery from the local training PDF and record the extraction assumptions.

Acceptance criteria:

- Seed creates one Open division, individual-only tests, benchmark competition settings, included/deferred tests, and pre-encoded thresholds from `/Users/zacjones/Downloads/HillerFit_Training_Guide.pdf`.
- Seed or companion receipt records extraction assumptions and any intentionally deferred tests.
- No task creates HillerFit-branded pages, routes, marketing copy, logos, or theme treatments.
- Seed validation proves every included test has 10 thresholds per supported variant and a matching tagged `trackWorkout`.
- No task creates HillerFit-branded pages, routes, product navigation, marketing copy, logos, or theme treatments.

## M2 Absolute-Tier Scoring

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ This design defines the components and contracts engineers should build for v1.

### New Tables

- `benchmark_batteries`: named product, owner, slug, categories JSON, rating bands JSON, `maxTier`, `scoreMax`, `videoPolicy`, `isOpenJoin`, `variantScalingGroupId`, `competitionId`, status, and non-null `ownerKey` for global slug uniqueness.
- `benchmark_batteries`: named product, owner, slug, categories JSON payload, rating bands JSON payload, `maxTier`, `scoreMax`, `videoPolicy`, `isOpenJoin`, `variantScalingGroupId`, `competitionId`, status, and non-null `ownerKey` for global slug uniqueness. The current MySQL schema stores these JSON payloads in text columns; every write/read/publish path must parse and validate them fail-closed.
- `benchmark_tests`: one test per battery test, with category key, name, position, workout scheme, score type, input unit, `includedInScoring`, optional time cap, and nullable v2 hybrid metadata.
- `benchmark_tier_thresholds`: one pre-encoded threshold per `(test, variant, tier)`, with raw display value preserved.

Expand All @@ -35,10 +35,11 @@ This design defines the components and contracts engineers should build for v1.
## Core Dependencies

- M0a registry extension must land before benchmark submission and leaderboard chokepoints. The base registry already exists for current competition types.
- M1 schema/seed must land before M2 integration tests can use real benchmark rows.
- M1a schema primitives must land before M1b can seed benchmark rows.
- M1b PDF-derived seed and extraction receipt must land before M2 integration tests can use real benchmark rows.
- M2 absolute-tier algorithm must land before M3 can implement keep-best-on-write correctly.
- M3 submission must land before M4 can verify real leaderboard and stats flows.
- M1 seed data depends on extracting tests/thresholds from the local training PDF and recording extraction assumptions.
- M1b seed data depends on extracting tests/thresholds from the local training PDF and recording extraction assumptions.

## Read Flow

Expand Down Expand Up @@ -73,4 +74,4 @@ This design defines the components and contracts engineers should build for v1.

## Branding Boundary

Benchmark routes, tabs, headings, empty states, and stats components use WODsmith's generic benchmark language. The PDF may influence seed data names, test labels, thresholds, categories, and rating bands, but it must not introduce HillerFit-branded pages, routes, logos, marketing sections, or visual theme.
Benchmark routes, tabs, product navigation, headings, empty states, and stats components use WODsmith's generic benchmark language. The PDF may influence seed data names, test labels, thresholds, categories, and rating bands, but it must not introduce HillerFit-branded pages, routes, navigation entries, logos, marketing sections, or visual theme.
4 changes: 2 additions & 2 deletions ai/research/hillerfit-benchmark-leaderboard/test-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This strategy makes the implementation testable without requiring the full histo
- Leaderboard chooses online visual variant for benchmark through `leaderboardVariant` while avoiding publish gating.
- Stats tab appears only for `absolute_tier` competitions.
- Stats page distinguishes untested, attempted tier 0, unavailable config, pending, verified, adjusted, and invalid/excluded states.
- Route/component assertions confirm benchmark pages use generic WODsmith benchmark language and do not introduce HillerFit-branded pages/routes.
- Route/component/navigation assertions confirm benchmark pages use generic WODsmith benchmark language and do not introduce HillerFit-branded pages, routes, tabs, product navigation entries, or calls to action.
- Benchmark event detail submission UI blocks missing gender and surfaces the profile-completion path.
- Open-join flow rejects unpublished/private boards and duplicate races collapse to one registration.

Expand All @@ -51,7 +51,7 @@ This strategy makes the implementation testable without requiring the full histo
6. Mark a score invalid and confirm public leaderboard/stat page excludes it.
7. Confirm the leaderboard shows Overall/100, category scores, rating band, verification state, and no publish-required empty board.
8. Confirm standard online and in-person competition smoke paths still behave as before for the refactored chokepoints.
9. Confirm no HillerFit-branded page, route, logo, marketing section, or theme treatment was added.
9. Confirm no HillerFit-branded page, route, tab, product navigation entry, logo, marketing section, call to action, or theme treatment was added.

## Required Checks

Expand Down
9 changes: 5 additions & 4 deletions ai/research/hillerfit-benchmark-leaderboard/traceability.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ This matrix links implementation artifacts back to the guide and the required te

| Contract | Guide Source | Implementation Artifact | Tests |
| --- | --- | --- | --- |
| Distinct benchmark competition type | Guide §0 D-A, §5.2, §10 M0a/M1 | `requirements.md`, `technical-design.md`, `tasks.md` M0a/M1 | Extend current registry, capability truth table, chokepoint characterization |
| Distinct benchmark competition type | Guide §0 D-A, §5.2, §10 M0a/M1 | `requirements.md`, `technical-design.md`, `tasks.md` M0a/M1a | Extend current registry, capability truth table, chokepoint characterization |
| Benchmark schema primitives | Guide §5.2, §6.3, §10 M1 | `technical-design.md` data model, `tasks.md` M1a, `lat.md/domain.md` | Migration, schema ownership, JSON validators, scoring-config validation, fail-closed scoring dispatch |
| Variant snapshot, not division | Guide §5.1, §8.1, §13.5 | `requirements.md`, `technical-design.md` data/write flow, `tasks.md` M3 | Submission integration, leaderboard variant read, missing variant fail-closed |
| Best-to-date v1 | Guide §7, §8.1, §10 M3 | `requirements.md`, `technical-design.md` write flow, `tasks.md` M3 | Keep-best-on-write, worse/equal retest ignored |
| No publish gate for benchmark | Guide §5.2, §7.4, §13.19 | `requirements.md`, `technical-design.md` failure behavior, `tasks.md` M4 | Public visibility without `divisionResults`, invalid rows excluded |
| 55-test training-guide Lite denominator | Guide §2.1, §9, §10 M1 | `requirements.md`, `technical-design.md` data model, `tasks.md` M1/M2 | PDF-derived seed validation, derived denominator, deferred tests excluded |
| 55-test training-guide Lite denominator | Guide §2.1, §9, §10 M1 | `requirements.md`, `technical-design.md` data model, `tasks.md` M1b/M2 | PDF-derived seed validation, derived denominator, deferred tests excluded |
| Absolute tier scoring | Guide §6.1-§6.5, §10 M2 | `technical-design.md`, `tasks.md` M2 | Tier, direction, half-tier, tiebreak, aggregation unit tests |
| Stats page | Guide §8.3, §10 M4 | `requirements.md`, `technical-design.md`, `tasks.md` M4 | Route/component tests and manual QA |
| V2 boundary | Guide §7, §10 V2 | `requirements.md`, `tasks.md` V2, `assumptions-and-decisions.md` | Migration check excludes `score_attempts` |
| No HillerFit-branded pages | Guide §0 D-D, §12.12 | `requirements.md`, `technical-design.md`, `assumptions-and-decisions.md` | Route/component assertions and manual QA |
| PDF source data | Guide §0 D-D, §12.12 | `requirements.md`, `tasks.md` M1, `test-strategy.md` | Extraction receipt and seed validation |
| No HillerFit-branded pages or navigation | Guide §0 D-D, §12.12 | `requirements.md`, `technical-design.md`, `tasks.md`, `assumptions-and-decisions.md` | Route/component/navigation assertions and manual QA |
| PDF source data | Guide §0 D-D, §12.12 | `requirements.md`, `tasks.md` M1b, `test-strategy.md` | Extraction receipt and seed validation |
4 changes: 3 additions & 1 deletion apps/crew/src/server/crew-judge-rotations.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type RotationQueryDb = Pick<DbClient, "select" | "query">
type CompetitionJudgeRotation =
typeof competitionJudgeRotationsTable.$inferSelect
type JudgeAssignmentVersion = typeof judgeAssignmentVersionsTable.$inferSelect
type StoredCompetitionType =
(typeof competitionsTable.$inferSelect)["competitionType"]
type CrewJudgeHeatRow = {
id: string
trackWorkoutId: string
Expand All @@ -71,7 +73,7 @@ export interface CrewJudgeEvent {
startDate: string
endDate: string
timezone: string | null
competitionType: "in-person" | "online"
competitionType: StoredCompetitionType
}

export interface CrewJudgeWorkout {
Expand Down
4 changes: 2 additions & 2 deletions apps/wodsmith-start/src/components/cohost-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { canDisplayPhysicalVenue } from "@/lib/competitions/venue-volunteer-gate
interface CohostSidebarProps {
competitionId: string
competitionName: string
competitionType?: "in-person" | "online"
competitionType?: string
permissions: CohostMembershipMetadata
children: React.ReactNode
}
Expand All @@ -74,7 +74,7 @@ interface NavGroup {

const getNavigation = (
basePath: string,
competitionType?: "in-person" | "online",
competitionType?: string,
permissions?: CohostMembershipMetadata,
): { overview: NavItem; groups: NavGroup[] } => {
const type = competitionType ?? ""
Expand Down
16 changes: 16 additions & 0 deletions apps/wodsmith-start/src/components/compete/scoring-config-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ function EditablePointsPreview({
)
}

if (!basePoints && algorithm === "absolute_tier") {
return (
<div className="rounded-lg border bg-muted/20 p-6">
<h3 className="font-semibold mb-4">Points Preview</h3>
<p className="text-sm text-muted-foreground">
Absolute-tier scoring uses fixed benchmark thresholds from the
selected benchmark battery.
</p>
</div>
)
}

// scoringAlgorithm axis, not competitionType.
// Online scoring - static display, no customization
if (algorithm === "online" || baseTemplate === "online") {
Expand Down Expand Up @@ -257,6 +269,7 @@ function getBaseTemplateFromAlgorithm(
case "custom":
case "p_score":
case "online":
case "absolute_tier":
return "traditional"
case "winner_takes_more":
return "winner_takes_more"
Expand All @@ -273,6 +286,7 @@ function getPreviewTemplateFromAlgorithm(
case "traditional":
case "custom":
case "p_score":
case "absolute_tier":
return "traditional"
case "winner_takes_more":
return "winner_takes_more"
Expand Down Expand Up @@ -477,6 +491,8 @@ export function ScoringConfigForm({
return null // P-Score is dynamic
case "online":
return null // Online is handled separately in preview
case "absolute_tier":
return null // Benchmark thresholds are displayed by benchmark-specific UI
default:
return generatePointsTable("traditional", 100)
}
Expand Down
4 changes: 2 additions & 2 deletions apps/wodsmith-start/src/components/competition-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ import { cn } from "@/utils/cn"

interface CompetitionSidebarProps {
competitionId: string
competitionType?: "in-person" | "online"
competitionType?: string
children: React.ReactNode
}

Expand All @@ -82,7 +82,7 @@ interface NavGroup {

const getNavigation = (
basePath: string,
competitionType?: "in-person" | "online",
competitionType?: string,
): { overview: NavItem; groups: NavGroup[] } => {
const type = competitionType ?? ""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ interface LeaderboardDivision {

interface LeaderboardCompetitionInfo {
slug: string
competitionType: "in-person" | "online"
competitionType: string
}

/**
Expand Down
77 changes: 42 additions & 35 deletions apps/wodsmith-start/src/components/organizer-competition-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,12 @@ export function OrganizerCompetitionForm({
const navigate = useNavigate()
const router = useRouter()
const isEditMode = !!competition
const initialCompetitionType: CompetitionTypeId =
isSelectableCompetitionTypeValue(competition?.competitionType)
? competition.competitionType
: "in-person"
const showCompetitionTypeField =
!isEditMode || isSelectableCompetitionTypeValue(competition?.competitionType)

// Determine if existing competition is multi-day (start and end dates differ)
const existingIsMultiDay = competition
Expand All @@ -168,7 +174,7 @@ export function OrganizerCompetitionForm({
teamId: competition?.organizingTeamId ?? selectedTeamId,
name: competition?.name ?? "",
slug: competition?.slug ?? "",
competitionType: competition?.competitionType ?? "in-person",
competitionType: initialCompetitionType,
isMultiDay: existingIsMultiDay,
startDate: competition?.startDate
? formatDateForInput(competition.startDate)
Expand Down Expand Up @@ -442,40 +448,41 @@ export function OrganizerCompetitionForm({
)}
/>

{/* Competition Type */}
<FormField
control={form.control}
name="competitionType"
render={({ field }) => (
<FormItem>
<FormLabel>Competition Type</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
disabled={isEditMode}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select competition type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{selectableCompetitionTypeOptions().map((option) => (
<SelectItem key={option.id} value={option.id}>
{option.displayLabel}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
{field.value === "online"
? "Athletes submit video recordings of their workouts"
: "Athletes compete at a physical venue"}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{showCompetitionTypeField && (
<FormField
control={form.control}
name="competitionType"
render={({ field }) => (
<FormItem>
<FormLabel>Competition Type</FormLabel>
<Select
onValueChange={field.onChange}
value={field.value}
disabled={isEditMode}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select competition type" />
</SelectTrigger>
</FormControl>
<SelectContent>
{selectableCompetitionTypeOptions().map((option) => (
<SelectItem key={option.id} value={option.id}>
{option.displayLabel}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
{field.value === "online"
? "Athletes submit video recordings of their workouts"
: "Athletes compete at a physical venue"}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
)}

{/* Series/Group Selector */}
{groups.length > 0 && (
Expand Down
1 change: 1 addition & 0 deletions apps/wodsmith-start/src/db/schemas/benchmarks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "@repo/wodsmith-db/schemas/benchmarks"
6 changes: 6 additions & 0 deletions apps/wodsmith-start/src/lib/scoring/algorithms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ export function calculateEventPoints(
return calculateOnlineEventPoints(scores, scheme, config)
case "custom":
return calculateCustomEventPoints(scores, scheme, config)
case "absolute_tier":
throw new Error("absolute_tier scoring is configured but not implemented")
default: {
// TypeScript exhaustiveness check
const _exhaustive: never = config.algorithm
Expand Down Expand Up @@ -656,6 +658,8 @@ export function calculatePointsForPlace({
)
case "p_score":
return 0
case "absolute_tier":
throw new Error("absolute_tier place points are not available")
default: {
const _exhaustive: never = config.algorithm
throw new Error(`Unknown scoring algorithm: ${_exhaustive}`)
Expand All @@ -680,6 +684,8 @@ export function getScoringAlgorithmName(
return "Online"
case "custom":
return "Custom"
case "absolute_tier":
return "Absolute Tier"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ interface JudgeSchedulingContainerProps {
competitionId: string
competitionSlug: string
organizingTeamId: string
competitionType: "in-person" | "online"
competitionType: string
events: CompetitionWorkout[]
heats: HeatWithAssignments[]
judges: JudgeVolunteerInfo[]
Expand Down
Loading
Loading