Frontend shell: the pristine theater drawn at / (closes #39) - #60
Merged
Conversation
`make api` now serves a hand-written SVG theater at `/`, drawn at the instructor's authored coordinates, with `/docs` untouched. Zero dependencies: no Node, no npm, no build step, no CDN, no web fonts. The assets live inside the package and are mounted as static files, so a `pip install` gets the frontend with nothing extra to run. Seven modules with fixed ownership. `api.js` is the only one that knows an endpoint URL, and `graph.js` builds the DOM once while `render.js` only ever sets geometry, text and classes -- splitting the two makes build-once structural, since appending from the render pass would be a visible import-direction violation. Every appearance value lives in one `:root` token block, so retuning for a projector is an edit to one stylesheet. graph.js freezes four solves against the pristine network, so no mark slides when a lane is removed: the layered fallback for unplaced nodes, the directed-pair lookup (reverse arcs have no id by construction), the sliding lane-anchor solve, and the crossing solve. Casings are knockouts on the annotation tier only, never lane-over-lane, so the opening screen carries zero marks. Removal is a style rather than a deletion -- a cut lane is struck out, not deleted, because a lane that vanishes teaches less. The frontend is verified from Python or not at all, and that is a rule rather than an absence: no test asserts the SVG rendered, and the picture is eyeballed forever. tests/test_web_assets.py covers the offline scan, the token subset (a mistyped var() is dropped at computed-value time and renders plausibly wrong) and the type floor, with the --type-/--panel- prefix rule. tests/test_scenarios.py gains the six clearances and a 45-degree crossing-angle floor -- an angle floor rather than a crossing count, because the count is a proven global floor that can never fire while the angles are a windfall protected by nothing. The anchor solve is mirrored in tests/layout_geometry.py, which is where the rule is written down. The two were compared coordinate-by-coordinate across all three datasets and agree to 6e-14; that check needs a JS runtime and so is run by hand rather than added to this suite.
The opening screen was not pristine. The Theater view switched lane labels on, and the plate and casing rules named only `.show-flow` -- so the first thing the room saw was numerals painted over lanes with nothing under them and no crossing broken. The default view now carries the bare network, and the plate and casing follow the marks they back rather than one named layer, so either annotation layer brings its own backing. The anchor mirror diverged where it mattered least and propagated where it mattered most: on an infeasible lane JS fell back to the midpoint and pushed that fallback onto `placed`, which would shift every anchor solved after it while Python skipped it. The fallback stays -- a numeral in a tight spot beats a missing one -- but it no longer takes part in the solve, so the two agree about the whole rest of the diagram. Both sides now say so in words. MIN_LANE_LENGTH was 120 against an anchor clearance of 74. A lane between 120 and 148 would pass that floor and then fail the anchor check, which is how a floor comes to be believed and never fires; it is now derived from the clearance rather than restated beside it. Nothing shipped moves -- the shortest lane in `data/` is 242. Also: the terminals derive their x from the authored range instead of two hardcoded values that were only "the extremes" for today's theater (the textbook dataset clamps at 972, which the old constant got wrong); `state.dataset` is set on first load rather than only on a switch, so the field stops lying; `isRemoved` takes the change map instead of rebuilding it once per lane; arrowheads moved to the stylesheet, being appearance; the plate's type-dependent numerals are named, since a projector retune is the expected edit and would otherwise mis-size every plate silently; and the casing width, which unavoidably lives in three files, is now pinned by a test. Dropped: an empty territory tier, an unused `distance` export, and a per-lane anchor handle nothing read.
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.
Closes #39.
make apinow serves a hand-written SVG theater at/, drawn at theinstructor's authored coordinates, with
/docsuntouched. Zero dependencies:no Node, no npm, no build step, no CDN, no web fonts. The assets live inside
the package and are mounted as static files, so
pip installgets the frontendwith nothing extra to run — verified by building a wheel and confirming all
seven modules are inside it.
Structure
Seven modules with the ownership the spec fixes.
api.jsis the only one thatknows an endpoint URL;
graph.jsbuilds the DOM once whilerender.jsonlyever sets geometry, text and classes — splitting the two makes build-once
structural, since appending from the render pass would be a visible
import-direction violation rather than an invisible line in a diff. Every
appearance value lives in one
:roottoken block, so retuning for a projectoris an edit to one stylesheet.
Boot wiring is inline in
index.htmlrather than amain.js: the spec fixesthe inventory at seven, and an eighth module would have been the implementer's
choice rather than the spec's.
graph.jsfreezes four solves against the pristine network, so no mark slideswhen a lane is removed: the layered fallback, the directed-pair lookup (reverse
arcs have no id by construction), the sliding lane-anchor solve, and the
crossing solve. Casings are knockouts on the annotation tier only, so the
opening screen carries zero marks.
Verification
The frontend is verified from Python or not at all, and that is a rule rather
than an absence — no test asserts the SVG rendered; the picture is eyeballed,
forever.
tests/test_web_assets.pycovers the offline scan, the token subsetand the type floor.
tests/test_scenarios.pygains the clearances and a 45°crossing-angle floor — an angle floor rather than a crossing count, because
the count is a proven global floor that can never fire while the angles are a
windfall protected by nothing.
Each geometry check was confirmed able to go red rather than trusted on a green
run. The angle floor is the one that matters: nudging
AIRHEAD-7by-120,-120holds every other check green and keeps the crossing count at exactly 8,
while dropping an angle to 42.5° — precisely the invisible regression a count
assertion would never catch.
The anchor solve is mirrored in
tests/layout_geometry.py. The two werecompared coordinate-by-coordinate across all three datasets and agree to 6e-14.
That check needs a JS runtime, so it is run by hand rather than added to the
suite;
layout_geometry.pydocuments how.Two things worth a reviewer's judgement
a 120 minimum lane length; those cannot both hold, since a lane needs 2×74 =
148 to seat an anchor at all. A lane between 120 and 148 would pass the floor
and then fail the anchor check. I derived the floor from the clearance and
documented why. Nothing shipped moves — the shortest lane is 242.
to the panel ticket; I read those three as the row's identity rather than
its content. Easy to trim.
Both review axes (standards + spec) were run and their findings fixed in the
second commit — the largest being that the opening screen was not actually
pristine, and that the anchor mirror diverged on infeasible lanes in a way that
would have shifted every anchor solved after one.
250 tests pass.