Skip to content

feat(benchmark): meeting action items — gating, tier-linking UX, scoring lock, copy and seed fixes#613

Open
zacjones93 wants to merge 2 commits into
feat/hillerfit-planfrom
claude/benchmark-feature-refinements-7yzcfh
Open

feat(benchmark): meeting action items — gating, tier-linking UX, scoring lock, copy and seed fixes#613
zacjones93 wants to merge 2 commits into
feat/hillerfit-planfrom
claude/benchmark-feature-refinements-7yzcfh

Conversation

@zacjones93

@zacjones93 zacjones93 commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Benchmark feature refinements from 7/7 meeting

Implements the action items from the Zac/Ian meeting reviewing the benchmark feature on this branch. Each item below lists what was decided, why it was addressed, the solution taken, and questions to consider while reviewing. Work was implemented by parallel subagents, then adversarially reviewed by two independent review agents; both HIGH-severity review findings were fixed in a follow-up round.

Action items addressed

1. Remove the "Tier context is live" card (organizer tiers page)

  • Meeting: "Let's remove that. It doesn't make a lot of sense."
  • Solution: Card removed entirely from scoring/tiers.tsx (icon import cleaned up). The page now flows header → link-events → how-scoring-works → settings → categories → rating bands → thresholds.

2. "Half a tier" → "half a point" + tier-count wording

  • Meeting: the how-scoring-works copy shouldn't hardcode "1 to 10", and "earns half a tier" should say "half a point".
  • Why/solution: The tier count was already interpolated from the configured maxTier (it rendered "10" because the training-guide battery has 10 tiers) — no change needed there. The wording is now "still earns half a point".
  • Review question: technically the value credited is 0.5 tier (worth ~5 points on the 100-point category scale, and the leaderboard shows "Tier 0.5"). "Half a point" is what the meeting asked for but it's the less precise phrasing — happy to reword to "half credit" if you prefer accuracy.

3. "Link events to tier thresholds" section + derive test data from the event

  • Meeting: "Instead of adding event tiers, have a list of events up here that don't have a tier… when you select it, it pops open this modal with the category and the scoring inputs"; "derive all this data from the event that it's tied to — all of this data other than category and model."
  • Solution: New card at the top of the tiers page listing events with no linked benchmark test; clicking one opens the existing Add-test dialog locked to that event with name, scheme, score type, and input unit prefilled from the event (server summary events now carry scheme/scoreType). Category, score model, and thresholds remain for the organizer. All prefilled fields stay editable, and manual names are never clobbered. The per-category "Add event tiers" buttons remain as a secondary path (they get the same prefill for free). The event-edit page's inline dialog also passes the live scheme/scoreType now.
  • Review questions: (a) should the per-category "Add event tiers" buttons be removed entirely ("instead of")? I kept them since standalone tests without events are still legitimate. (b) There is no time-cap field on the test model, so a time-with-cap event's cap isn't prefilled — benchmark_tests.timeCapMs exists but no editor flow writes it; follow-up?

4. Time schemes auto-select the "time" input unit

  • Meeting: "Seems like when you select time, it should auto select input unit is time."
  • Solution: Selecting time, time-with-cap, or emom (also time-entered on the athlete form) in the test editor auto-sets input unit "time"; leaving a time scheme reverts the unit (Add → default, Edit → the test's original) so a stale "time" unit can't corrupt threshold encoding. Choosing the hybrid score model (which forces time-with-cap) routes through the same logic.

5. Bounded linked-event select

  • Meeting: "Select should be a bounded amount… like 10 items."
  • Solution: The linked-event dropdown is capped at ~10 rows (max-h-80) and scrolls. No search combobox — the meeting settled on bounded-with-scroll being fine.

6. Benchmarks lose cohosts, volunteer shifts, and public volunteer signup

  • Meeting: "Probably just remove cohosts from benchmarks for now"; "get rid of volunteer shift, that makes no sense"; "we should probably take this [sign up to volunteer] off of benchmarks" — volunteer roster/inviting and waivers stay.
  • Solution: Three new registry capabilities (cohosts, volunteerShifts, publicVolunteerSignup) — true for in-person and online, false for benchmark, fail-closed for unknown types. Organizer sidebar hides Co-Hosts and Volunteer shifts for benchmarks (roster + registration questions stay); the co-hosts, volunteer-shifts, and public volunteer-signup routes redirect defensively; the "Sign up to volunteer" card is hidden on benchmark public pages. Truth-table, sidebar, and registration-sidebar tests extended; lat.md updated.
  • Review question: the underlying server fns (inviteCohostFn, volunteer-shift fns, submitVolunteerSignupFn) are not capability-gated — the gate is UI + route loaders. Reachable only by scripted calls or a comp switched to benchmark after acquiring cohosts/shifts. Want server-side guards as a follow-up?

7. Benchmark scoring hard-coded to online (tiebreakers kept)

  • Meeting: "This should probably just always be online… we'll hard code this, but you still want the tie breaker."
  • Solution: For benchmark boards the algorithm radio group is replaced with a static "Benchmark boards always use Online scoring" note across all three surfaces that render the form (organizer scoring, organizer settings, cohost scoring); the form forces online at init and on submit; the organizer and cohost save server fns force it server-side; and — per review finding — getCompetitionLeaderboard now forces the effective algorithm to online for benchmark boards at read time, so a freshly created benchmark (which stores no scoringConfig and previously fell back to "traditional") ranks correctly without the organizer ever pressing Save. Tiebreaker and DNF/DNS settings remain fully editable.

8. Leaderboard affiliate placement

  • Meeting: "It should probably be by the athlete."
  • Solution: The dedicated sortable "Affiliate" column (which duplicated the subtext already shown under the athlete name on public boards) is removed from both overall and event views; affiliate renders only under the athlete/team name (desktop + mobile). The affiliate URL filter is untouched. Group-header column math adjusted; a persisted sort on the removed column is dropped safely.
  • Review question: the in-person leaderboard table still has its own affiliate column — intentional (the meeting discussed the benchmark/online board), but flag if you want parity. Sort-by-affiliate is the only lost ability.

9. Stats page athlete selector label

  • Meeting: "Oh, it should probably have a label."
  • Solution: Both the Division and Athlete selects now have visible, accessibly-associated labels, matching the labeled-select idiom used on the home page.

10. "Pre-published" → "Auto-publish"

  • Meeting: "Autopublish might be the better word."
  • Solution: All user-facing strings renamed (selector, option, explanatory paragraph, server error); the resultsAutoPublish settings key and identifiers are unchanged. lat.md updated.

11. Seed data: vertical jump in inches

  • Meeting: "Oh, it's in feet… This should probably be in inches. Four feet is an insane amount of distance."
  • Why: real data bug — the athlete input keyed off the workout scheme ("feet") and collected feet, while the seeded thresholds converted inches→mm. An athlete typing "30" (meaning inches) encoded 30 ft and blew past the top tier.
  • Solution: Vertical Jump switched to scheme points + inputUnit inches (the same pattern the seed already uses for "BikeErg Avg Watts"): thresholds and athlete entries are now plain integer inches in the same number space, verified end-to-end (parse → encode → tier comparison) by review.
  • Review questions: (a) the athlete input for this test is now labeled "Points" and the leaderboard shows "38 pts" — numerically right, cosmetically wrong. The real fix is letting the submission form honor a benchmark test's inputUnit (or adding an inches distance unit); worth a follow-up ticket. (b) inputUnit is stored as "inches" not "in" because both threshold encoders apply an inches→feet→mm conversion when the unit is exactly "in" — consequence: the Edit-test dialog's unit select shows empty for this test (harmless; unchanged fields aren't submitted).

12. Seed data: L-sit hold top standard = 3:00

  • Meeting: "This should probably be three minutes… that's what it is in the CrossFit Journal."
  • Solution: Tier 10 changed 1:30 → 3:00 for both male and female. Tiers 1–9 untouched.
  • Review question: this leaves a big jump from tier 9 (1:21 M / 1:19 F) to 3:00. Rescaling tiers 2–9 would diverge from the source PDF — do you want an interpolated progression instead?

Miscellaneous action items identified but NOT acted on

  • Gender-split leaderboard as a setting — the male/female filter already exists on benchmark boards; making split-by-default a per-benchmark setting was left as tentative in the meeting ("might be a request someone wants"). Needs a product decision + settings schema.
  • Category scores as separate grouped leaderboard columns — debated; Zac preferred the combined Categories summary cell, and category grouping of event columns already shipped. No decision.
  • Submit-score button placement on the benchmark event card ("should probably be on the right hand side… or up there") — no settled placement.
  • Movement-pattern tagging of events (upper-body press, hinge, etc.) and pattern-based visualizations — future idea; requires tagging infrastructure.
  • Personalized athlete dashboard / spider-web chart over time — future idea.
  • Private benchmarks / password-protected private competitions (Competition Corner-style) — future feature.
  • Sponsor/vendor packages, applications, and payments — competitions-scope feature; needs its own design (pricing section vs sponsor section, application workflow, Stripe).
  • Video-proof "require for top scores" first-submitter annoyance (first athlete to log everything must film everything) — no decision was reached in the meeting.
  • "Host to comp" copy for teams without the organizer entitlement — seed/entitlement data issue, not a code change.
  • Testing a from-scratch benchmark with non-default tiers / imperfect data — Zac said he wants to do this manually; existing tests cover non-10 maxTier math, but a full organizer-flow E2E is a good follow-up.
  • Crew-app items (copy-from-previous heats, auto-preview volunteer import, single add-people button, Excel import) — Ian's, in the crew app, out of scope here.
  • Comment cleanup: two non-user-facing comments still say "pre-publish" (src/types/competitions.ts:65, src/server/competition-leaderboard.ts:249).

Notes

  • Team memory (/recall) was unreachable from this environment (worker fetch failed through the proxy), and the GitNexus MCP tools were not connected, so the mandated gitnexus_impact/detect_changes runs could not be performed; blast radius was instead bounded by capability-gate tests, the full suite, and two independent review agents.
  • Validation: full test suite (162 files / 3069 tests), pnpm type-check, Biome on all changed files, and lat check all pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R5MgTsR5FNrj7Tvu7T6Ebv


Generated by Claude Code


Summary by cubic

Implements 7/7 benchmark meeting items: locks benchmark boards to online scoring, adds event-to-tier linking with smart prefills, gates benchmark-only capabilities, and makes copy/UI and seed fixes to improve clarity and data accuracy.

  • New Features

    • Added a “Link events to tier thresholds” card at the top of Tiers: lists unlinked events and opens the Add-test dialog prefilled (name, scheme, score type, input unit); bounded select (~10 items); time schemes auto-pick “time”.
    • Benchmark boards always rank with the online algorithm: algorithm picker hidden; forced on submit and at leaderboard read; tiebreaker and DNF/DNS stay editable.
    • Capability gates: benchmarks disable cohosts, volunteerShifts, and publicVolunteerSignup; sidebar items hidden and routes redirect; volunteer roster and waivers remain.
  • Refactors

    • Leaderboard: removed the dedicated Affiliate column; affiliate now shows under the athlete/team name.
    • Copy and labels: “Pre-published” → “Auto-publish”; scoring explainer says “half a point”; added labels for Division and Athlete on the stats page; removed the “Tier context is live” card.
    • Seed fixes: Vertical Jump now scores in whole inches via the points scheme; L-sit hold top tier set to 3:00.

Written for commit 45f6985. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Benchmark scoring boards now consistently use online scoring, with scoring options simplified where appropriate.
    • Added clearer benchmark tier linking and event prefill behavior, including better handling for time-based schemes.
    • Updated stats filters with labeled Division and Athlete selectors.
  • Bug Fixes

    • Hidden volunteer, co-host, and shift options when a competition type doesn’t support them.
    • Improved benchmark leaderboard display by removing the Affiliate column and refining publish wording to “auto-publish.”

claude added 2 commits July 8, 2026 05:46
- Remove the 'Tier context is live' card and say 'half a point' in the
  scoring explainer on the organizer tiers page
- Add a 'Link events to tier thresholds' section listing unlinked events;
  selecting one opens the add-test dialog locked to that event with
  name/scheme/score type/input unit prefilled from the event
- Auto-select the 'time' input unit for time schemes in the test editor
  and bound the linked-event select to a scrollable ~10 items
- Add cohosts/volunteerShifts/publicVolunteerSignup capabilities; benchmarks
  drop cohosts, volunteer shifts, and public volunteer signup (roster and
  waivers stay); sidebar items hidden and routes redirect defensively
- Lock benchmark ranking algorithm to online (UI note + submit force +
  server-side guard) while keeping tiebreakers editable
- Remove the dedicated leaderboard Affiliate column; affiliate stays as
  subtext under the athlete name
- Label the Division and Athlete selects on the stats page
- Rename 'Pre-published' to 'Auto-publish' in division results copy
- Seed: vertical jump now scores in plain inches (points scheme) fixing a
  feet/inches mismatch; L-sit hold top tier raised to 3:00 per the
  CrossFit Journal standard
- Update lat.md capability and organizer-dashboard docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5MgTsR5FNrj7Tvu7T6Ebv
Review-round fixes from adversarial code review:
- Resolve the effective leaderboard scoring config through a helper that
  forces the online algorithm for benchmark boards, so a fresh benchmark
  with no stored scoringConfig no longer silently ranks 'traditional'
- Mirror the benchmark online force in the cohost scoring save path
- Move the 'Link events to tier thresholds' card to the top of the tiers
  page per the meeting's 'list of events up here'
- Treat emom as a time scheme for input-unit auto-selection
- Drop unused timeCapSeconds plumbing from benchmark event options
- Update lat.md docs for the read-time force and tier-linking UX

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R5MgTsR5FNrj7Tvu7T6Ebv
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds capability-based feature gating (cohosts, volunteer shifts, public volunteer signup) across sidebar UI and route loaders, forces benchmark competitions to always use the online scoring algorithm at save and read time, reworks benchmark test editor dialogs with event-linked prefill, removes the leaderboard affiliate column, updates benchmark seed thresholds, and renames "pre-publish" to "auto-publish" terminology.

Changes

Capability-Gated Feature Access

Layer / File(s) Summary
Capability registry
src/lib/competitions/capabilities.ts, test/lib/competitions/capabilities.test.ts
Adds cohosts, volunteerShifts, publicVolunteerSignup capabilities and per-type registry entries plus truth-table tests.
Sidebar gating
src/components/competition-sidebar.tsx, src/components/registration-sidebar.tsx, test/components/competition-sidebar-capability-gates.test.ts, test/components/registration-sidebar.test.tsx
Wraps Co-Hosts, Volunteer shifts, and volunteer signup UI in competitionCan checks with matching tests.
Route loader redirects
src/routes/compete/$slug/volunteer.tsx, .../organizer/$competitionId/co-hosts.tsx, .../organizer/$competitionId/volunteers/shifts.tsx
Loaders redirect when the competition type lacks the required capability.
Docs
lat.md/competition-type-capabilities.md
Documents new capability gates and effects.

Benchmark Online Scoring Lock

Layer / File(s) Summary
Form lock UI
src/components/compete/scoring-config-form.tsx, test/components/scoring-config-form.test.tsx
Adds lockAlgorithmOnline prop that hides the algorithm picker and shows fixed text.
Settings form and route wiring
.../-components/scoring-settings-form.tsx, .../cohost/$competitionId/scoring.tsx, .../scoring/index.tsx, .../settings.tsx
Adds forceOnlineAlgorithm and computes/passes lockAlgorithmOnline from capability checks.
Server enforcement
src/server-fns/cohost/cohost-competition-fns.ts, src/server-fns/competition-detail-fns.ts, src/server/competition-leaderboard.ts, test/server/competition-leaderboard-capability-gates.test.ts
Forces algorithm to "online" on save; adds resolveLeaderboardScoringConfig used in leaderboard reads.
Docs
lat.md/domain.md, lat.md/competition-type-capabilities.md
Describes benchmark leaderboards always using online scoring.

Benchmark Test Editor Dialogs & Event Linking

Layer / File(s) Summary
Contract updates
src/components/benchmark-tiers/test-editor-dialogs.tsx
Adds isTimeScheme helper and extends EventLinkOption/AddTestDialogProps.
AddTestDialog logic
test-editor-dialogs.tsx
Adds event-derived name tracking, handleSchemeChange, applyEventPrefill.
EditTestDialog logic
test-editor-dialogs.tsx
Updates scheme-change handling using isTimeScheme.
Event data plumbing & tier UI
src/server/benchmark-scoring-tiers.ts, src/components/events/event-details-form.tsx, .../scoring/tiers.tsx
Adds scheme/scoreType to event options and a new "Link events to tier thresholds" card.
Tests & docs
test/components/benchmark-test-editor-dialogs.test.tsx, test/server/benchmark-scoring-tiers.test.ts, lat.md/organizer-dashboard.md
Covers time-scheme prefill and event linkage; updates dashboard docs.

Leaderboard Affiliate Column Removal

Layer / File(s) Summary
Column removal
src/components/online-competition-leaderboard-table.tsx, test/components/online-competition-leaderboard-table.test.tsx, lat.md/organizer-dashboard.md
Removes Affiliate column and adjusts colspan/dependency logic; affiliate now renders as subtext.

Benchmark Seed Data Adjustments

Layer / File(s) Summary
Threshold updates
scripts/seed/data/benchmark-training-guide.ts, test/scripts/benchmark-seed-data.test.ts
Vertical Jump switches to points/inches scheme; L Sit Hold tier-10 changes to 03:00.

Auto-Publish Wording & Stats UI

Layer / File(s) Summary
Terminology rename
.../-components/quick-actions-division-results.tsx, src/server-fns/division-results-fns.ts, lat.md/organizer-dashboard.md
Renames "Pre-published" to "Auto-publish" in labels and messages.
Stats selector labels
src/routes/compete/$slug/stats.tsx, test/routes/compete/benchmark-stats-route.test.tsx
Adds labeled division/athlete select controls with tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • wodsmith/thewodapp#205: Related to the online scoring algorithm introduction that this PR now forces/persists for benchmark boards.
  • wodsmith/thewodapp#315: Both modify the leaderboard table's Affiliate column rendering logic.
  • wodsmith/thewodapp#422: Both modify the registration sidebar's "Sign up to volunteer" card visibility logic.

Poem

A rabbit hops through gated doors,
Online scores locked, forevermore.
Prefilled names from linked events fly,
Affiliate tucked beneath the eye.
Thump-thump-thump, the seeds now grow —
Twenty inches, three-minute hold! 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is specific and accurately summarizes the main benchmark gating, tier-linking, scoring lock, and seed/copy updates.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/benchmark-feature-refinements-7yzcfh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 45f6985f9e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}

// Public volunteer signup is capability-gated (benchmark boards exclude it)
if (!competitionCan(competition.competitionType, "publicVolunteerSignup")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce volunteer signup capability in the mutations

For benchmark boards this loader hides /volunteer, but the actual write paths (submitVolunteerSignupFn and especially the anonymous createAccountAndApplyAsVolunteerFn) still accept only competitionTeamId and create the volunteer invitation without resolving the competition type. A stale client or direct server-function call can therefore still create public volunteer applications for benchmark competitions, bypassing the new publicVolunteerSignup gate; please enforce the same capability check in the mutation/shared helper before creating the application.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (5)
apps/wodsmith-start/test/scripts/benchmark-seed-data.test.ts (1)

139-176: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a // @lat: reference beside this test.
This benchmark seed-data test should carry the matching spec link inline to follow the test annotation rule.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/wodsmith-start/test/scripts/benchmark-seed-data.test.ts` around lines
139 - 176, Add the required inline // `@lat`: reference to this benchmark
seed-data test so it satisfies the test annotation rule. Update the `it("scores
vertical jump in plain inches and tops the L-sit at three minutes", ...)` block
in `benchmark-seed-data.test.ts` and place the matching spec link beside the
test declaration or its leading comment, keeping the rest of the assertions and
helpers like `buildBenchmarkSeedRows` and `BENCHMARK_SEED_TESTS` unchanged.

Source: Coding guidelines

apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-components/scoring-settings-form.tsx (1)

30-43: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

LGTM!

The three-layer defense (init coercion, save re-force, server enforcement) is solid. One minor consistency note: forceOnlineAlgorithm early-returns when algorithm === "online" without clearing customTable, while the server-side enforcement in updateCompetitionScoringConfigFn always clears it. In practice this is harmless since handleAlgorithmChange already clears customTable when switching to a non-custom algorithm, but you could align them if desired:

Optional: align forceOnlineAlgorithm with server behavior
 function forceOnlineAlgorithm(config: ScoringConfig): ScoringConfig {
-  if (config.algorithm === "online") return config
-  return { ...config, algorithm: "online", customTable: undefined }
+  if (config.algorithm === "online" && !config.customTable) return config
+  return { ...config, algorithm: "online", customTable: undefined }
 }

Also applies to: 72-99, 124-124

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-components/scoring-settings-form.tsx
around lines 30 - 43, `forceOnlineAlgorithm` is slightly inconsistent with
`updateCompetitionScoringConfigFn` because it returns the config unchanged when
`algorithm` is already "online", leaving any stale `customTable` intact. Update
`forceOnlineAlgorithm` in `scoring-settings-form.tsx` to always normalize the
config the same way the server does by clearing `customTable` whenever the
online algorithm is enforced, and keep the save-path coercion and
`handleAlgorithmChange` behavior aligned with that rule.
apps/wodsmith-start/src/routes/compete/cohost/$competitionId/scoring.tsx (1)

79-82: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider centralizing the lock-algorithm capability check.

competitionCan(competition.competitionType, "benchmarkScoringTiers") is duplicated identically in this file, scoring/index.tsx, and settings.tsx. Extracting a small helper (e.g. isScoringAlgorithmLocked(type) in capabilities.ts) would give a single place to update if the gating capability ever changes.

♻️ Proposed helper
+// apps/wodsmith-start/src/lib/competitions/capabilities.ts
+export function isScoringAlgorithmLocked(type: string): boolean {
+  return competitionCan(type, "benchmarkScoringTiers")
+}
-        lockAlgorithmOnline={competitionCan(
-          competition.competitionType,
-          "benchmarkScoringTiers",
-        )}
+        lockAlgorithmOnline={isScoringAlgorithmLocked(competition.competitionType)}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/scoring.tsx
around lines 79 - 82, The lock-algorithm capability check is duplicated across
scoring-related routes, so centralize it behind a shared helper instead of
repeating competitionCan(competition.competitionType, "benchmarkScoringTiers").
Add a small function such as isScoringAlgorithmLocked(type) in capabilities.ts
and use it from scoring.tsx, scoring/index.tsx, and settings.tsx so the gating
logic lives in one place and is easy to update.
apps/wodsmith-start/src/server/competition-leaderboard.ts (1)

263-279: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer a top-level type import over inline import(...).

ScoringConfig is almost certainly already used/imported elsewhere in this file (e.g. via calculateEventPoints, TiebreakerInput.scoringAlgorithm). Using import("@/types/scoring").ScoringConfig inline here is inconsistent with normal import style.

✨ Proposed cleanup
+import type { ScoringConfig } from "`@/types/scoring`"
...
 export function resolveLeaderboardScoringConfig(params: {
   competitionType: string
   settings: CompetitionSettings | null | undefined
-}): import("`@/types/scoring`").ScoringConfig {
+}): ScoringConfig {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/wodsmith-start/src/server/competition-leaderboard.ts` around lines 263 -
279, The return type in resolveLeaderboardScoringConfig uses an inline import
type, which is inconsistent with the rest of the file’s import style. Add a
top-level ScoringConfig type import from "`@/types/scoring`" alongside the
existing scoring-related imports, then update resolveLeaderboardScoringConfig to
reference that imported type directly instead of
import("`@/types/scoring`").ScoringConfig.
apps/wodsmith-start/src/server-fns/cohost/cohost-competition-fns.ts (1)

408-423: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Force-online save logic is correct; add unit coverage for this path.

The override (algorithm: "online", customTable: undefined) correctly mirrors resolveLeaderboardScoringConfig's read-time override and preserves tiebreaker/statusHandling. However, only competition-leaderboard-capability-gates.test.ts (read path) was added in this cohort — there's no test exercising cohostUpdateScoringConfigFn forcing the algorithm on save for a benchmark competition, or leaving it untouched for non-benchmark types.

Want me to draft a test for cohostUpdateScoringConfigFn covering both branches?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/wodsmith-start/src/server-fns/cohost/cohost-competition-fns.ts` around
lines 408 - 423, Add unit coverage for the save-time override in
cohostUpdateScoringConfigFn: verify that when competitionCan(...,
"benchmarkScoringTiers") is true, the saved scoringConfig forces algorithm to
"online" and clears customTable while preserving the other fields, and verify
that non-benchmark competitions pass data.scoringConfig through unchanged. Use
the existing resolveLeaderboardScoringConfig/read-path test behavior as the
reference for the expected benchmark branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/wodsmith-start/src/routes/compete/cohost/`$competitionId/scoring.tsx:
- Around line 79-82: The lock-algorithm capability check is duplicated across
scoring-related routes, so centralize it behind a shared helper instead of
repeating competitionCan(competition.competitionType, "benchmarkScoringTiers").
Add a small function such as isScoringAlgorithmLocked(type) in capabilities.ts
and use it from scoring.tsx, scoring/index.tsx, and settings.tsx so the gating
logic lives in one place and is easy to update.

In
`@apps/wodsmith-start/src/routes/compete/organizer/`$competitionId/-components/scoring-settings-form.tsx:
- Around line 30-43: `forceOnlineAlgorithm` is slightly inconsistent with
`updateCompetitionScoringConfigFn` because it returns the config unchanged when
`algorithm` is already "online", leaving any stale `customTable` intact. Update
`forceOnlineAlgorithm` in `scoring-settings-form.tsx` to always normalize the
config the same way the server does by clearing `customTable` whenever the
online algorithm is enforced, and keep the save-path coercion and
`handleAlgorithmChange` behavior aligned with that rule.

In `@apps/wodsmith-start/src/server-fns/cohost/cohost-competition-fns.ts`:
- Around line 408-423: Add unit coverage for the save-time override in
cohostUpdateScoringConfigFn: verify that when competitionCan(...,
"benchmarkScoringTiers") is true, the saved scoringConfig forces algorithm to
"online" and clears customTable while preserving the other fields, and verify
that non-benchmark competitions pass data.scoringConfig through unchanged. Use
the existing resolveLeaderboardScoringConfig/read-path test behavior as the
reference for the expected benchmark branch.

In `@apps/wodsmith-start/src/server/competition-leaderboard.ts`:
- Around line 263-279: The return type in resolveLeaderboardScoringConfig uses
an inline import type, which is inconsistent with the rest of the file’s import
style. Add a top-level ScoringConfig type import from "`@/types/scoring`"
alongside the existing scoring-related imports, then update
resolveLeaderboardScoringConfig to reference that imported type directly instead
of import("`@/types/scoring`").ScoringConfig.

In `@apps/wodsmith-start/test/scripts/benchmark-seed-data.test.ts`:
- Around line 139-176: Add the required inline // `@lat`: reference to this
benchmark seed-data test so it satisfies the test annotation rule. Update the
`it("scores vertical jump in plain inches and tops the L-sit at three minutes",
...)` block in `benchmark-seed-data.test.ts` and place the matching spec link
beside the test declaration or its leading comment, keeping the rest of the
assertions and helpers like `buildBenchmarkSeedRows` and `BENCHMARK_SEED_TESTS`
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 069c0960-5c71-4fab-b7e0-a1005ceec5a8

📥 Commits

Reviewing files that changed from the base of the PR and between 6a60302 and 45f6985.

📒 Files selected for processing (36)
  • apps/wodsmith-start/scripts/seed/data/benchmark-training-guide.ts
  • apps/wodsmith-start/src/components/benchmark-tiers/test-editor-dialogs.tsx
  • apps/wodsmith-start/src/components/compete/scoring-config-form.tsx
  • apps/wodsmith-start/src/components/competition-sidebar.tsx
  • apps/wodsmith-start/src/components/events/event-details-form.tsx
  • apps/wodsmith-start/src/components/online-competition-leaderboard-table.tsx
  • apps/wodsmith-start/src/components/registration-sidebar.tsx
  • apps/wodsmith-start/src/lib/competitions/capabilities.ts
  • apps/wodsmith-start/src/routes/compete/$slug/stats.tsx
  • apps/wodsmith-start/src/routes/compete/$slug/volunteer.tsx
  • apps/wodsmith-start/src/routes/compete/cohost/$competitionId/scoring.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-components/quick-actions-division-results.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/-components/scoring-settings-form.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/co-hosts.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/scoring/index.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/scoring/tiers.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/settings.tsx
  • apps/wodsmith-start/src/routes/compete/organizer/$competitionId/volunteers/shifts.tsx
  • apps/wodsmith-start/src/server-fns/cohost/cohost-competition-fns.ts
  • apps/wodsmith-start/src/server-fns/competition-detail-fns.ts
  • apps/wodsmith-start/src/server-fns/division-results-fns.ts
  • apps/wodsmith-start/src/server/benchmark-scoring-tiers.ts
  • apps/wodsmith-start/src/server/competition-leaderboard.ts
  • apps/wodsmith-start/test/components/benchmark-test-editor-dialogs.test.tsx
  • apps/wodsmith-start/test/components/competition-sidebar-capability-gates.test.ts
  • apps/wodsmith-start/test/components/online-competition-leaderboard-table.test.tsx
  • apps/wodsmith-start/test/components/registration-sidebar.test.tsx
  • apps/wodsmith-start/test/components/scoring-config-form.test.tsx
  • apps/wodsmith-start/test/lib/competitions/capabilities.test.ts
  • apps/wodsmith-start/test/routes/compete/benchmark-stats-route.test.tsx
  • apps/wodsmith-start/test/scripts/benchmark-seed-data.test.ts
  • apps/wodsmith-start/test/server/benchmark-scoring-tiers.test.ts
  • apps/wodsmith-start/test/server/competition-leaderboard-capability-gates.test.ts
  • lat.md/competition-type-capabilities.md
  • lat.md/domain.md
  • lat.md/organizer-dashboard.md

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

6 issues found across 36 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/wodsmith-start/src/components/benchmark-tiers/test-editor-dialogs.tsx">

<violation number="1" location="apps/wodsmith-start/src/components/benchmark-tiers/test-editor-dialogs.tsx:488">
P2: Time-based benchmark tests can still be saved with a non-time input unit: `handleSchemeChange` only sets `inputUnit` when the scheme changes, but the Input unit selector remains editable afterward. Consider enforcing `isTimeScheme(scheme) ? "time" : inputUnit` at save/update time or disabling/filtering the unit selector for time schemes.</violation>
</file>

<file name="apps/wodsmith-start/src/components/registration-sidebar.tsx">

<violation number="1" location="apps/wodsmith-start/src/components/registration-sidebar.tsx:395">
P2: Benchmark pages can still show volunteer dashboard/check-in UI for users with an existing volunteer team role because the new capability guard only wraps the non-volunteer signup card. Consider gating the `isVolunteer` dashboard/check-in block with the relevant volunteer/check-in capabilities too, so benchmark pages consistently hide volunteer surfaces.</violation>
</file>

<file name="apps/wodsmith-start/src/routes/compete/organizer/$competitionId/scoring/tiers.tsx">

<violation number="1" location="apps/wodsmith-start/src/routes/compete/organizer/$competitionId/scoring/tiers.tsx:350">
P2: The new event-link card only pre-fills scheme and score type, so adding tiers from a load/distance/calories event can still create the benchmark test with the default input unit (`reps`) and default category. Consider passing the event-derived category and input unit through `EventLinkOption` and applying them in `AddTestDialog` so the top-of-page workflow matches the event data it is linking.</violation>
</file>

<file name="apps/wodsmith-start/src/server/benchmark-scoring-tiers.ts">

<violation number="1" location="apps/wodsmith-start/src/server/benchmark-scoring-tiers.ts:539">
P2: Events whose workout has no explicit `scoreType` can prefill new benchmark tests with the dialog default `max`, even for time-based schemes that should use the scheme default. Consider normalizing nullable workout `scoreType` to the same default used by score submission before exposing it for event-derived prefill.</violation>
</file>

<file name="apps/wodsmith-start/src/components/compete/scoring-config-form.tsx">

<violation number="1" location="apps/wodsmith-start/src/components/compete/scoring-config-form.tsx:249">
P3: `ScoringConfigForm` now advertises `lockAlgorithmOnline` as hard-coding Online scoring, but inside this component it only hides the picker; a direct caller with a non-online `value.algorithm` would still show/edit the non-online preview and emit non-online config changes. Consider enforcing an effective online config in this component, or narrowing the prop name/contract to “hide algorithm picker.”</violation>
</file>

<file name="apps/wodsmith-start/src/routes/compete/$slug/volunteer.tsx">

<violation number="1" location="apps/wodsmith-start/src/routes/compete/$slug/volunteer.tsx:18">
P2: This loader-level redirect prevents navigation to the volunteer page for benchmark boards, but the underlying mutations (`submitVolunteerSignupFn`, `createAccountAndApplyAsVolunteerFn`) still accept a `competitionTeamId` and create volunteer applications without verifying the competition type supports public volunteer signup. A direct server-function call or stale client state could bypass this gate and create volunteer applications for benchmark competitions. Consider adding the same `competitionCan(competitionType, "publicVolunteerSignup")` check inside those mutations before creating the application.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

const handleSchemeChange = (value: string) => {
setScheme(value)
if (isTimeScheme(value)) {
setInputUnit("time")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Time-based benchmark tests can still be saved with a non-time input unit: handleSchemeChange only sets inputUnit when the scheme changes, but the Input unit selector remains editable afterward. Consider enforcing isTimeScheme(scheme) ? "time" : inputUnit at save/update time or disabling/filtering the unit selector for time schemes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/components/benchmark-tiers/test-editor-dialogs.tsx, line 488:

<comment>Time-based benchmark tests can still be saved with a non-time input unit: `handleSchemeChange` only sets `inputUnit` when the scheme changes, but the Input unit selector remains editable afterward. Consider enforcing `isTimeScheme(scheme) ? "time" : inputUnit` at save/update time or disabling/filtering the unit selector for time schemes.</comment>

<file context>
@@ -462,10 +477,49 @@ export function AddTestDialog({
+  const handleSchemeChange = (value: string) => {
+    setScheme(value)
+    if (isTimeScheme(value)) {
+      setInputUnit("time")
+    } else {
+      setInputUnit((current) =>
</file context>

{!isVolunteer &&
competitionCan(
competition.competitionType,
"publicVolunteerSignup",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Benchmark pages can still show volunteer dashboard/check-in UI for users with an existing volunteer team role because the new capability guard only wraps the non-volunteer signup card. Consider gating the isVolunteer dashboard/check-in block with the relevant volunteer/check-in capabilities too, so benchmark pages consistently hide volunteer surfaces.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/components/registration-sidebar.tsx, line 395:

<comment>Benchmark pages can still show volunteer dashboard/check-in UI for users with an existing volunteer team role because the new capability guard only wraps the non-volunteer signup card. Consider gating the `isVolunteer` dashboard/check-in block with the relevant volunteer/check-in capabilities too, so benchmark pages consistently hide volunteer surfaces.</comment>

<file context>
@@ -309,96 +310,104 @@ export function RegistrationSidebar({
+      {!isVolunteer &&
+        competitionCan(
+          competition.competitionType,
+          "publicVolunteerSignup",
+        ) && (
+          <Card className="border-white/10 bg-white/5 backdrop-blur-md">
</file context>

name: event.eventName,
linkedTestName: event.linkedTestName,
scheme: event.scheme,
scoreType: event.scoreType,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new event-link card only pre-fills scheme and score type, so adding tiers from a load/distance/calories event can still create the benchmark test with the default input unit (reps) and default category. Consider passing the event-derived category and input unit through EventLinkOption and applying them in AddTestDialog so the top-of-page workflow matches the event data it is linking.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/routes/compete/organizer/$competitionId/scoring/tiers.tsx, line 350:

<comment>The new event-link card only pre-fills scheme and score type, so adding tiers from a load/distance/calories event can still create the benchmark test with the default input unit (`reps`) and default category. Consider passing the event-derived category and input unit through `EventLinkOption` and applying them in `AddTestDialog` so the top-of-page workflow matches the event data it is linking.</comment>

<file context>
@@ -341,6 +342,16 @@ function BenchmarkScoringTiersEditor({
+    name: event.eventName,
+    linkedTestName: event.linkedTestName,
+    scheme: event.scheme,
+    scoreType: event.scoreType,
+  }))
+  const unlinkedEvents = summary.events.filter(
</file context>

? (testNameById.get(row.benchmarkTestId) ?? null)
: null,
scheme: row.scheme,
scoreType: row.scoreType,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Events whose workout has no explicit scoreType can prefill new benchmark tests with the dialog default max, even for time-based schemes that should use the scheme default. Consider normalizing nullable workout scoreType to the same default used by score submission before exposing it for event-derived prefill.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/server/benchmark-scoring-tiers.ts, line 539:

<comment>Events whose workout has no explicit `scoreType` can prefill new benchmark tests with the dialog default `max`, even for time-based schemes that should use the scheme default. Consider normalizing nullable workout `scoreType` to the same default used by score submission before exposing it for event-derived prefill.</comment>

<file context>
@@ -514,21 +512,32 @@ export async function loadBenchmarkEventTestOptions({
+      ? (testNameById.get(row.benchmarkTestId) ?? null)
+      : null,
+    scheme: row.scheme,
+    scoreType: row.scoreType,
+  }
 }
</file context>

}

// Public volunteer signup is capability-gated (benchmark boards exclude it)
if (!competitionCan(competition.competitionType, "publicVolunteerSignup")) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This loader-level redirect prevents navigation to the volunteer page for benchmark boards, but the underlying mutations (submitVolunteerSignupFn, createAccountAndApplyAsVolunteerFn) still accept a competitionTeamId and create volunteer applications without verifying the competition type supports public volunteer signup. A direct server-function call or stale client state could bypass this gate and create volunteer applications for benchmark competitions. Consider adding the same competitionCan(competitionType, "publicVolunteerSignup") check inside those mutations before creating the application.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/routes/compete/$slug/volunteer.tsx, line 18:

<comment>This loader-level redirect prevents navigation to the volunteer page for benchmark boards, but the underlying mutations (`submitVolunteerSignupFn`, `createAccountAndApplyAsVolunteerFn`) still accept a `competitionTeamId` and create volunteer applications without verifying the competition type supports public volunteer signup. A direct server-function call or stale client state could bypass this gate and create volunteer applications for benchmark competitions. Consider adding the same `competitionCan(competitionType, "publicVolunteerSignup")` check inside those mutations before creating the application.</comment>

<file context>
@@ -13,6 +14,11 @@ export const Route = createFileRoute("/compete/$slug/volunteer")({
     }
 
+    // Public volunteer signup is capability-gated (benchmark boards exclude it)
+    if (!competitionCan(competition.competitionType, "publicVolunteerSignup")) {
+      throw redirect({ to: "/compete/$slug", params: { slug: params.slug } })
+    }
</file context>

* Hard-codes the ranking algorithm to "online" (benchmark boards): hides
* the algorithm picker while keeping tiebreaker and DNF/DNS settings.
*/
lockAlgorithmOnline?: boolean

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: ScoringConfigForm now advertises lockAlgorithmOnline as hard-coding Online scoring, but inside this component it only hides the picker; a direct caller with a non-online value.algorithm would still show/edit the non-online preview and emit non-online config changes. Consider enforcing an effective online config in this component, or narrowing the prop name/contract to “hide algorithm picker.”

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/wodsmith-start/src/components/compete/scoring-config-form.tsx, line 249:

<comment>`ScoringConfigForm` now advertises `lockAlgorithmOnline` as hard-coding Online scoring, but inside this component it only hides the picker; a direct caller with a non-online `value.algorithm` would still show/edit the non-online preview and emit non-online config changes. Consider enforcing an effective online config in this component, or narrowing the prop name/contract to “hide algorithm picker.”</comment>

<file context>
@@ -242,6 +242,11 @@ interface ScoringConfigFormProps {
+   * Hard-codes the ranking algorithm to "online" (benchmark boards): hides
+   * the algorithm picker while keeping tiebreaker and DNF/DNS settings.
+   */
+  lockAlgorithmOnline?: boolean
 }
 
</file context>

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.

2 participants