Applications — tech notes¶
Version: 0.2 · 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/020_applications/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 applications. 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 feature, and the one a consumer of this framework actually writes. Every
other entry of 10_server is something they configure or inherit; this is the
class they subclass. The entry's own pages say nothing about being a feature —
the classification is editorial and lives here.
Third in reading order, and it assumes the two before it: the identity triplet and the demux are 010 server's, the recipe and the read door are 015 configuration's. It defines the routing tree, which four later entries assume.
Who stands on this¶
| They lean on it as | Entries |
|---|---|
| the tree they arm a capability onto | 025 routing system |
| the dispatch the middleware chain wraps, and the exceptions they answer | 030 middleware |
| the request their identity rides on | 050 authentication, 040 sessions |
| a lens on the same tree | openapi, mcp |
| the base class they subclass | 090 server-application, 20_spa/010 spa-application |
| the streaming they push over | 070 tasks (SSE push), 090 inspector |
A change to the four obligations reaches every one of them. A change to the argument reconciliation reaches the two lenses, because they share the method.
The working trail¶
Founding decisions — D7 (the app-side contract, phase 0, defined by its
tests), D16 (cooperative init), D18 (slots), D4 (no service endpoint injected
into a hosted router). Then D23's wave rulings (handlers are pure), D25
(declarative branches, invariant 10 enforced), D26 (pydantic/openapi fixed
structure), and the scope ruling at SPECIFICATION.md:363 that puts the complete
OpenAPI and MCP applications in the core rather than above it.
The invariants that bind this entry — SPECIFICATION.md §5, numbers 2 (thread-correct teardown), 3 (a denied node answers natively, never falls through), 9 (one contract-test suite across implementations of one interface) and 10 (never routing as registry). All four are recorded from defects in the old implementation, and each one is worth reading before changing the dispatch.
Delivery commits — 53b4e38 (2026-07-21, core 1c: request, response,
plugins, the two lenses), c360f60 (2026-07-24, core 1e: SSE over streaming),
a1a8f7e (2026-07-25: code + mount), 0dff4ed (2026-08-12:
route_cleanup), 5b567a3 (2026-08-14: 401 for the anonymous, 403 for the
known).
The ruling that is not in the log — the same one that bites
010 server: a1a8f7e was never appended to
SPECIFICATION.md, so §4's app-side contract still names mount_name. Do not
search the specification for code/mount on the application side; the only
record is that commit message. Friction S1.
Traps¶
route_cleanuplooks dead and is not. No override anywhere insrc/, and the only tests are the ones that assert it is called. Its production consumer is in another repository:genropy-asgi/src/genropy_asgi/proxy/genropy_proxy.py:69. Same discipline aswsgi_app— a seam whose filler is the bridge.- The live request does not reach a hosted application's handler. The
injection everyone remembers is
ServerApplication.bind_kwargs(server_app.py:200), not the base one, and the parameter is_request, notrequest. Searchingrouted_application.pyfor the seam D23 ratified finds nothing. Friction S2. request.pathis already mount-relative. The server stripped the prefix before the handover, so an application resolvingrequest.pathagainst its own tree is correct, and anything reconstructing an absolute URL from it is not.asyncio.iscoroutinefunction, neverinspect's. On 3.11 genro-routes falls back to the asyncio sentinel and only the asyncio check reads it (routed_application.py:221-222). Swapping the import silently sends every async handler through the thread pool.- The
routeproperty has a side effect. First access after attachment arms the server's plugins (routed_application.py:143-156). A debugging line that touchesapp.routeearly changes when arming happens. - But touching
routein the constructor is safe, and looks like it is not. Before attachmentself.serverisNone, soarm_routeris not found, the branch is skipped and_armedstaysFalse. Verified on a booted server. Every application in the package touchesroutein its own constructor — this is the normal shape, not a hazard. - Every branch in the package uses the
instanceform. Thecls+paramsform D25 named as the destination is used nowhere, and the library derives the build timing from which form is used. Converting one call site changes when that sub-tree exists. Friction S15. fields is Noneis not reachable on a real server.pydanticis inFIXED_PLUGINSand cannot be switched off, so the no-signature branches ofbind_kwargs/spread_over_paramsonly run under a bareBaseServer. Do not write a test that expects to reach them throughAsgiServer.
What was verified live while writing this¶
Nine probes, all against a composed AsgiServer at the ASGI level:
- the
design.mdrecipe, whose seven-row answer table is the probe's own output; - a
POSTwith and withoutcontent-type—{"sum": 5}against{"sum": 0}; - a
TypeErrorin a sync handler body → 400 with the internal message, the same bug in an async body → 500, and aValueErrorin a sync body → 500; grepfor consumers ofRequest.created_at/age/scopeacrosssrc/,tests/and the genropy-asgi bridge — none;_armedon a booted application: stillFalseafter the constructor's ownadd_branchesandroute.plug,Trueafter the first access made once the server owns it, with['auth', 'openapi', 'pydantic']on the router. The constructor's early touch ofroutedoes not consume the guard, and the recipe's sub-tree answers 200. A blind reader flagged this as a probable defect; it is not one, and the pages now say why;grepfor"cls"insrc/andtests/— none, which is what makes the D25 migration half done (S15);- the custom-grammar snippet of README §1, run end to end: a recipe writing
only
branding(title=…)readsMain Storeback andEURfrom the parameter's own declared default; @route(auth_rule="admin")on a composed server: 401 to an anonymous caller, 200 on the unruled route beside it;SseStreamover a two-event async generator: the wire carriesretry: 2000, thenevent: takings/data: {"today": 42}, then a baredata:record.
Whoever reopens frictions S4, S5, S10 or S15 can reproduce each in a few lines rather than re-deriving them.
Before the next step is written¶
decisions.md is 🔴 with sixteen frictions, and they do not all belong to the
same conversation. The interview is temp/interview_020_applications.md,
fourteen turns; S16 has no turn of its own because it closes by building, not
by deciding.
S1 and S2 are cheap and should go first: S1 is one amendment to SPECIFICATION.md §4 that rides along with 010's S1/S2, and S2 is a placement plus a name.
S4, S5, S9 and S14 are one conversation about failure, not four. All of them are about a wrong thing reported as the right kind of thing; S9 sits inside S5, and S14 is likely its mechanical cause. Settling them means deciding what a 400 body may carry and which of the two mappings owns which case, which the interview should take as one question.
S3 and S7 are the ones with real design content: whether the response shapes are ratified or conventional, and whether the arrival wants a streaming request body. S7 in particular cannot be closed by a "no" alone — the overview's first rule requires the reason to be written where the limit is accepted.
Four cannot be settled inside this entry. S6 (the WebSocket entry point) is recorded in the same wording in 20_spa/030 channel, S14 in 030 middleware, S12 in both 010 server and 015 configuration, S13 in 015, and S11 in 010. All five were seeded there by this audit and carry the same text on both sides. Settling one of them means editing two documents in one change; answering it in only one place is how the dossier would start disagreeing with itself again.
S12 is the one to settle first among those, because §1 of this entry is a proposal and the other two entries state contracts of their own. Everything written about the application contract anywhere depends on which list wins.
The obvious content of steps/step_01/ is the small, self-contained half: the
missing tests (S8, and the streaming handover in particular) and the dead
surface of S10. Neither depends on any of the open questions above, and both
shrink the distance between the two documents without deciding anything. S15 —
converting the four branch call sites to the factory form — is a candidate for
the same step, but only after T10 says which form is the destination, because
the conversion also changes when those sub-trees are built.