Routing system — tech notes¶
Version: 0.3 · Last Updated: 2026-09-08 · Status: 🔴 DA REVISIONARE
Renewal evidence — 2026-09-08¶
The current implementation is recorded in status, verified against
2465fcc. The August notes below are archival: their line references, coverage
numbers, test paths, missing-consumer assertions and proposed next steps are not
fresh measurements or current work authorization. The original detailed status
can be recovered with git show 2465fcc:internals/10_server/025_routing-system/status.md.
The five complete server/configuration/application/routing/middleware recipes
are now collected by tests/test_documentation_recipes.py; the documentation
renewal verified construction in isolated subprocesses. Other code blocks are
contextual fragments unless explicitly identified as a complete recipe. A
constructed server is not proof that every request or lifecycle path succeeds.
Historical interviews were recovered in the original repository's
temp/internal_doc/ directory, including interview_010_server.md (moved there
on 2026-08-29). These are local archives, not files promised in a new checkout
and not owner ratifications by themselves. Do not recreate absent answers or
start implementation steps from an old scaffold. Current tests live under
tests/core/ and tests/spa/; their x/ directories still contain initializers.
August audit trail (preserved)¶
For whoever works ON this entry, not for whoever reads about plugins. The working trail: what decided what, what is easy to look for and not find, and what the next person needs to know before touching it.
Classification and position¶
A shelf — a technical stratum. Nobody asks for "a routing system"; they ask for a URL that calls a method, for authorization, for a schema.
It was 025_plugins until 2026-08-24, when the owner made the routing system
its subject and put the plugins after it. Two reasons, both worth keeping: the
plugins were unreadable without the tree they arm, and
020 applications was carrying two subjects — what an
application is, and what a routing tree is — which is why its friction tail grew
to twice any other entry's.
Fourth in reading order and it comes after what it explains, deliberately: 020 says an application is a routing class, and this entry then explains a routing class in full. That inversion is the owner's, recorded as §1 of design.md.
Who stands on this¶
| They lean on it as | Entries |
|---|---|
| the mechanism that arms their reader of the tree | 020 applications / openapi, 020 applications / mcp |
| the filter half, applied during resolution | 050 authentication |
| the section it reads its switches from | 015 configuration |
| the thing it is confused with | 030 middleware |
A change to arming reaches every routed application. A change to the shipped dialect plugin reaches only what publishes a schema.
Most of the subject is in another repository¶
The tree, the walk, the three filters and the plugin base class are
genro-routes'. This package owns the arming, one dialect plugin and the
configuration section. When a claim here needs checking, the file is usually
under genro-routes/src/genro_routes/, not under src/genro_asgi/ — and
status.md names which.
All three filters have a live consumer, and they are not where you would
look. Only tags are passed by the HTTP dispatch. Channel is the MCP
engine's (mcp/engine.py:205 and :292) — that face walks the same tree with
its own channel, which is the whole mechanism behind "one tree, several
surfaces". Capabilities are the OpenAPI translator's, published as x-requires
(translator.py:245-247). Searching routed_application.py for the other two
finds nothing, and concluding they are unused is the mistake this note exists
to prevent — it is the mistake this entry's first draft made.
The boundary with 020/openapi — read this before editing anything¶
src/genro_asgi/plugins/openapi/ holds two different subjects, and only
one is this entry's:
plugin.py— the router plugin. This entry.translator.py(307 lines) androuter_openapiin__init__.py— the dialect that turns the neutral description into an OpenAPI document. 020 applications / openapi.
The test file splits the same way: of the 32 items in tests/test_plugins.py,
the eleven in TestTranslator (:255) and TestRouterOpenapi (:311) are
the dialect's. Whoever audits 020/openapi should take them, and should not be
surprised to find them under a plugin-shaped filename.
The working trail¶
Founding decisions — D17 (capabilities are mixins, amending D2's closed
list), D16 (cooperative init), D26 (pydantic/openapi fixed structure, and
the reason: per-entry controls must always apply), the D22 scope ruling
(SPECIFICATION.md:363) that keeps the dialects in the core.
The two rules with no D-entry — no import side effect, and no module-level
mutable registry. They are the coding rules', and the modules state them in
their own contracts (plugin_mixin.py:35-38,
plugins/openapi/plugin.py:35-37). Searching the specification for them finds
nothing; they are real all the same.
Traps¶
default_plugin_registryis a function on purpose. It returns a fresh dict per call so that no module-level mutable registry exists. Turning it into a constant would be a one-line "simplification" that breaks the rule the docstring names.- But the registry that actually decides is not ours.
arm_routerguards onRouter.available_plugins(), which is the routing library's class-level registry (genro-routes/core/router.py:116,:145). Our per-server registry only decides which class we would offer; the library decides which class a code resolves to, once, for the whole process. Friction S2. plugin_registrycannot come from a configuration. It is a construction kwarg with no read-door helper. Searching_configured_kwargsfor it finds nothing because nothing produces it. Friction S3.- Three always-present plugins, two mechanisms.
pydanticandopenapiare the server's (FIXED_PLUGINS);authis the application's, plugged inRoutedApplication.__init__. Counting the plugins on a booted tree gives three, and D26 mentions two. - The per-route option prefix is the plugin code.
openapi_method,openapi_tags,auth_rule. There is no registry of those names to grep: the prefix is the code and the suffix is whateverconfiguredeclares. entry_metadatacontributes nothing when every option is default. It returns{}rather than an empty block, so a route with no publishing options carries no key at all — do not test for its presence.- Two keys, and the reader uses the other one. A plugin's
entry_metadataoutput lands atentry["plugins"][code]["metadata"]; the raw configuration lands atentry["metadata"]["plugin_config"][code]. The OpenAPI translator reads the second (translator.py:164). Writing a new dialect against the first is correct; assuming the shipped one does is not. Friction S4. _armedisFalseon a freshly booted server. Nothing is armed until something readsroute. To inspect a tree without arming it, useRoutingClass.route.fget(app).
What was verified live while writing this¶
Six probes, all building real servers:
- the
design.mdrecipe, whose five-row answer table is the probe's own output — including that@route(openapi_method="delete", openapi_tags="admin")publishes/dropas aDELETEwithtags: ['admin']; - a recipe naming a plugin code nobody registered:
ValueError, "Unknown plugin 'myown'. Register it first. Available plugins: auth, channel, env, logging, openapi, pydantic" — loud, as the design claims; - two servers in one process, each with its own class under the code
mine: the first registers, the second silently gets the first's class on its own tree. Friction S2, reproducible in about twenty lines; grepforentry_pointacross this package, the routing library and bothpyproject.tomlfiles — nothing, which is friction S1;- a plugin written from scratch —
BasePluginsubclass,plugin_code,configure,entry_metadata— handed over asplugin_registry=and named in the description: it arms, and its block lands atentry["plugins"]["owner"]["metadata"]. That probe is also what showed the shippedopenapiplugin's own block going unread (S4); POSTon a route published as aGET: 200. The verb is documentation (S7).
Before the next step is written¶
decisions.md is 🔴 with seven frictions, all tagged by family. None of them is
settled here: they join the grouped pass over the skeleton (010, 015, 020, 025,
030) that the owner chose on 2026-08-23.
S1 and S6 are the cheap ones. S1 edits one line of the overview. S6 is one sentence stating that three plugins are always present and that two mechanisms put them there.
S2 is the one with real content and it is not only ours: the registry that decides is the routing library's, so settling it may mean a change there, or a guard here that refuses to arm a code whose registered class is not the one this server offers. It shares the silent family with S7, and the two are unrelated in mechanism — grouping them by family will put them side by side anyway.
S5 — server-wide rather than per-application — is the one to think about before anything is built on it. If plugins ever become per-application, the section moves from the server's vocabulary into each application's, which is a configuration change of the kind 015 owns.
S4 turned out to be bigger than a test gap. The shipped plugin's contribution has no reader at all, which is why nothing tested it. It closes by deciding which of the two keys is the contract — so it is not the free step it first looked like.
That leaves steps/step_01/ without an obvious free content here. The nearest
is S7, and only in its documentation half: saying in the schema what the server
actually enforces costs nothing and decides nothing.