EidosPaths (frozen path register)¶
Verified against the code · 421c63ba3fab5894cdb9e707eed8ef694881d60d · 2026-10-20
956 D1b-1 (2026-10-20): 4 finding(s) fixed — head plugins load before eidos-paths.js, _EXCEPTIONS holds only domains.json (two-phase omission documented in the eidos-paths.js header), the contract guard is a lexical scan, the freshness test compares in memory. Re-verified against main 421c63ba.¶
Layer: frontend
window.EidosPaths is the frontend's canonical path register. Every path that
goes to the backend is either (a) a bare property read like EidosPaths.me
(static path), or (b) a builder call like EidosPaths.nodeNodeId(uid)
(templated path, with each parameter URL-encoded). Loaded before app.js and
the plugins that follow it (index.html:6979; the three <head> plugins
rule-sentence.js, domain-routing.js and tree-chip-type.js at lines 29-31
load earlier), so a caller never needs to hand-write '/node/' + uid.
Generated, not hand-maintained¶
frontend/eidos-paths.js is auto-generated by
scripts/gen_frontend_paths.py from docs/api/openapi.yaml. Regenerate with:
python scripts/gen_frontend_paths.py
One entry per unique contract path. Static paths (/me, /relations,
/tree) land as string literals. Templated paths (/node/{nodeId},
/mutations/node/{nodeUuid}/impact) land as arrow builders, each parameter
wrapped in encodeURIComponent. The register is Object.freezed at module
end so a caller cannot mutate it at runtime.
Two freshness + contract tests¶
tests/test_frontend_paths_generated_fresh.py— callsgen_frontend_paths.generate()in memory and asserts the string equals the committed file; nothing is written to disk. A new OpenAPI path, a rename, or any manual edit offrontend/eidos-paths.jsfails this test.tests/test_frontend_paths_in_contract.py— static lexical scan (the_CALL/_ALIASregexes plus a string/comment token scanner; no AST parser) of every deployed script inindex.htmlplusfrontend/plugins/*.js. For each HTTP layer call (apiFetch,_apiFetch,EidosApi.raw,EidosApi.request), the first argument must beEidosPaths.<name>, a frozen literal exception, or a runtime expression. Aliasing the HTTP layer under a new name is explicitly forbidden — a renamed alias would hide the call from the scan.
The one chokepoint not in the register¶
The EidosMutations-owned two-phase routes
/mutations/{node,node-update,relation}/{preview,commit} are deliberately
absent from EidosPaths. They are reached only through that layer; promoting
them into the register would let any caller hit commit directly, bypassing
the two-phase guard. That omission is documented in the frontend/eidos-paths.js
header (and its generator). The in_contract test's _EXCEPTIONS list holds
only domains.json / /domains.json; it never mentions the two-phase routes.
Dev/test runtime guard¶
EidosApi.raw(path, opts) consults pathRegistry + devMode() injected by
configure(). In dev or test (devMode() === true AND pathRegistry set to
window.EidosPaths), the pathname of the request URL is asserted present in
the register before fetch( is called. In prod both branches short-circuit
and the check is a single boolean eval. This complements the static scan by
catching aliasing or dynamic-dispatch holes the static scanner cannot see.
Load order¶
<script src="frontend/eidos-api.js"> <!-- defines window.EidosApi; dep-light -->
<script src="frontend/eidos-paths.js"> <!-- freezes window.EidosPaths -->
<script src="frontend/app.js"> <!-- calls EidosApi.configure({pathRegistry: window.EidosPaths, ...}) -->
eidos-api.js loads BEFORE eidos-paths.js in frontend/index.html (per the
EidosApi entity's own load-order block). eidos-api.js is dep-light and
reads pathRegistry lazily through the configure() seam, so the file-load
order does not matter for correctness — app.js wires the frozen register in
during bootstrap once both files have loaded.