feat(scoring): carry the reason for a result through to the record - #1474
Merged
Merged
Conversation
The scoring modal can now record WHY a match ended — "Ret [inj]" rather than just RETIRED — when the tournament has a scoring policy that says what the reasons are. This is the TMX half: resolve that vocabulary and forward the chosen code into the mutation. The engine has accepted `outcome.matchUpStatusCodes` for a long time; it reaches `modifyMatchUpScore` untouched. TMX was the one place that dropped it, building its outcome without the field, so nothing a modal offered could ever have landed. The vocabulary is resolved per EVENT, since a scoring policy can be attached at that level, and is read through `findPolicy` — the same call the draw action options already use. Nothing is bundled: TMX ships no built-in vocabulary by design (CA, 2026-09-20), so the reason control appears only where a governing body's policy is attached and is absent everywhere else. `undefined` here is the ordinary case, not a failure to defend against. The code is forwarded only when one was actually chosen. The factory reads an EMPTY `matchUpStatusCodes` array as an instruction to blank the codes, so sending `[]` to mean "no reason given" would erase what a previous edit recorded. DEPENDS ON UNPUBLISHED WORK — do not open this as a PR yet. It needs the courthive-components status-code picker, which is merged into neither main nor a release. TMX pins courthive-components 4.4.0, which is also the latest published version, and CI strips the link: overrides and installs exactly that — so check-types would fail in CI even though it passes locally against the linked sibling. The order is: merge the components picker, publish components, bump the pin here, then open this. Local gate, all against the LINKED sibling and stated as such: lint, format:check, attr-audit --ci, i18n-audit --ci, check-types all exit 0; 183 test files / 2005 tests pass.
4.5.0 is the release carrying the status-code picker this branch depends on. Until it existed, CI could not type-check the commit below: TMX CI strips the `link:` overrides and resolves the PUBLISHED package, and the previous pin — 4.4.0, which was also the latest published — knew nothing of `ScoringModalParams.matchUpStatusCodes` or the `StatusCodeGroups` type. `pnpm-lock.yaml` is deliberately unchanged. The lockfile records `courthive-components` as `link:../courthive-components` because the workspace override wins locally; CI strips that line and re-resolves this one entry from the pin above, so the pin is the load-bearing value and the lockfile has nothing to say about it. Verified against the published resolution rather than the linked sibling — overrides stripped, `pnpm install --no-frozen-lockfile`, components resolving to 4.5.0 from npm: check-types, lint, and 183 test files / 2005 tests all pass. The workspace file was then restored byte-identical and the sibling symlink is back.
…hout
Journey 126 drives the real modal end to end and asserts the chosen reason
reaches `matchUp.matchUpStatusCodes`. Every layer beneath that assertion could
be green while the code was dropped in transit — the engine has accepted
`outcome.matchUpStatusCodes` for a long time, and TMX was the one place that
never sent it, which is exactly the kind of gap unit tests cannot see.
BOTH halves are asserted, and the negative one is not a formality. TMX ships no
built-in vocabulary by design, so "no reason control" is the state nearly every
tournament is in — and it is the state most likely to become permanent by
accident, because a resolver that quietly returns undefined is indistinguishable
from a tournament with no policy attached. So: without a policy the control must
be absent and the result must still submit; with one attached the control must
offer the policy's own codes and the chosen code must land on the record.
The journey enters a partial set before marking the retirement, which is both
what a retirement actually looks like and what REVEALS the irregular-ending
controls — dynamicSets keeps them hidden until a score is in progress. The
radios are in the DOM from the start but not visible, so waiting only for them
to be attached would have waited on a control the operator cannot click.
Also adds a mock tournament, "Reason Codes Invitational", carrying the same
policy. It is the only mock with a reason vocabulary attached, and it exists so
the control can be found by hand rather than only by Playwright.
`completeAllMatchUps` is deliberately off: a tournament with everything already
scored has nothing to demonstrate the control on.
Both take the vocabulary from `fixtures.policies.POLICY_SCORING_USTA` rather
than restating it. A hand-copied vocabulary is how the scoring editor and the
factory fixture drifted apart while both looked correct, and a test asserting
against its own copy asserts nothing about the real one.
`enterMatchUpScore` joins the dev bridge for the same reason `fetchUserContext`
is there — exposing the real entry point rather than a test-only stand-in keeps
the journey on the production path, including the policy lookup that decides
whether the control exists at all.
Verified by falsification, not just by passing: removing the one line in
scoreMatchUp.ts that forwards the codes turns the positive test red at exactly
`expect(codes).toContain('RJ')`, while the negative test correctly stays green.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The scoring modal can now record why a match ended —
Ret [inj]rather than justRETIRED— and TMX carries that through to the record.The gap this closes
The engine has accepted
outcome.matchUpStatusCodesfor a long time: it reachesmodifyMatchUpScoreuntouched. TMX was the one place that dropped it, building its outcome without the field, so nothing a modal offered could ever have landed.How the vocabulary is resolved
Per event, since a scoring policy can be attached at that level, and read through
findPolicy— the same callrenderDraws/getActionOptions.tsalready uses.Nothing is bundled. TMX ships no built-in vocabulary by design (CA, 2026-09-20): the reason control appears only where a governing body's policy is attached, and is absent everywhere else.
undefinedhere is the ordinary case, not a failure to defend against.One detail that is a decision
The code is forwarded only when one was actually chosen. The factory reads an empty
matchUpStatusCodesarray as an instruction to blank the codes — so sending[]to mean "no reason given" would erase what a previous edit recorded.Verified against the published resolution, not the linked sibling
This is a cross-repo boundary change, so a local green against
link:../courthive-componentswould have proved nothing. Reproduced CI's resolution instead — overrides stripped,pnpm install --no-frozen-lockfile,courthive-componentsresolving to 4.5.0 from npm:check-typeslinttestThe workspace file was then restored byte-identical and the sibling symlink is back.
Full gate also green against the linked sibling:
lint/format:check/attr-audit --ci/i18n-audit --ci/check-types/testall 0.The pin bump is load-bearing; the lockfile is not
pnpm-lock.yamlis deliberately unchanged. It recordscourthive-componentsaslink:../courthive-componentsbecause the workspace override wins locally; CI strips that line and re-resolves this one entry from thepackage.jsonpin. 4.4.0 — the previous pin, and also the previously-latest published version — knew nothing ofScoringModalParams.matchUpStatusCodesorStatusCodeGroups.Completes the series
courthive-components#571 (explicit double exits), #573 (the six policy endings), #574 (the reason-code picker) — all merged and released as 4.5.0.Acceptance test included
Journey 126 drives the real modal end to end and asserts the chosen reason reaches
matchUp.matchUpStatusCodes. Both halves are asserted, and the negative one is not a formality — "no reason control" is the state nearly every tournament is in, and a resolver that quietly returnsundefinedis indistinguishable from a tournament with no policy attached.RJlands on the recordFalsified rather than assumed: removing the single line in
scoreMatchUp.tsthat forwards the codes turns the positive test red at exactlyexpect(codes).toContain('RJ'), while the negative test correctly stays green.Also adds a mock tournament, "Reason Codes Invitational" — the only mock carrying a reason vocabulary — so the control can be found by hand and not only by Playwright.
completeAllMatchUpsis deliberately off: a tournament with everything already scored has nothing to demonstrate it on.Both take the vocabulary from
fixtures.policies.POLICY_SCORING_USTArather than restating it; a test asserting against its own copy asserts nothing about the real one.enterMatchUpScorejoins the dev bridge for the same reasonfetchUserContextis there: exposing the real entry point keeps the journey on the production path, including the policy lookup that decides whether the control exists at all.Journey 126 passes locally (2/2). Note TMX PR CI does not run the journey suite —
electron.ymlscopes Playwright to the desktop smoke — so that number is local-only evidence, as the standards require me to say.