Skip to content

feat(web): documentation is linked, never embedded — and the anchors are checked (#539) - #552

Merged
eaitbrahim merged 1 commit into
feat-538-pwafrom
feat-539-doc-links
Aug 25, 2026
Merged

feat(web): documentation is linked, never embedded — and the anchors are checked (#539)#552
eaitbrahim merged 1 commit into
feat-538-pwafrom
feat-539-doc-links

Conversation

@eaitbrahim

Copy link
Copy Markdown
Contributor

Closes #539. Stacked on #551#550. Neither has merged yet, so this branches from
feat-538-pwa; I'll rebase down onto main as 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 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, so
no wheel has ever carried it. 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 installed deployment.

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. tests/web/test_doc_links.py does 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 one
a view actually puts on screen — because kv links by lookup, so 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. Two labels are left out on purpose and pinned as such: mode
reads paper or live (one label, two definitions, no way to pick), and 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.

Verification

ruff, mypy and the full suite are green (4868 passed, 3 skipped); 17 new tests, plus a
mutation check that the anchor comparison notices a renamed heading.

Driven in a real Chromium against a snapshot of the live 155 MB deployment database:

  • Eight outbound links on /static/status — the nav's Docs ↗ plus seven term links — all
    versioned on the first paint
    (?v=0.11.2%2Bc1634a3fa17f), all target="_blank"
    rel="noopener noreferrer".
  • The link style inherits the label's --muted colour with a dotted underline, confirmed by
    computed 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.
  • /glossary is a 404 on the rendered pages; the rendered nav's eighth entry is now the outbound
    link; /static/js/docs.js serves 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 carries id="rail", id="attestation",
id="kill-switch", id="autonomy", id="market-clock", id="session-bound-venue" and
id="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 spelling
passed 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 a
running server; both values are non-empty strings, so 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 the test passes both strings so they cannot be confused again.

One architectural-pin entry. render.py needed urllib.parse.quote to encode a + in a
version. RULE5_IMPORT_ALLOWLIST already carries server and staticfiles for exactly this
reason, scoped by (module, import) so urllib.request still fails everywhere; render joins them
with 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 tested
render_glossary. The property it protected did not go — the "not stated" disclaimer is written
into the definition text in docs/glossary.md itself, which is why parse_glossary can derive
stated from it and why test_help_console.py still asserts it on the gharar entry. A note in
test_render.py records that where the test used to be.

🤖 Generated with Claude Code

https://claude.ai/code/session_01T6yA5khYnJ2qzheArRToQ2

… 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
eaitbrahim merged commit 1507463 into feat-538-pwa Aug 25, 2026
4 checks passed
@eaitbrahim
eaitbrahim deleted the feat-539-doc-links branch August 25, 2026 18:56
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>
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.

1 participant