Fix the asset guard and establish the odds router boundary #27

Open
kristofferopsahl wants to merge 2 commits from fix/issue-22 into main
Collaborator

Outcome

  • repairs the asset-version guard currently failing on main: version 135 recorded a digest for frontend files that were not committed, so the clean checkout suite was red
  • introduces backend/web/routes/odds.py and makes it own FastAPI registration for /api/market and /api/matches
  • preserves the existing scorer/market service functions and their direct-import characterization seam while removing endpoint decorators from main.py

This advances #22 but does not close it: moving the remaining calculation body behind the existing odds/load service boundary should be reviewed as the next mechanical step.

Verification

  • full python -m pytest -q suite passes
  • focused odds/market/API characterization: 132 passed, 4 skipped
## Outcome - repairs the asset-version guard currently failing on `main`: version 135 recorded a digest for frontend files that were not committed, so the clean checkout suite was red - introduces `backend/web/routes/odds.py` and makes it own FastAPI registration for `/api/market` and `/api/matches` - preserves the existing scorer/market service functions and their direct-import characterization seam while removing endpoint decorators from `main.py` This advances #22 but does not close it: moving the remaining calculation body behind the existing odds/load service boundary should be reviewed as the next mechanical step. ## Verification - full `python -m pytest -q` suite passes - focused odds/market/API characterization: 132 passed, 4 skipped
refactor: register odds endpoints through a domain router (#22)
Some checks failed
CI / syntax (pull_request) Failing after 1s
CI / tests (pull_request) Has been skipped
CI / wheel (pull_request) Has been skipped
CI / browser (pull_request) Has been skipped
89eba0d34f
Owner

Reviewed. Your diagnosis was right, and so was ours — that is the actual bug here, and neither side could see it.

test_asset_version.py hashes the four assets byte-for-byte. They are text files, the repo has no .gitattributes, and CI checks out LF on debian-trixie-python while a Windows clone with core.autocrlf=true holds CRLF. So the digest differs by platform, and the guard is green only for whoever recorded it last.

Measured at each commit, the same content under both conventions:

commit recorded LF (CI) CRLF (Windows) CI Windows
1c94db7 (ours, v135) d20d1fa11d2d9a94 a932c205c61efb9b d20d1fa11d2d9a94 RED green
bbf3c01 (yours, v136) a932c205c61efb9b a932c205c61efb9b d20d1fa11d2d9a94 green RED
9c2faca (ours, v137) d9ca43b746e10252 350840c64b1a1aa0 d9ca43b746e10252 RED green

Two things follow.

main has been red in CI since 9c2faca, exactly as this PR says. We could not see it because we only ever ran the suite on Windows, where it passes.

Your bbf3c01 is not wrong — it is correct for Linux. We would have "fixed" it straight back to a CRLF digest and broken CI again, which is the third leg of a ping-pong that has already run 135 → 136 → 137. Apologies for the round trip; the conflict on this PR is that ping-pong, not a disagreement about the code.

What we changed instead

Opened as #28.

Branch fix/asset-guard-line-endings normalises line endings before hashing, so the platform difference is unrepresentable rather than documented:

h.update((FRONTEND / name).read_bytes().replace(b"\r\n", b"\n"))

EXPECTED_DIGEST becomes the normalised value, which is identical under both conventions. This is not a new approach — the repo had already solved it one digest over. projection_digest in pipeline.py normalises `

to
and explains why in terms that describe this bug exactly: *"git stash/checkoutrewrite tracked files between LF and CRLF undercore.autocrlf=true, and on Windows that flipped this digest with zero code difference: same source, two different hashes, one commit apart."* It is covered by test_crlf_and_lf_produce_the_same_digest`. The asset digest is simply the one place that remedy was never applied.

We chose this over a .gitattributes entry deliberately: .gitattributes would leave the guard's correctness depending on every contributor's checkout config, which is the disconnected-mechanism shape this repo keeps paying for.

Proved RED on both the property fixed and the one that must not break:

case result
CRLF checkout (Windows) GREEN
LF checkout (CI debian-trixie) GREEN
app.js changed, ?v= not bumped RED
tokens.css changed, ?v= not bumped RED

Could you rebase on that branch once it lands? bbf3c01 then becomes unnecessary rather than conflicting, and neither side has to concede a digest that was correct where it was measured.

One note on the verification line in the description — "full python -m pytest -q suite passes". It does, on Linux. On Windows this PR fails test_the_version_moved_when_the_assets_did. Not a criticism of the run; it is the same platform split, seen from the other side. Worth stating because it is the second time in this repo a green suite has meant "green where I ran it" (the earlier one was test_css_characterization at a single viewport width).

On the router half (#22)

Held back from this merge, and we would rather not take it as-is:

  • main.py goes 2105 → 2110 lines. #22 is a child of #7, "split oversized modules", and the module grew.
  • The bodies did not move: market_response (51 lines) and matches_response (284) are still in main.py. Only the two @app.get decorators left.
  • The dependency is now a cycle — main.pyroutes/odds.py → (lazily) main.py — deferred to call time by the function-scoped import. That has to be unwound by the next step rather than built on.

Against the issue's own wording, "main.py no longer owns these handlers", this moves registration rather than ownership. The description is honest that it "advances #22 but does not close it", so this is not a disagreement about intent — we would just rather the first commit on this boundary move the calculation body behind the existing odds/load service seam, so the router is thin because the logic is elsewhere, not because it delegates back.

Also flagging an ordering point from #25 that we should settle before more of this ladder lands: the asset guard currently works because there are exactly four files and one version string, and splitting app.js into modules breaks both halves at once. Worth confirming the guard rebuild comes first, as agreed there.

Reviewed. **Your diagnosis was right, and so was ours — that is the actual bug here, and neither side could see it.** `test_asset_version.py` hashes the four assets byte-for-byte. They are text files, the repo has no `.gitattributes`, and CI checks out LF on `debian-trixie-python` while a Windows clone with `core.autocrlf=true` holds CRLF. So the digest differs by platform, and the guard is green only for whoever recorded it last. Measured at each commit, the same content under both conventions: | commit | recorded | LF (CI) | CRLF (Windows) | CI | Windows | |---|---|---|---|---|---| | `1c94db7` (ours, v135) | `d20d1fa11d2d9a94` | `a932c205c61efb9b` | `d20d1fa11d2d9a94` | **RED** | green | | `bbf3c01` (yours, v136) | `a932c205c61efb9b` | `a932c205c61efb9b` | `d20d1fa11d2d9a94` | green | **RED** | | `9c2faca` (ours, v137) | `d9ca43b746e10252` | `350840c64b1a1aa0` | `d9ca43b746e10252` | **RED** | green | Two things follow. **`main` has been red in CI since `9c2faca`**, exactly as this PR says. We could not see it because we only ever ran the suite on Windows, where it passes. **Your `bbf3c01` is not wrong** — it is correct for Linux. We would have "fixed" it straight back to a CRLF digest and broken CI again, which is the third leg of a ping-pong that has already run 135 → 136 → 137. Apologies for the round trip; the conflict on this PR is that ping-pong, not a disagreement about the code. ### What we changed instead Opened as **#28**. Branch `fix/asset-guard-line-endings` normalises line endings before hashing, so the platform difference is unrepresentable rather than documented: ```python h.update((FRONTEND / name).read_bytes().replace(b"\r\n", b"\n")) ``` `EXPECTED_DIGEST` becomes the normalised value, which is identical under both conventions. **This is not a new approach — the repo had already solved it one digest over.** `projection_digest` in `pipeline.py` normalises ` ` to ` ` and explains why in terms that describe this bug exactly: *"`git stash`/`checkout` rewrite tracked files between LF and CRLF under `core.autocrlf=true`, and on Windows that flipped this digest with zero code difference: same source, two different hashes, one commit apart."* It is covered by `test_crlf_and_lf_produce_the_same_digest`. The asset digest is simply the one place that remedy was never applied. We chose this over a `.gitattributes` entry deliberately: `.gitattributes` would leave the guard's correctness depending on every contributor's checkout config, which is the disconnected-mechanism shape this repo keeps paying for. Proved RED on both the property fixed and the one that must not break: | case | result | |---|---| | CRLF checkout (Windows) | GREEN | | LF checkout (CI debian-trixie) | GREEN | | `app.js` changed, `?v=` not bumped | **RED** | | `tokens.css` changed, `?v=` not bumped | **RED** | **Could you rebase on that branch once it lands?** `bbf3c01` then becomes unnecessary rather than conflicting, and neither side has to concede a digest that was correct where it was measured. One note on the verification line in the description — "full `python -m pytest -q` suite passes". It does, on Linux. On Windows this PR fails `test_the_version_moved_when_the_assets_did`. Not a criticism of the run; it is the same platform split, seen from the other side. Worth stating because it is the second time in this repo a green suite has meant "green where I ran it" (the earlier one was `test_css_characterization` at a single viewport width). ### On the router half (#22) Held back from this merge, and we would rather not take it as-is: - `main.py` goes **2105 → 2110 lines**. #22 is a child of #7, "split oversized modules", and the module grew. - The bodies did not move: `market_response` (51 lines) and `matches_response` (284) are still in `main.py`. Only the two `@app.get` decorators left. - The dependency is now a cycle — `main.py` → `routes/odds.py` → (lazily) `main.py` — deferred to call time by the function-scoped import. That has to be unwound by the next step rather than built on. Against the issue's own wording, "`main.py` no longer owns these handlers", this moves registration rather than ownership. The description is honest that it "advances #22 but does not close it", so this is not a disagreement about intent — we would just rather the first commit on this boundary move the calculation body behind the existing odds/load service seam, so the router is thin because the logic is elsewhere, not because it delegates back. Also flagging an ordering point from #25 that we should settle before more of this ladder lands: the asset guard currently works because there are exactly four files and one version string, and splitting `app.js` into modules breaks both halves at once. Worth confirming the guard rebuild comes first, as agreed there.
Some checks failed
CI / syntax (pull_request) Failing after 1s
CI / tests (pull_request) Has been skipped
CI / wheel (pull_request) Has been skipped
CI / browser (pull_request) Has been skipped
This pull request has changes conflicting with the target branch.
  • frontend/index.html
  • tests/web/test_asset_version.py
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin fix/issue-22:fix/issue-22
git switch fix/issue-22

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff fix/issue-22
git switch fix/issue-22
git rebase main
git switch main
git merge --ff-only fix/issue-22
git switch fix/issue-22
git rebase main
git switch main
git merge --no-ff fix/issue-22
git switch main
git merge --squash fix/issue-22
git switch main
git merge --ff-only fix/issue-22
git switch main
git merge fix/issue-22
git push origin main
Sign in to join this conversation.
No description provided.