feat(web): documentation is linked, never embedded — and the anchors are checked (#539) - #552
Merged
Merged
Conversation
… are checked (#539) Every documentation reference is now an outbound keeltrading.com link. The app fetches, bundles and caches nothing, and `/glossary` -- a page that has been EMPTY in every installed deployment keel has ever shipped -- is deleted. ── THE SHIPPED BUG THIS CLOSES ──────────────────────────────────────────────── `docs/` lives at the REPOSITORY root, outside `keel/`, and `uv_build` packages the module root. No wheel has ever carried it. `help_console.load_glossary` says so in its own docstring: "an installed deployment has no docs/ checkout, and the help screen renders that notice as its empty state." It is not fixable with a packaging glob -- measured at #535, building with `artifacts` set and with `artifacts = []` produces byte-identical wheels. Linking out is not the cheaper option here; it is the only one that reaches an install. ── A BROKEN DEEP LINK IS NOT AN ERROR, SO A TEST HAS TO BE ──────────────────── `…/glossary/#rial` opens the glossary at the TOP of the page, looking exactly like a link to a term that happens not to scroll, and nothing anywhere reports it. keel's `docs/` is the source and the site is the mirror, so this repository is the only place a rename and the links depending on it can be compared -- and `tests/web/test_doc_links.py` is that comparison, in both directions: * every `(slug, anchor)` the client can emit resolves to a heading that exists in the named document in `docs/`; * every LABEL in the table is one a view actually puts on screen, because `kv` links by lookup -- a label renamed in `render.js` would silently stop being a link rather than break. ── THE TABLE IS KEYED BY LABEL, AND IS DELIBERATELY INCOMPLETE ──────────────── `kv` consults `docs.TERMS` for every pair it builds, so a label that names a term is a link wherever it appears and no call site knows. The alternative -- tagging each site with a term -- spreads the decision across six views and makes "which words on this screen are defined somewhere" unanswerable without reading all of them. Two labels are left out on purpose and pinned as such. `mode` reads `paper` or `live`: one label, two definitions, no way to pick. `evidence required` sits on the CAPABILITY gates, not the promotion gate, so `#promotion-gate` would be confidently and invisibly wrong. A missing link costs a reader one search; a wrong one costs them their trust in every other link on the page. ── THE FIRST PAINT WAITS FOR THE BUILD, AND `?v=` IS NOT `build` ────────────── `show()` moved inside the `/api/config` callback: a link built before the build is known carries no version until the next poll -- fifteen seconds of links that do not say which build the reader is running, on the first screen they see. `/api/config` opens no database, and `api.read` resolves rather than rejecting, so this cannot hang the app with nothing on the port. The rendered nav links out too, and its first spelling put `cfg.build` in the query -- which is the footer's human-readable LINE, not a version. It rendered `?v=keel%200.11.2%2Bc1634a3fa17f%20%28DIRTY%29%20%5Bcheckout%5D`. Caught by reading the href of a running server, not by a test: both are non-empty strings and every assertion about "the link carries a version" passed. `server._docs_version` now reads `full_version`, the same field `/api/config` hands the client, and a test passes both strings so the two can never be confused again. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2
eaitbrahim
added a commit
that referenced
this pull request
Aug 25, 2026
… are checked (#539) (#552) Every documentation reference is now an outbound keeltrading.com link. The app fetches, bundles and caches nothing, and `/glossary` -- a page that has been EMPTY in every installed deployment keel has ever shipped -- is deleted. ── THE SHIPPED BUG THIS CLOSES ──────────────────────────────────────────────── `docs/` lives at the REPOSITORY root, outside `keel/`, and `uv_build` packages the module root. No wheel has ever carried it. `help_console.load_glossary` says so in its own docstring: "an installed deployment has no docs/ checkout, and the help screen renders that notice as its empty state." It is not fixable with a packaging glob -- measured at #535, building with `artifacts` set and with `artifacts = []` produces byte-identical wheels. Linking out is not the cheaper option here; it is the only one that reaches an install. ── A BROKEN DEEP LINK IS NOT AN ERROR, SO A TEST HAS TO BE ──────────────────── `…/glossary/#rial` opens the glossary at the TOP of the page, looking exactly like a link to a term that happens not to scroll, and nothing anywhere reports it. keel's `docs/` is the source and the site is the mirror, so this repository is the only place a rename and the links depending on it can be compared -- and `tests/web/test_doc_links.py` is that comparison, in both directions: * every `(slug, anchor)` the client can emit resolves to a heading that exists in the named document in `docs/`; * every LABEL in the table is one a view actually puts on screen, because `kv` links by lookup -- a label renamed in `render.js` would silently stop being a link rather than break. ── THE TABLE IS KEYED BY LABEL, AND IS DELIBERATELY INCOMPLETE ──────────────── `kv` consults `docs.TERMS` for every pair it builds, so a label that names a term is a link wherever it appears and no call site knows. The alternative -- tagging each site with a term -- spreads the decision across six views and makes "which words on this screen are defined somewhere" unanswerable without reading all of them. Two labels are left out on purpose and pinned as such. `mode` reads `paper` or `live`: one label, two definitions, no way to pick. `evidence required` sits on the CAPABILITY gates, not the promotion gate, so `#promotion-gate` would be confidently and invisibly wrong. A missing link costs a reader one search; a wrong one costs them their trust in every other link on the page. ── THE FIRST PAINT WAITS FOR THE BUILD, AND `?v=` IS NOT `build` ────────────── `show()` moved inside the `/api/config` callback: a link built before the build is known carries no version until the next poll -- fifteen seconds of links that do not say which build the reader is running, on the first screen they see. `/api/config` opens no database, and `api.read` resolves rather than rejecting, so this cannot hang the app with nothing on the port. The rendered nav links out too, and its first spelling put `cfg.build` in the query -- which is the footer's human-readable LINE, not a version. It rendered `?v=keel%200.11.2%2Bc1634a3fa17f%20%28DIRTY%29%20%5Bcheckout%5D`. Caught by reading the href of a running server, not by a test: both are non-empty strings and every assertion about "the link carries a version" passed. `server._docs_version` now reads `full_version`, the same field `/api/config` hands the client, and a test passes both strings so the two can never be confused again. Claude-Session: https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
eaitbrahim
added a commit
that referenced
this pull request
Aug 25, 2026
…a balance (#538) (#551) * feat(web): installable, with a worker that structurally cannot cache a balance (#538) Manifest, icons and a service worker, so `keel serve` installs as a desktop app and opens to the shell -- and its own banner -- when the engine is not running. ── THE API RULE IS ENFORCED BY SCOPE, NOT BY CARE ───────────────────────────── A PWA caching financial data is actively dangerous: opening the app to last week's equity styled as current is worse than an error, because an error is visible. The spec routes `/api/*` `NetworkOnly`, "no exceptions", and the implementation makes that structural rather than disciplined: `sw.js` ships at the STATIC ROOT, so its scope is `/static/` and `/api/*` is outside it. A worker's `fetch` handler is never invoked for a request outside its own scope -- not skipped, not returned early from: the browser does not consult it. No edit to that file can cache an API response, because no edit to that file can see one. The only cache write in the module is `addAll(PRECACHE)`; there is no runtime `put` anywhere, and a test pins that there never is. The explicit `/api/` guard in `fetch` is therefore dead code today, and is pinned by a test that says so -- #540 widens the scope to `/` and it stops being redundant on the same day nobody is thinking about it. This is where the implementation departs from the spec's file list, which puts `sw` among the modules in `js/`. `js/sw.js` is scoped to `/static/js/` and cannot answer a navigation to `/static/insights`. The alternative -- a `Service-Worker-Allowed` header widening the scope from `js/` -- was rejected for failing SILENTLY: remove the header and the worker still installs, still activates, and simply stops controlling the app. The spec is amended in place rather than quietly diverged from. ── THE ICONS ARE GENERATED, BECAUSE FOUR BINARY BLOBS ARE NOT REVIEWABLE ────── `scripts/build_icons.py` defines the mark ONCE as three stroke coordinates and emits the SVG and all three PNGs from it, with a stdlib rasteriser -- point-in-polygon and `zlib.compress`, no Pillow. `--check` re-renders and compares bytes, and a test runs it, so a hand-edited PNG fails and a deliberate change has to be made where it is legible. The mark is a monogram, and that was decided by LOOKING. Three nautical marks were drawn and rendered first -- a hull in section over a keel, a hull in profile with a fin, and a bulb keel -- and at icon size each one read as something else: a funnel, a letter T, and an exclamation mark on a saucer. A tile is 32-48px on a background nobody chose, and at that size a silhouette gets one reading, not necessarily the drawn one. ── ONE ATTRIBUTE DECIDES WHETHER ANY OF THIS INSTALLS ───────────────────────── `<link rel="manifest">` is fetched with credentials mode "omit" by DEFAULT, and every response this server sends is gated on the session cookie. Without `crossorigin="use-credentials"` the manifest fetch is a 403, the browser reports no manifest, and the app is simply not installable -- with nothing in the page or the console pointing at the cause. Measured, not assumed: with the worker unregistered, a credential-less fetch of the manifest returns 403 and a credentialed one returns 200. The cache name carries the build from `/api/config`, registered as `sw.js?v=<build>`. `encodeURIComponent`, because `keel.version` produces `0.11.2+88fb17bcab15` and a raw `+` in a query string decodes to a SPACE -- the cache would silently stop tracking the binary it exists to track. A failed config read registers NOTHING: with the engine stopped the installed worker is the thing letting the operator read the page, and re-registering it under `unknown` would swap a correct cache for an empty one exactly when the network cannot refill it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2 * feat(web): documentation is linked, never embedded -- and the anchors are checked (#539) (#552) Every documentation reference is now an outbound keeltrading.com link. The app fetches, bundles and caches nothing, and `/glossary` -- a page that has been EMPTY in every installed deployment keel has ever shipped -- is deleted. ── THE SHIPPED BUG THIS CLOSES ──────────────────────────────────────────────── `docs/` lives at the REPOSITORY root, outside `keel/`, and `uv_build` packages the module root. No wheel has ever carried it. `help_console.load_glossary` says so in its own docstring: "an installed deployment has no docs/ checkout, and the help screen renders that notice as its empty state." It is not fixable with a packaging glob -- measured at #535, building with `artifacts` set and with `artifacts = []` produces byte-identical wheels. Linking out is not the cheaper option here; it is the only one that reaches an install. ── A BROKEN DEEP LINK IS NOT AN ERROR, SO A TEST HAS TO BE ──────────────────── `…/glossary/#rial` opens the glossary at the TOP of the page, looking exactly like a link to a term that happens not to scroll, and nothing anywhere reports it. keel's `docs/` is the source and the site is the mirror, so this repository is the only place a rename and the links depending on it can be compared -- and `tests/web/test_doc_links.py` is that comparison, in both directions: * every `(slug, anchor)` the client can emit resolves to a heading that exists in the named document in `docs/`; * every LABEL in the table is one a view actually puts on screen, because `kv` links by lookup -- a label renamed in `render.js` would silently stop being a link rather than break. ── THE TABLE IS KEYED BY LABEL, AND IS DELIBERATELY INCOMPLETE ──────────────── `kv` consults `docs.TERMS` for every pair it builds, so a label that names a term is a link wherever it appears and no call site knows. The alternative -- tagging each site with a term -- spreads the decision across six views and makes "which words on this screen are defined somewhere" unanswerable without reading all of them. Two labels are left out on purpose and pinned as such. `mode` reads `paper` or `live`: one label, two definitions, no way to pick. `evidence required` sits on the CAPABILITY gates, not the promotion gate, so `#promotion-gate` would be confidently and invisibly wrong. A missing link costs a reader one search; a wrong one costs them their trust in every other link on the page. ── THE FIRST PAINT WAITS FOR THE BUILD, AND `?v=` IS NOT `build` ────────────── `show()` moved inside the `/api/config` callback: a link built before the build is known carries no version until the next poll -- fifteen seconds of links that do not say which build the reader is running, on the first screen they see. `/api/config` opens no database, and `api.read` resolves rather than rejecting, so this cannot hang the app with nothing on the port. The rendered nav links out too, and its first spelling put `cfg.build` in the query -- which is the footer's human-readable LINE, not a version. It rendered `?v=keel%200.11.2%2Bc1634a3fa17f%20%28DIRTY%29%20%5Bcheckout%5D`. Caught by reading the href of a running server, not by a test: both are non-empty strings and every assertion about "the link carries a version" passed. `server._docs_version` now reads `full_version`, the same field `/api/config` hands the client, and a test passes both strings so the two can never be confused again. Claude-Session: https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2 Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closed
5 tasks
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 #539. Stacked on #551 → #550. Neither has merged yet, so this branches from
feat-538-pwa; I'll rebase down ontomainas each lands.Every documentation reference is now an outbound keeltrading.com link. The app fetches, bundles and
caches nothing, and
/glossary— a page that has been empty in every installed deployment keelhas ever shipped — is deleted.
The shipped bug this closes
docs/lives at the repository root, outsidekeel/, anduv_buildpackages the module root, sono wheel has ever carried it. It is not fixable with a packaging glob (measured at #535: building
with
artifactsset and withartifacts = []produces byte-identical wheels). Linking out is notthe cheaper option here — it is the only one that reaches an installed deployment.
A broken deep link is not an error, so a test has to be
…/glossary/#rialopens the glossary at the top of the page, looking exactly like a link to aterm that happens not to scroll, and nothing anywhere reports it. keel's
docs/is the source andthe site is the mirror, so this repository is the only place a rename and the links depending on it
can be compared.
tests/web/test_doc_links.pydoes that in both directions: every(slug, anchor)the client emits resolves to a heading that exists in
docs/, and every label in the table is onea view actually puts on screen — because
kvlinks by lookup, so a label renamed inrender.jswould silently stop being a link rather than break.
The table is keyed by label, and is deliberately incomplete
kvconsultsdocs.TERMSfor every pair it builds, so a label that names a term is a link whereverit appears and no call site knows. Two labels are left out on purpose and pinned as such:
modereads
paperorlive(one label, two definitions, no way to pick), andevidence requiredsitson the capability gates, not the promotion gate, so
#promotion-gatewould be confidently andinvisibly wrong. A missing link costs a reader one search; a wrong one costs them their trust in
every other link on the page.
Verification
ruff,mypyand the full suite are green (4868 passed, 3 skipped); 17 new tests, plus amutation check that the anchor comparison notices a renamed heading.
Driven in a real Chromium against a snapshot of the live 155 MB deployment database:
/static/status— the nav'sDocs ↗plus seven term links — allversioned on the first paint (
?v=0.11.2%2Bc1634a3fa17f), alltarget="_blank"rel="noopener noreferrer".--mutedcolour with a dotted underline, confirmed bycomputed style rather than by eye — colour alone would repeat exactly what Palette: profit and loss are the same brightness, and inputs have no visible border #532 found.
/glossaryis a 404 on the rendered pages; the rendered nav's eighth entry is now the outboundlink;
/static/js/docs.jsserves 200 and is in the worker's precache.And the loop closed against the published site, which is the half
docs/cannot prove:https://keeltrading.com/en/docs/glossary/returns 200 and carriesid="rail",id="attestation",id="kill-switch",id="autonomy",id="market-clock",id="session-bound-venue"andid="paper-mode"— every anchor this branch emits.Two things worth a reviewer's attention
?v=was carrying the wrong string, and no test caught it. The rendered nav's first spellingpassed
cfg.build— the footer's human-readable line — producing?v=keel%200.11.2%2Bc1634a3fa17f%20%28DIRTY%29%20%5Bcheckout%5D. Found by reading the href of arunning server; both values are non-empty strings, so every assertion about "the link carries a
version" passed.
server._docs_versionnow readsfull_version, the same field/api/confighandsthe client, and the test passes both strings so they cannot be confused again.
One architectural-pin entry.
render.pyneededurllib.parse.quoteto encode a+in aversion.
RULE5_IMPORT_ALLOWLISTalready carriesserverandstaticfilesfor exactly thisreason, scoped by (module, import) so
urllib.requeststill fails everywhere;renderjoins themwith a note that the entry goes when #540 deletes the module. The alternative was a hand-rolled
encoder for "the characters a version string might contain", which is a guess about an alphabet
rather than a rule about one.
Also deleted:
test_a_fiqh_term_that_fiqh_basis_does_not_state_says_so, which testedrender_glossary. The property it protected did not go — the "not stated" disclaimer is writteninto the definition text in
docs/glossary.mditself, which is whyparse_glossarycan derivestatedfrom it and whytest_help_console.pystill asserts it on the gharar entry. A note intest_render.pyrecords that where the test used to be.🤖 Generated with Claude Code
https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2