@orkestrel/scaffold 0.0.67 → 0.0.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/bin/main.js +67 -44
  2. package/dist/bin/main.js.map +1 -1
  3. package/dist/host/agents/templates/brief.md +9 -0
  4. package/dist/host/claude/agents/orkestrel.md +4 -4
  5. package/dist/host/claude/rules/names.md +15 -0
  6. package/dist/host/claude/rules/tests.md +33 -4
  7. package/dist/host/claude/rules/workspace.md +14 -2
  8. package/dist/host/dotfiles/prettierignore +3 -0
  9. package/dist/host/guides/README.md +65 -0
  10. package/dist/host/guides/abort.md +169 -0
  11. package/dist/host/guides/agent.md +1567 -0
  12. package/dist/host/guides/brief.md +1266 -0
  13. package/dist/host/guides/browser.md +2200 -0
  14. package/dist/host/guides/budget.md +196 -0
  15. package/dist/host/guides/codec.md +519 -0
  16. package/dist/host/guides/console.md +785 -0
  17. package/dist/host/guides/contract.md +1193 -0
  18. package/dist/host/guides/csv.md +541 -0
  19. package/dist/host/guides/database.md +2518 -0
  20. package/dist/host/guides/emitter.md +233 -0
  21. package/dist/host/guides/form.md +1791 -0
  22. package/dist/host/guides/html.md +717 -0
  23. package/dist/host/guides/indexeddb.md +505 -0
  24. package/dist/host/guides/interpret.md +1029 -0
  25. package/dist/host/guides/lsp.md +515 -0
  26. package/dist/host/guides/markdown.md +964 -0
  27. package/dist/host/guides/mcp.md +5554 -0
  28. package/dist/host/guides/middleware.md +927 -0
  29. package/dist/host/guides/msg.md +440 -0
  30. package/dist/host/guides/ndjson.md +120 -0
  31. package/dist/host/guides/ollama.md +380 -0
  32. package/dist/host/guides/pool.md +280 -0
  33. package/dist/host/guides/probe.md +1210 -0
  34. package/dist/host/guides/process.md +1620 -0
  35. package/dist/host/guides/program.md +1110 -0
  36. package/dist/host/guides/qualifier.md +854 -0
  37. package/dist/host/guides/queue.md +370 -0
  38. package/dist/host/guides/rater.md +330 -0
  39. package/dist/host/guides/reason.md +1122 -0
  40. package/dist/host/guides/relation.md +373 -0
  41. package/dist/host/guides/router.md +753 -0
  42. package/dist/host/guides/scaffold.md +192 -31
  43. package/dist/host/guides/sea.md +383 -0
  44. package/dist/host/guides/server.md +752 -0
  45. package/dist/host/guides/sqlite.md +330 -0
  46. package/dist/host/guides/sse.md +187 -0
  47. package/dist/host/guides/supervisor.md +4890 -0
  48. package/dist/host/guides/table.md +1556 -0
  49. package/dist/host/guides/template.md +280 -0
  50. package/dist/host/guides/terminal.md +1145 -0
  51. package/dist/host/guides/test.md +2969 -0
  52. package/dist/host/guides/timeout.md +252 -0
  53. package/dist/host/guides/tool.md +507 -0
  54. package/dist/host/guides/toolbox.md +1038 -0
  55. package/dist/host/guides/websocket.md +282 -0
  56. package/dist/host/guides/worker.md +615 -0
  57. package/dist/host/guides/workflow.md +1507 -0
  58. package/dist/host/guides/workspace.md +595 -0
  59. package/dist/host/manifest.json +1218 -10
  60. package/dist/host/tests/policy.test.ts +279 -2
  61. package/dist/host/tests/setupPolicy.ts +445 -6
  62. package/dist/src/core/index.cjs +38 -16
  63. package/dist/src/core/index.cjs.map +1 -1
  64. package/dist/src/core/index.d.cts +33 -9
  65. package/dist/src/core/index.d.ts +33 -9
  66. package/dist/src/core/index.js +37 -17
  67. package/dist/src/core/index.js.map +1 -1
  68. package/dist/src/server/index.cjs +1750 -1567
  69. package/dist/src/server/index.cjs.map +1 -1
  70. package/dist/src/server/index.d.cts +106 -24
  71. package/dist/src/server/index.d.ts +106 -24
  72. package/dist/src/server/index.js +1751 -1570
  73. package/dist/src/server/index.js.map +1 -1
  74. package/package.json +3 -3
@@ -0,0 +1,753 @@
1
+ # Router
2
+
3
+ > The typed request router: a path-matching engine (`Router`) that compiles route patterns,
4
+ > extracts URL-decoded params, and resolves the most specific match, with a fetch-standard,
5
+ > method-dimensioned dispatcher (`Dispatcher`), a headless History or hash `Navigator`, and a
6
+ > `node:http` adapter all composing that same engine.
7
+
8
+ This guide covers every face the package publishes. The core is pure and
9
+ environment-agnostic: it speaks `string`, `RegExp`, `URL`, `Request`, and `Response`, and
10
+ neither DOM nor `node:*`. Precedence, trailing-slash folding, tolerant percent-decoding, and
11
+ the `answers` override seam all live in the engine rather than in a face, which is what keeps
12
+ the browser and server faces thin; a native override earns its place only on a genuinely
13
+ faster path. Source: [`src/core`](../src/core), [`src/browser`](../src/browser), and
14
+ [`src/server`](../src/server), surfaced through the `@orkestrel/router` barrel (aliased
15
+ `@src/core` / `@src/browser` / `@src/server` inside this repo).
16
+
17
+ ## Surface
18
+
19
+ ### Register and match
20
+
21
+ Register routes on a `Router`, resolve the most-specific match, and dispatch
22
+ fetch-standard requests through a `Dispatcher`:
23
+
24
+ ```ts
25
+ import { createDispatcher, createRouter } from '@orkestrel/router'
26
+
27
+ const router = createRouter<{ readonly page: string }>()
28
+ router.add({ path: '/users/:id', meta: { page: 'profile' } })
29
+ router.match('/users/7') // { path: '/users/:id', params: { id: '7' }, meta: { page: 'profile' } }
30
+
31
+ const dispatcher = createDispatcher<{ readonly userId: string }>({
32
+ routes: [
33
+ {
34
+ method: 'GET',
35
+ path: '/users/:id',
36
+ handler: (_request, context) => Response.json(context.params),
37
+ },
38
+ ],
39
+ })
40
+ const response = await dispatcher.handle(new Request('http://x/users/7'), { userId: 'me' })
41
+ ```
42
+
43
+ Path patterns are `/`-prefixed: a literal segment (`/users`), a `:name` param
44
+ (one segment), or a final `*name` wildcard (captures the rest of the path).
45
+ Matching is case-sensitive by default (`sensitive: false` opts out); a single
46
+ trailing slash is always optional except on the root `/` and the empty
47
+ pattern.
48
+
49
+ Browser and server usage appear under [Patterns](#patterns).
50
+
51
+ ### Factories
52
+
53
+ | API | Kind | Summary |
54
+ | ------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
55
+ | `createRouter` | function | Creates a `RouterInterface` — the pure path-matching + registry engine shared by the browser `Navigator` and the core `Dispatcher`. |
56
+ | `createDispatcher` | function | Creates a `DispatcherInterface` — the fetch-standard, method-dimensioned dispatch entity over one internal `Router<RouteRecord<TState>>`. |
57
+ | `createNavigator` | function | Creates a `NavigatorInterface` — the headless History/hash navigation entity composing one core `Router<Meta>`. |
58
+
59
+ ### Constants
60
+
61
+ A `Shape` cell holds the constant's declared type.
62
+
63
+ | API | Kind | Shape | Summary |
64
+ | --------------- | ----- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
65
+ | `METHOD_LIST` | const | `readonly ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']` | Lists the HTTP methods a `DispatcherInterface` registers routes under, in canonical order — a frozen literal tuple, and the single source the `Method` type, `METHODS`, and `parseMethod` are all derived from. |
66
+ | `METHODS` | const | `ReadonlySet<string>` | Holds every HTTP method a `DispatcherInterface` registers routes under as a `ReadonlySet` — backs the registration guard (`add` rejects any `method` outside this set) and the auto-`OPTIONS` `Allow` derivation. |
67
+ | `TIER_LITERAL` | const | `number` | Names the specificity tier for a \*\*literal\*\* path segment (`/users`) — the highest tier, always outranking a param or wildcard segment at the same position. |
68
+ | `TIER_PARAM` | const | `number` | Names the specificity tier for a \*\*param\*\* path segment (`:name`) — ranks below a literal segment and above a wildcard segment at the same position. |
69
+ | `TIER_WILDCARD` | const | `number` | Names the specificity tier for a \*\*wildcard\*\* path segment (`*name`) — the lowest tier; a wildcard only ever wins against another wildcard shape (an equal-specificity tie resolved by registration order). |
70
+
71
+ ### Helpers
72
+
73
+ | API | Kind | Summary |
74
+ | ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
75
+ | `escapeRegExp` | function | Escapes every regex metacharacter in a literal string so it can be embedded inside a larger `RegExp` source without being interpreted as syntax. |
76
+ | `canonicalizePath` | function | Canonicalizes a route path for registry identity — strips a single trailing slash, except the root `/` (and the empty pattern). The trailing-slash fold `compilePath` normalizes a pattern through, so identity agrees with the matcher. |
77
+ | `computeDispatchKey` | function | Computes the canonical `METHOD /path` registry key for a method-dimensioned dispatcher route. |
78
+ | `compilePath` | function | Compiles a route path pattern into an anchored regex and its ordered param names. |
79
+ | `decodeParam` | function | Decodes one captured param value from a URL, tolerating a malformed percent-escape — the decode `matchPath` applies to each captured group. |
80
+ | `matchPath` | function | Extracts the URL-decoded params a compiled path captures from a concrete pathname, or `undefined` when the pathname does not match. |
81
+ | `classifySegment` | function | Classifies one path segment into its specificity tier — the same syntax `compilePath` rewrites: a syntactically valid `:name` head is a param segment, a final `*name` is a wildcard segment, and everything else (including a literal segment that merely contains a `:` mid-string, for example `a:b`) is a literal segment. |
82
+ | `computeSpecificity` | function | Computes a route path's specificity vector — the per-segment type ranking that breaks a tie when several registered routes match the same concrete pathname. |
83
+ | `compareSpecificity` | function | Compares two route paths by specificity — the comparator that picks the most-specific matching route (literal-over-param-over-wildcard, registration-order-independent). |
84
+ | `joinPaths` | function | Joins a group prefix and a route path into one `/`-prefixed path, normalizing duplicate or missing joining slashes. |
85
+ | `defineRoute` | function | Provides an identity pass-through for a `RouteInput` that pins its `Path` generic to the literal registration-site string, so `context.params` types correctly through `PathParams` without an explicit type argument. |
86
+ | `computeNavigationKey` | function | Computes the canonical path key a `Navigator` registers a browser navigation route under. |
87
+ | `extractHashPath` | function | Extracts the `/`-prefixed pathname from a `location.hash` value — strips the leading `#` (keeping the route's own leading `/`) and any `?query` suffix. |
88
+ | `resolveLocationPath` | function | Resolves the `/`-prefixed pathname to match for the current location, in either navigation mode — the one seam `extractHashPath` (hash mode) and history-mode base-stripping share. |
89
+ | `findAnchor` | function | Finds the nearest enclosing `<a>` element a DOM event originated from, by walking its composed path — the pure lookup behind history-mode link interception. |
90
+ | `buildRequest` | function | Builds a fetch-standard `Request` from a `node:http` `IncomingMessage` — the server-adapter half of the fetch/node conversion seam. |
91
+ | `sendResponse` | function | Writes a fetch-standard `Response` back to a `node:http` `ServerResponse` — the reverse half of the fetch/node conversion seam. |
92
+
93
+ ### Parsers
94
+
95
+ | API | Kind | Summary |
96
+ | ------------- | -------- | ---------------------------------------------------------------------------------- |
97
+ | `parseMethod` | function | Narrows a raw `request.method` string into a typed `Method` — total, never throws. |
98
+
99
+ ### Guards
100
+
101
+ In a guard table a `Shape` cell holds the type the guard narrows to.
102
+
103
+ | API | Kind | Shape | Summary |
104
+ | ------------------- | -------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
105
+ | `isEncryptedSocket` | function | `{ encrypted }` | Determines whether a `node:http` connection socket is TLS-encrypted — the total, never-throwing narrow `buildRequest` uses to pick the derived scheme (`https` vs `http`). |
106
+
107
+ ### Handlers
108
+
109
+ | API | Kind | Summary |
110
+ | ----------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
111
+ | `handleListenerRequest` | function | Handles one `node:http` request through a core dispatcher and writes its fetch-standard response. |
112
+ | `createListener` | function | Creates a `node:http` request listener over a core `DispatcherInterface` — the whole server face's entry point: converts the incoming message to a fetch `Request`, hands it to the dispatcher with the consumer's per-request `state`, and writes the resulting `Response` back. |
113
+
114
+ ### Classes
115
+
116
+ | API | Kind | Summary |
117
+ | --------------- | ----- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
118
+ | `Router` | class | Represents the path-matching + registry engine — registers `{ path, meta, name? }` entries (compiling each path once) and resolves a concrete pathname to the most specific matching entry. The shared machine both the `Navigator` (browser) and the `Dispatcher` (core, method-dimensioned) compose. |
119
+ | `Group` | class | Represents a prefix-scoped registration handle over a `Router` — pure string composition, no independent state or storage. |
120
+ | `Dispatcher` | class | Represents the fetch-standard, method-dimensioned dispatch entity — layers HTTP method dispatch and web-standard `Request`/`Response` handling over one internal `Router<RouteRecord<TState>>`. The core machine the server face and any fetch-native runtime consume directly. |
121
+ | `DispatchGroup` | class | Represents a prefix-scoped registration handle over a `Dispatcher` — the method-dimensioned counterpart of `Group`. |
122
+ | `Navigator` | class | Represents the headless History/hash navigation entity — composes one core `Router<Meta>`, resolving the current location on `start()` and every subsequent navigation event, tracking `active`, and emitting `navigate` through the core `Emitter`. No `render` / `outlet` — the consumer owns rendering. |
123
+
124
+ ### Types
125
+
126
+ A `Shape` cell holds an interface's data members as bare names in braces, `?` marking an optional member and `plus` introducing its call-signature members, and a type alias's own type literal with a union's arms escaped as `\|`.
127
+
128
+ | Type | Kind | Shape | Summary |
129
+ | ------------------------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
130
+ | `PathParams` | type | `{ readonly [K in keyof PathParamsRaw<Path>]: PathParamsRaw<Path>[K] }` | Extracts `{ name: string }` param records from a path pattern at the type level — the typed half of the path grammar. |
131
+ | `PathParamsRaw` | type | ``string extends Path ? Readonly<Record<string, string>> : Path extends `${infer Segment}/${infer Rest}` ? SegmentParam<Segment> & PathParamsRaw<Rest> : SegmentParam<Path>`` | Performs recursive, unflattened param extraction for `PathParams` — walks a path pattern segment by segment (split on `/`), extracting each segment's `SegmentParam` contribution and intersecting the rest. |
132
+ | `IdentifierStartChar` | type | `'a' \| 'b' \| 'c' \| 'd' \| 'e' \| 'f' \| 'g' \| 'h' \| 'i' \| 'j' \| 'k' \| 'l' \| 'm' \| 'n' \| 'o' \| 'p' \| 'q' \| 'r' \| 's' \| 't' \| 'u' \| 'v' \| 'w' \| 'x' \| 'y' \| 'z' \| 'A' \| 'B' \| 'C' \| 'D' \| 'E' \| 'F' \| 'G' \| 'H' \| 'I' \| 'J' \| 'K' \| 'L' \| 'M' \| 'N' \| 'O' \| 'P' \| 'Q' \| 'R' \| 'S' \| 'T' \| 'U' \| 'V' \| 'W' \| 'X' \| 'Y' \| 'Z' \| '_'` | Names the identifier start characters an identifier-grammar param name may begin with — mirrors the runtime classifier's `[A-Za-z_]` head class, the one `classifySegment` and `compilePath` share. |
133
+ | `IdentifierChar` | type | `IdentifierStartChar \| '0' \| '1' \| '2' \| '3' \| '4' \| '5' \| '6' \| '7' \| '8' \| '9'` | Names the identifier continuation characters after the first — mirrors the runtime classifier's `[A-Za-z0-9_]*` tail class. |
134
+ | `TakeIdentifierTail` | type | ``S extends `${infer Head}${infer Tail}` ? Head extends IdentifierChar ? TakeIdentifierTail<Tail, `${Acc}${Head}`> : Acc : Acc`` | Consumes the identifier-continuation run at the front of a string literal, char by char, appending each onto the accumulator. |
135
+ | `IdentifierHead` | type | ``S extends `${infer Head}${infer Tail}` ? Head extends IdentifierStartChar ? TakeIdentifierTail<Tail, Head> : '' : ''`` | Captures the identifier at the front of a string literal, or an empty string when the literal does not begin with an identifier-start char. |
136
+ | `SegmentParam` | type | ``Segment extends `:${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : { readonly [K in Name]: string } : unknown : Segment extends `*${infer Rest}` ? IdentifierHead<Rest> extends infer Name extends string ? Name extends '' ? unknown : { readonly [K in Name]: string } : unknown : unknown`` | Contributes one path segment's type-level param record — the type-level mirror of the runtime `classifySegment` and `compilePath` segment parser. |
137
+ | `CompiledPath` | interface | `{ regex, params }` | Represents a compiled route path — the anchored regex plus its ordered param names. |
138
+ | `RouteEntry` | interface | `{ path, meta, name? }` | Represents one registered route in a `RouterInterface` — the `path` pattern plus the opaque `meta` payload to return on a match, with an optional `name`. |
139
+ | `RouterMatch` | interface | `{ path, params, meta, name? }` | Represents one matched route — the winning entry's registered pattern, its decoded params, its `meta` payload, and its optional `name`. |
140
+ | `AnswerHandler` | type | `(meta: Meta) => boolean` | Represents the native-override seam — a predicate deciding whether an entry's `meta` answers a given `match` call, beyond path matching. |
141
+ | `RouterOptions` | interface | `{ entries?, sensitive?, key? }` | Represents the options for `createRouter` — an optional initial entry set, the case-sensitivity toggle, and the dedup identity function. |
142
+ | `RouterInterface` | interface | `{ count } plus add, match, entries, group, clear` | Represents the path-matching + registry engine contract (the behavioral-interface role for the one-class-per-file `Router`). Registers `{ path, meta, name? }` entries (compiling each path once) and resolves a concrete pathname to the most specific matching entry — a literal segment beats a param beats a wildcard at the earliest differing segment, registration-order-independent. The shared engine both the `Navigator` (browser) and the `Dispatcher` (core, method-dimensioned) compose. |
143
+ | `GroupInterface` | interface | `{ prefix } plus add, group` | Represents a prefix-scoped registration handle over a `RouterInterface` — pure string composition, no independent state or storage. |
144
+ | `Method` | type | `'GET' \| 'POST' \| 'PUT' \| 'PATCH' \| 'DELETE' \| 'HEAD' \| 'OPTIONS'` | Names the HTTP methods a `DispatcherInterface` dimensions dispatch over — derived from `METHOD_LIST`, whose membership counterpart is `METHODS`. |
145
+ | `RouteContext` | interface | `{ params, pattern, url, state }` | Represents the ambient context a `RouteHandler` receives alongside the raw `Request` — decoded params, the winning pattern, the parsed URL, and the consumer's opaque per-request state. |
146
+ | `RouteHandler` | type | `(request: Request, context: RouteContext<Path, TState>) => Response \| Promise<Response>` | Receives the raw fetch `Request` plus its typed `RouteContext` and returns (or resolves) a fetch `Response`. |
147
+ | `RouteInput` | interface | `{ method, path, handler, name? }` | Represents one route registration input for `DispatcherInterface.add` — the method-dimensioned counterpart of `RouteEntry`. |
148
+ | `RouteRecord` | interface | `{ method, handler, name? }` | Represents the `meta` payload a `DispatcherInterface` stores in its underlying `Router` — what `RouterInterface.match` returns as `RouterMatch.meta` on a dispatch hit. |
149
+ | `DispatchResult` | type | `{ status: 'matched', match } \| { status: 'unmethoded', allow } \| { status: 'unmatched' }` | Represents the outcome of `DispatcherInterface.match` — a discriminated union over the dispatch tiers: a full hit, a path that matches with no route for the method (405 territory), or nothing matched at all (404 territory). |
150
+ | `DispatcherEventMap` | type | `{ match, miss }` | Represents the `Dispatcher`'s event map — the dispatch-outcome signals a consumer can observe alongside the return value of `handle`. |
151
+ | `DispatcherOptions` | interface | `{ routes?, sensitive?, unmatched?, unmethoded?, on?, error? }` | Represents the options for `createDispatcher` — initial routes, case sensitivity, the default-responder overrides, and the Emitter pattern's wiring. |
152
+ | `DispatcherInterface` | interface | `{ router, emitter } plus add, group, match, handle, destroy` | Represents the fetch-standard, method-dimensioned dispatch entity contract (the behavioral-interface role for the one-class-per-file `Dispatcher`). Layers HTTP method dispatch and web-standard `Request`/`Response` handling over a single internal `Router<RouteRecord<TState>>`. |
153
+ | `DispatchGroupInterface` | interface | `{ prefix } plus add, group` | Represents a prefix-scoped registration handle over a `DispatcherInterface` — the method-dimensioned counterpart of `GroupInterface`. |
154
+ | `NavigatorEventMap` | type | `{ navigate }` | Represents the `Navigator`'s event map — the single `navigate` signal a consumer observes. |
155
+ | `NavigatorOptions` | interface | `{ routes, history?, base?, fallback?, guard?, intercept?, sensitive?, on?, error? }` | Represents the options for `createNavigator` — the `routes` to dispatch between, the navigation substrate, the optional guard hook, and the Emitter pattern's wiring. |
156
+ | `NavigatorInterface` | interface | `{ router, emitter, active } plus start, stop, navigate, match, destroy` | Represents the headless History/hash navigation entity contract (the behavioral-interface role for the one-class-per-file `Navigator`). Composes a core `Router<Meta>`, resolves the current location on `start()` and on every subsequent navigation event, tracks `active`, and emits `navigate` through the `EmitterInterface`. |
157
+ | `RequestOptions` | interface | `{ origin?, response? }` | Represents the options for `buildRequest` — URL origin and response-side disconnect tracking. |
158
+ | `ListenerFunction` | type | `(request: IncomingMessage, response: ServerResponse) => void` | Represents a `node:http` request handler — the function `createListener` returns, matching `http.createServer`'s handler signature. |
159
+ | `StateFunction` | type | `(message: IncomingMessage) => TState` | Derives a consumer's opaque per-request `TState` from the raw `IncomingMessage` — the `state` argument `createListener` threads into `dispatcher.handle`. |
160
+
161
+ The `count` member of `RouterInterface`, the `prefix` member of `GroupInterface` and
162
+ `DispatchGroupInterface`, the `router` and `emitter` members of `DispatcherInterface`, and the
163
+ `router`, `emitter`, and `active` members of `NavigatorInterface` are all `readonly` data members
164
+ (the preceding Surface rows) — the call-signature members each `Shape` cell names after `plus` are
165
+ documented under [Methods](#methods).
166
+
167
+ ## Methods
168
+
169
+ The public methods of `RouterInterface`, `GroupInterface`,
170
+ `DispatcherInterface`, `DispatchGroupInterface`, and `NavigatorInterface` —
171
+ every call-signature member listed (their `readonly` data members stay
172
+ Surface rows). `Router`, `Group`, `Dispatcher`, `DispatchGroup`, and
173
+ `Navigator` implement their interfaces exactly, so this doubles as each
174
+ class's instance-method surface.
175
+
176
+ #### `RouterInterface`
177
+
178
+ The registry engine's call-signature members, each documented on the interface
179
+ declaration it belongs to:
180
+
181
+ | Method | Returns | Summary |
182
+ | --------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
183
+ | `add` | `void` | Registers one entry, or many in one call (batch registration), compiling each path once; throws a `ContractError` on a malformed path. |
184
+ | `match` | `RouterMatch \| undefined` | Resolves the most-specific matching entry for a pathname, or `undefined` when nothing matches. |
185
+ | `entries` | `readonly RouteEntry[]` | Lists every registered entry in registration order, or only those whose path matches a given pathname. |
186
+ | `group` | `GroupInterface` | Returns a prefix-scoped registration handle over this router. |
187
+ | `clear` | `void` | Drops every entry, leaving the router reusable. |
188
+
189
+ #### `DispatcherInterface`
190
+
191
+ The dispatch entity's call-signature members, each documented on the interface
192
+ declaration it belongs to:
193
+
194
+ | Method | Returns | Summary |
195
+ | --------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
196
+ | `add` | `void` | Registers one route input, or many in one call (batch registration); throws a `ContractError` on a malformed registration. |
197
+ | `group` | `DispatchGroupInterface` | Returns a prefix-scoped registration handle over this dispatcher. |
198
+ | `match` | `DispatchResult` | Decides the raw `DispatchResult` for a method and pathname pair, with no `Request` or `Response` involvement — the pure decision `handle` builds its response from. |
199
+ | `handle` | `Promise<Response>` | Runs the full dispatch: parses the request URL, matches, and invokes either the winning handler or the `unmatched`/`unmethoded` responder. |
200
+ | `destroy` | `void` | Tears down the emitter; the underlying router is left registered rather than cleared, so introspection stays valid afterwards. |
201
+
202
+ #### `NavigatorInterface`
203
+
204
+ The navigation entity's call-signature members, each documented on the interface
205
+ declaration it belongs to:
206
+
207
+ | Method | Returns | Summary |
208
+ | ---------- | -------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
209
+ | `start` | `void` | Begins listening and resolves the current location — idempotent, so a second call is a no-op. |
210
+ | `stop` | `void` | Stops listening and aborts any pending guard — idempotent. |
211
+ | `navigate` | `void` | Navigates programmatically — sets `location.hash` in hash mode or calls `history.pushState` in history mode, then resolves. |
212
+ | `match` | `RouterMatch \| undefined` | Looks one path up through the underlying `Router` — a pure lookup with no location read, no fallback, no guard, and no emit. |
213
+ | `destroy` | `void` | Stops listening and tears down the emitter. |
214
+
215
+ #### `GroupInterface`
216
+
217
+ The group handle's call-signature members — a group holds no registry of its own,
218
+ and every registration lands on the owning router:
219
+
220
+ | Method | Returns | Summary |
221
+ | ------- | ---------------- | ---------------------------------------------------------------------------------------------------------------- |
222
+ | `add` | `void` | Registers one entry, or many in one call, on the owning router with this group's prefix composed onto each path. |
223
+ | `group` | `GroupInterface` | Returns a nested group whose prefix is this prefix followed by the given one. |
224
+
225
+ #### `DispatchGroupInterface`
226
+
227
+ The dispatch group handle's call-signature members — the owning dispatcher's
228
+ registration guard still applies to every route a group registers:
229
+
230
+ | Method | Returns | Summary |
231
+ | ------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
232
+ | `add` | `void` | Registers one route input, or many in one call, on the owning dispatcher with this group's prefix composed onto each path. |
233
+ | `group` | `DispatchGroupInterface` | Returns a nested group whose prefix is this prefix followed by the given one. |
234
+
235
+ ## Contract
236
+
237
+ These invariants hold across `src/core` / `src/browser` / `src/server` ↔
238
+ `router.md`.
239
+
240
+ 1. **Doc-to-source bijection.** Every `function` / `class` / `interface` /
241
+ `type` / `const` row in the `## Surface` tables is a real export of its
242
+ source directory, and every export appears as a Surface row — exhaustive,
243
+ both directions.
244
+ 2. **Doc-to-source method bijection.** The `## Methods` tables list exactly
245
+ `RouterInterface`'s, `GroupInterface`'s, `DispatcherInterface`'s,
246
+ `DispatchGroupInterface`'s, and `NavigatorInterface`'s public methods —
247
+ exhaustive, both directions — and `Router` / `Group` / `Dispatcher` /
248
+ `DispatchGroup` / `Navigator` expose the same public methods, no more.
249
+ 3. **Path grammar.** Segment kinds: literal (`/users`), param
250
+ (`:name`, one segment), wildcard (`*name`, final segment only, captures
251
+ the rest of the path including slashes). A wildcard anywhere but the
252
+ final segment throws a `ContractError` at compile time, guarded at the boundary.
253
+ 4. **Precedence tiers.** A literal segment (`TIER_LITERAL`, 2) outranks a
254
+ param (`TIER_PARAM`, 1), which outranks a wildcard (`TIER_WILDCARD`, 0).
255
+ Two matching routes compare left to right, and the higher tier at the
256
+ earliest differing segment wins, independent of registration order. A
257
+ shorter pattern that is a prefix of a longer one ranks below it. An
258
+ equal-specificity tie, possible only between distinct wildcard shapes,
259
+ resolves to the earliest registered.
260
+ 5. **Trailing slash is insensitive.** A single trailing slash on the request
261
+ path is always optional, folded both at registration (`canonicalizePath`)
262
+ and at match time — except the root `/` and the empty pattern, which are
263
+ exempt and anchor exactly.
264
+ 6. **Case-sensitive by default.** `sensitive: true` (`Router`/`Dispatcher`
265
+ construction) is the default; `sensitive: false` folds case during
266
+ matching without altering the pattern's own stored casing.
267
+ 7. **Dedup with `key`.** When `RouterOptions.key` is set, an entry whose
268
+ computed key already exists replaces the prior entry in place (last write
269
+ wins, no engine rebuild); `Dispatcher` always constructs its internal
270
+ `Router` with `key: computeDispatchKey`, which pairs the route's method
271
+ with `canonicalizePath(entry.path)`, so two registrations differing only
272
+ by a trailing slash replace each other while different methods stay
273
+ distinct.
274
+ 8. **The `answers` seam.** `RouterInterface.match`'s optional
275
+ `AnswerHandler<Meta>` predicate is the single native-override point both
276
+ faces compose differently: the `Dispatcher` passes a method-check, the
277
+ browser `Navigator` omits it entirely — every path match answers.
278
+ 9. **Dispatch semantics.** A `HEAD` request with no explicit `HEAD` route
279
+ runs the matching `GET` handler and strips the response body. An
280
+ `OPTIONS` request with no explicit `OPTIONS` route answers `204` with a
281
+ derived `Allow` header (from `router.entries(pathname)`, `GET` implying
282
+ `HEAD`). A path-matches-but-method-doesn't dispatch invokes the
283
+ `unmethoded` responder (default `405` + `Allow`); nothing matching
284
+ invokes the `unmatched` responder (default `404`). **A handler throw
285
+ propagates uncaught** — the dispatcher never invents an error boundary
286
+ (that is the consuming server's policy).
287
+ 10. **Wildcard trailing-slash capture is asymmetric with param folding
288
+ (intended).** A final `*name` wildcard captures any trailing slash on the
289
+ request path into its own captured value (`/files/a/b/` → `rest: 'a/b/'`)
290
+ — unlike a `:name` param segment, whose own trailing slash is folded away
291
+ by the shared trailing-slash-insensitivity rule stated earlier. This is
292
+ deliberate: the wildcard's capture is "the rest of the path, verbatim,"
293
+ including whatever trailing slash the caller sent.
294
+ 11. **Event map.** `DispatcherEventMap` carries `match` (emitted on every
295
+ dispatch the dispatcher answers, the derived `HEAD` and `OPTIONS` cases
296
+ included; for a derived `OPTIONS` answer the `pattern` is the
297
+ most-specific pattern the pathname resolved to) and `miss` (emitted on
298
+ every non-matching dispatch, tagged
299
+ `'unmatched'`/`'unmethoded'` through its `status` field) — no
300
+ `error`/`observerError` domain event (listener errors route through the
301
+ emitter's own `error` option).
302
+ 12. **Headless by design.** No `render`/`outlet`. The `Navigator` resolves,
303
+ tracks `active`, and emits `navigate`; rendering is entirely the
304
+ consumer's responsibility.
305
+ 13. **One shared engine.** Each route's `path` is registered once on the same
306
+ `Router` machine the core `Dispatcher` composes, keyed for dedup by its
307
+ `canonicalizePath` (last write wins, replace-in-place). `Navigator` never
308
+ rebuilds matching logic of its own.
309
+ 14. **The history toggle.** `history: false` (default) reads/writes
310
+ `location.hash` and binds `hashchange`; `history: true` reads/writes through
311
+ `pushState`/`popstate`, with an optional `base` path prefix stripped
312
+ before matching and prepended when navigating. `intercept: true`
313
+ (history mode only) adds same-origin `<a>` click interception — a plain
314
+ left-click with no modifier keys, no `target`, and no `download`
315
+ attribute.
316
+ 15. **Fallback semantics.** A location that matches nothing resolves the
317
+ configured `fallback` pattern (default: the first route's path) through
318
+ the same engine. A `fallback` that itself matches no registered route
319
+ leaves `active` `undefined` and emits nothing — no phantom match is ever
320
+ fabricated.
321
+ 16. **Guard + supersede semantics.** An optional `guard(to, from, signal)` may
322
+ veto (or asynchronously veto) a navigation. The `Navigator` mints an
323
+ `@orkestrel/abort` handle per navigation and aborts the previous handle
324
+ when a newer navigation starts (or on `stop`/`destroy`) — a guard verdict
325
+ that resolves after its navigation was superseded (`signal.aborted`) is
326
+ discarded, same as a synchronous `false`/rejected verdict: `active` stays
327
+ unchanged and nothing is emitted. A guard throw routes to the `error`
328
+ handler (not through the emitter's own `emit`) and vetoes.
329
+ 17. **Case-sensitive by default.** `sensitive: true` (forwarded to the
330
+ underlying `Router`) is the default; `sensitive: false` folds case during
331
+ matching.
332
+ 18. **Intercepted links carry pathname only (known limitation).** Click
333
+ interception passes only the intercepted link's `/`-prefixed pathname
334
+ through to `navigate` — a query string on the link's `href` is not
335
+ preserved (the pathname-only grammar has no query concept). A consumer
336
+ needing query data reads it from `window.location.search` after
337
+ navigating, or skips interception for that link.
338
+ 19. **Only HTML `<a>` elements are intercepted (known limitation).** Click
339
+ interception ({@link findAnchor}) walks up the event's composed path for
340
+ an `HTMLAnchorElement` — an SVG `<a>` (`SVGAElement`) is not intercepted,
341
+ even inside a same-origin document, and falls through to the browser's
342
+ native navigation.
343
+ 20. **Signal fires on client disconnect.** `buildRequest` mints an
344
+ `@orkestrel/abort` handle and builds the `Request` over its `signal`. The
345
+ handle aborts if the request connection closes before the message finished
346
+ (`!message.complete`), preserving that incomplete-request error, or if the
347
+ paired `RequestOptions.response` closes before the response finished
348
+ (`!response.writableEnded`). `handleListenerRequest` always supplies that
349
+ response, so a handler observes both an incomplete request body and the
350
+ ordinary post-request client disconnect through `request.signal`, with zero
351
+ router-specific cancellation API. A normally completed response does not
352
+ abort the signal, and each close observer is one-shot.
353
+ 21. **Transport-level 500 is a last resort, not an error policy.**
354
+ `createListener`'s handler wraps `dispatcher.handle` in a try/catch purely
355
+ for the connection: when nothing has been sent yet, it writes a bare `500`
356
+ head and ends the response (never leaking a hanging socket); after headers
357
+ are already sent, it destroys the connection outright. The router still
358
+ owns no error policy — a consumer wanting mapped error responses installs
359
+ its own boundary around `dispatcher.handle` directly; and the core
360
+ `Dispatcher` never swallows a handler throw into a generic response, as
361
+ stated earlier.
362
+ 22. **Streaming both ways.** `buildRequest` streams a body-carrying method's
363
+ message into the `Request` through a manual `ReadableStream` pump — a `for
364
+ await` loop over the `IncomingMessage` enqueueing each chunk, with
365
+ `duplex: 'half'` set as Node's fetch implementation requires for a
366
+ streamed request body; `sendResponse` streams a non-`null` `Response`
367
+ body back to the `ServerResponse` chunk by chunk, ending the target when
368
+ the stream completes. When a write reports backpressure, it waits for
369
+ `drain` before pulling the next body chunk, raced against target
370
+ close/error/destruction so a client disconnect stops the pump promptly;
371
+ every race listener is removed when that wait settles. A target destroyed
372
+ mid-stream stops cleanly without throwing.
373
+ 23. **Header fidelity.** `buildRequest` copies every incoming header
374
+ (multi-value headers comma-joined, except `set-cookie`, appended
375
+ individually); `sendResponse` writes every outgoing header and re-derives
376
+ `set-cookie` through `Headers.getSetCookie()` so multiple response cookies
377
+ stay distinct instead of collapsing into one comma-joined header.
378
+
379
+ ## Patterns
380
+
381
+ ### Groups and dedup
382
+
383
+ `group(prefix)` scopes a registration handle that composes its prefix onto
384
+ every entry it registers on the same underlying router; a `key` function
385
+ lets a later registration replace an earlier one in place instead of adding
386
+ a duplicate candidate.
387
+
388
+ ```ts
389
+ import { createRouter } from '@orkestrel/router'
390
+
391
+ const router = createRouter<{ readonly page: string }>({
392
+ key: (entry) => entry.path,
393
+ })
394
+ const api = router.group('/api')
395
+ api.add({ path: '/users', meta: { page: 'list' } })
396
+ router.match('/api/users')?.path // '/api/users'
397
+
398
+ router.add({ path: '/api/users', meta: { page: 'list-v2' } }) // replaces the prior entry
399
+ ```
400
+
401
+ ### Wildcard capture and precedence
402
+
403
+ A literal segment always outranks a param, which always outranks a wildcard,
404
+ compared left-to-right at the earliest differing segment:
405
+
406
+ ```ts
407
+ import { createRouter } from '@orkestrel/router'
408
+
409
+ const router = createRouter<{ readonly handler: string }>()
410
+ router.add([
411
+ { path: '/files/*rest', meta: { handler: 'catchAll' } },
412
+ { path: '/files/:name', meta: { handler: 'named' } },
413
+ { path: '/files/readme', meta: { handler: 'literal' } },
414
+ ])
415
+ router.match('/files/readme')?.meta.handler // 'literal'
416
+ router.match('/files/other')?.meta.handler // 'named'
417
+ router.match('/files/a/b.png')?.meta.handler // 'catchAll'
418
+ ```
419
+
420
+ ### Method-dimensioned dispatch (auto-HEAD, auto-OPTIONS, 405)
421
+
422
+ Registering a single `GET` route yields an auto-derived `HEAD`, an auto-derived `OPTIONS`,
423
+ and a `405` for every other method on that path:
424
+
425
+ ```ts
426
+ import { createDispatcher } from '@orkestrel/router'
427
+
428
+ const dispatcher = createDispatcher()
429
+ dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
430
+
431
+ const head = await dispatcher.handle(new Request('http://x/health', { method: 'HEAD' }), undefined)
432
+ head.body // null — auto-HEAD strips the GET handler's body
433
+
434
+ const options = await dispatcher.handle(
435
+ new Request('http://x/health', { method: 'OPTIONS' }),
436
+ undefined,
437
+ )
438
+ options.headers.get('Allow') // 'GET, HEAD, OPTIONS'
439
+
440
+ const notAllowed = await dispatcher.handle(
441
+ new Request('http://x/health', { method: 'DELETE' }),
442
+ undefined,
443
+ )
444
+ notAllowed.status // 405
445
+ ```
446
+
447
+ ### Observing dispatch outcomes
448
+
449
+ The `on` hooks report every dispatch outcome, matched or missed, alongside the return
450
+ value of `handle`:
451
+
452
+ ```ts
453
+ import { createDispatcher } from '@orkestrel/router'
454
+
455
+ const dispatcher = createDispatcher({
456
+ on: {
457
+ match: (method, pattern) => console.log('matched', method, pattern),
458
+ miss: (method, pathname, status) => console.log('missed', method, pathname, status),
459
+ },
460
+ })
461
+ dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
462
+ await dispatcher.handle(new Request('http://x/missing'), undefined) // logs a 'miss'
463
+ ```
464
+
465
+ ### Typing a route input at the registration site
466
+
467
+ `defineRoute(...)` is a pure identity pass-through with a `const Path extends
468
+ string` generic — wrapping a route literal in it pins `Path` to the literal
469
+ string at the call site (instead of the widened `string` a bare intermediate
470
+ binding would get), so `context.params` types correctly through
471
+ `PathParams` even when the input is built before the `add` call:
472
+
473
+ ```ts
474
+ import { createDispatcher, defineRoute } from '@orkestrel/router'
475
+
476
+ const input = defineRoute({
477
+ method: 'GET',
478
+ path: '/users/:id',
479
+ handler: (_request, context) => new Response(context.params.id), // typed string
480
+ })
481
+
482
+ const dispatcher = createDispatcher()
483
+ dispatcher.add(input)
484
+ ```
485
+
486
+ A heterogeneous `RouteInput[]` built by collecting several `defineRoute(...)`
487
+ results still widens each element's `Path` to `string` the moment the array
488
+ type is inferred — TypeScript has no per-element literal-preserving array
489
+ type. The realistic ceiling `defineRoute` raises is per-call typing at the
490
+ registration site (a single `defineRoute({...})` or a direct `add({...})`
491
+ call), not a stored, still-literal-typed array of route records.
492
+
493
+ ### Introspection and reset
494
+
495
+ `entries()` lists every registration (or only those matching a pathname —
496
+ the same set a 405 response's `Allow` header derives from); `clear()` drops
497
+ every entry while leaving the router usable; `Dispatcher.destroy()` tears
498
+ down its emitter.
499
+
500
+ ```ts
501
+ import { createDispatcher, createRouter } from '@orkestrel/router'
502
+
503
+ const router = createRouter<{ readonly page: string }>()
504
+ router.add([
505
+ { path: '/users/:id', meta: { page: 'profile' } },
506
+ { path: '/tokens', meta: { page: 'tokens' } },
507
+ ])
508
+ router.entries().length // 2
509
+ router.entries('/users/7').length // 1 — only the matching entry
510
+ router.clear()
511
+ router.entries().length // 0 — the router stays usable
512
+
513
+ const dispatcher = createDispatcher()
514
+ dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
515
+ dispatcher.destroy() // tears down the #emitter; router.entries() is still valid afterward
516
+ ```
517
+
518
+ ### Hash-mode navigation
519
+
520
+ A `Navigator` in hash mode dispatches on `location.hash` and updates `active` after
521
+ each `hashchange`:
522
+
523
+ ```ts
524
+ import { createNavigator } from '@orkestrel/router/browser'
525
+
526
+ const navigator = createNavigator({
527
+ routes: [
528
+ { path: '/', meta: { title: 'Home' } },
529
+ { path: '/about', meta: { title: 'About' } },
530
+ ],
531
+ })
532
+ navigator.emitter.on('navigate', (match) => (document.title = match.meta.title))
533
+ navigator.start()
534
+ navigator.match('/about')?.meta.title // 'About' — a pure lookup, no location read
535
+ navigator.navigate('/about') // sets location.hash; `active` updates after the hashchange fires
536
+ navigator.stop()
537
+ navigator.destroy() // stop() plus tear down the #emitter
538
+ ```
539
+
540
+ ### History mode with link interception
541
+
542
+ History mode binds `popstate` and, with `intercept` set, same-origin `<a>` clicks:
543
+
544
+ ```ts
545
+ import { createNavigator } from '@orkestrel/router/browser'
546
+
547
+ const navigator = createNavigator({
548
+ routes: [{ path: '/users/:id', meta: { title: 'User' } }],
549
+ history: true,
550
+ base: '/app',
551
+ intercept: true,
552
+ })
553
+ navigator.start() // binds popstate + same-origin <a> click interception
554
+ ```
555
+
556
+ ### Guarding navigation (auth walls)
557
+
558
+ A guard may veto synchronously or asynchronously; a superseded guard's
559
+ verdict is discarded through its own `signal`.
560
+
561
+ ```ts
562
+ import { createNavigator } from '@orkestrel/router/browser'
563
+
564
+ const navigator = createNavigator({
565
+ routes: [
566
+ { path: '/private', meta: { title: 'Private' } },
567
+ { path: '/', meta: { title: 'Home' } },
568
+ ],
569
+ guard: async (to, _from, signal) => {
570
+ const allowed = await checkAuth({ signal }) // cancels its own work if superseded
571
+ return signal.aborted ? false : allowed
572
+ },
573
+ })
574
+ navigator.start()
575
+ ```
576
+
577
+ ### Basic server
578
+
579
+ `createListener` adapts a core `Dispatcher` into a `node:http` request listener:
580
+
581
+ ```ts
582
+ import { createListener } from '@orkestrel/router/server'
583
+ import { createDispatcher } from '@orkestrel/router'
584
+ import http from 'node:http'
585
+
586
+ const dispatcher = createDispatcher<{ readonly requestId: string }>()
587
+ dispatcher.add({
588
+ method: 'GET',
589
+ path: '/users/:id',
590
+ handler: (_request, context) =>
591
+ Response.json({ id: context.params.id, requestId: context.state.requestId }),
592
+ })
593
+
594
+ const server = http.createServer(
595
+ createListener(dispatcher, () => ({ requestId: crypto.randomUUID() })),
596
+ )
597
+ server.listen(0)
598
+ ```
599
+
600
+ ### Converting requests and responses directly
601
+
602
+ For a runtime seam that needs finer control than `createListener` (custom
603
+ error handling around `dispatcher.handle`, for instance), compose
604
+ `buildRequest`/`sendResponse` directly:
605
+
606
+ ```ts
607
+ import { buildRequest, sendResponse } from '@orkestrel/router/server'
608
+ import { createDispatcher } from '@orkestrel/router'
609
+ import http from 'node:http'
610
+
611
+ const dispatcher = createDispatcher()
612
+ dispatcher.add({ method: 'GET', path: '/health', handler: () => new Response('ok') })
613
+
614
+ const server = http.createServer(async (incoming, target) => {
615
+ const request = buildRequest(incoming, {
616
+ origin: 'https://api.example.com',
617
+ response: target,
618
+ })
619
+ try {
620
+ const response = await dispatcher.handle(request, undefined)
621
+ await sendResponse(response, target)
622
+ } catch (error) {
623
+ target.writeHead(500).end(String(error)) // this consumer's own error policy
624
+ }
625
+ })
626
+ server.listen(0)
627
+ ```
628
+
629
+ ### Observing client disconnect
630
+
631
+ The `Request` returned by `buildRequest` carries a `signal` that aborts when the connection
632
+ closes before the response completes:
633
+
634
+ ```ts
635
+ import { buildRequest } from '@orkestrel/router/server'
636
+ import http from 'node:http'
637
+
638
+ const server = http.createServer((incoming, response) => {
639
+ const request = buildRequest(incoming, { response })
640
+ request.signal.addEventListener('abort', () => console.log('client disconnected'))
641
+ })
642
+ ```
643
+
644
+ ### Practices
645
+
646
+ - **One engine, one seam per face** — compose `Router` directly for a
647
+ method-less consumer (a `Navigator`), or through `Dispatcher` for
648
+ method-dimensioned fetch dispatch; never rebuild the matching logic per
649
+ face.
650
+ - **Guard the registration boundary, not the hot path** — `add` throws a
651
+ `ContractError` on a malformed entry; `match`/`handle` carry zero guards.
652
+ - **Let handler throws propagate** — the dispatcher is not an error
653
+ boundary; a consuming server installs its own around `handle`.
654
+ - **Dedup with `key`, not manual lookups** — pass a `key` function instead of
655
+ checking `router.entries()` before every `add`.
656
+ - **Never build a second registry** — compose the same core `Router` other
657
+ faces use; a `Navigator` never hand-rolls its own path matching.
658
+ - **Thread `signal` into async guard work** — a slow guard can cancel its own
659
+ work when it observes `signal.aborted`, closing the stale-guard race.
660
+ - **Keep rendering outside the Navigator** — subscribe to `navigate` and
661
+ render in the consumer, never inside this headless entity.
662
+ - **`stop()`/`destroy()` before disposal** — releases listeners and aborts
663
+ any pending guard; `destroy()` also tears down the `#emitter`.
664
+ - **Prefer `createListener` for the common case** — it wires conversion,
665
+ dispatch, and the transport-level last-resort `500` together correctly.
666
+ - **Install your own error boundary for mapped error responses** — the
667
+ router (core and this adapter) never invents one; a handler throw
668
+ propagates.
669
+ - **Thread `request.signal` into downstream work** — a handler can cancel
670
+ its own I/O when the client disconnects, the fetch-standard idiom.
671
+ - **Skip this face entirely on fetch-native runtimes** — Bun, Deno, and
672
+ workers hand `Request`s to `dispatcher.handle` directly.
673
+
674
+ ## Tests
675
+
676
+ - [`tests/guides.test.ts`](../tests/guides.test.ts) — the `## Surface` ↔
677
+ `src/core` / `src/browser` / `src/server` bijection (value + type exports), the
678
+ interface-to-class method bijection for `RouterInterface`, `GroupInterface`,
679
+ `DispatcherInterface`, `DispatchGroupInterface`, and `NavigatorInterface`, and the
680
+ equality gate: every `Summary` cell against its declaration's description paragraph,
681
+ the titled `Basic server` fence against the `@example` block of that title (pinned so
682
+ the titled pair cannot be retired silently), and the README pitch against this guide's
683
+ tagline. It also runs the flagship fences this project can execute and asserts the
684
+ values their comments claim.
685
+ - [`tests/src/core/Router.test.ts`](../tests/src/core/Router.test.ts) —
686
+ registration boundary guard, method-less matching, order-independent
687
+ literal-over-param-over-wildcard precedence, wildcard capture, the
688
+ `answers` seam, `entries()` (all + filtered), dedup through `key`, case
689
+ sensitivity, and `RouterInterface` conformance.
690
+ - [`tests/src/core/Group.test.ts`](../tests/src/core/Group.test.ts) —
691
+ `Group` direct construction, prefix composition and nesting, batch
692
+ registration, dedup-key collision across differently-nested group chains,
693
+ and `GroupInterface` conformance.
694
+ - [`tests/src/core/Dispatcher.test.ts`](../tests/src/core/Dispatcher.test.ts) —
695
+ type-level surfaces (`RouteHandler` context typing, `TState` generic flow,
696
+ `DispatcherInterface` member shape, factory return type), emitter event
697
+ payload shapes, destroy idempotence, the cross-face grammar parity
698
+ fixture, the full functional dispatch matrix (auto-HEAD, auto-OPTIONS,
699
+ 404/405 responders, handler-throw propagation), and per-method dedup.
700
+ - [`tests/src/core/DispatchGroup.test.ts`](../tests/src/core/DispatchGroup.test.ts) —
701
+ `DispatchGroup` direct construction and group + nested group registration
702
+ with prefixes composed.
703
+ - [`tests/src/core/helpers.test.ts`](../tests/src/core/helpers.test.ts) —
704
+ `escapeRegExp`, `canonicalizePath`, `computeDispatchKey`, `compilePath` (literal/param/wildcard,
705
+ trailing-slash folding, case sensitivity, the wildcard-not-final throw),
706
+ `decodeParam` (including a malformed `%` escape), `matchPath`,
707
+ `classifySegment` (the literal-vs-param classification fix regression
708
+ case), `computeSpecificity`, `compareSpecificity`, `joinPaths`, and
709
+ `defineRoute` (identity pass-through, literal `Path` preservation at the
710
+ call site).
711
+ - [`tests/src/core/parsers.test.ts`](../tests/src/core/parsers.test.ts) —
712
+ `parseMethod` narrowing every registrable verb, rejecting an unknown verb
713
+ and the wrong casing, and accepting exactly the verbs `METHOD_LIST`
714
+ declares.
715
+ - [`tests/src/core/factories.test.ts`](../tests/src/core/factories.test.ts) —
716
+ `createRouter`/`createDispatcher` round-trips and factory return-type
717
+ assertions.
718
+ - [`tests/src/browser/Navigator.test.ts`](../tests/src/browser/Navigator.test.ts) —
719
+ hash and history modes, `navigate()`/`active`/`navigate` event, fallback
720
+ semantics, guard veto (sync + async, including supersede-discard), link
721
+ interception on/off, `start`/`stop`/`destroy` idempotence,
722
+ `NavigatorInterface` conformance, and the transcriptions of this guide's
723
+ `@orkestrel/router/browser` fences.
724
+ - [`tests/src/browser/factories.test.ts`](../tests/src/browser/factories.test.ts) —
725
+ `createNavigator` returns a working `NavigatorInterface`.
726
+ - [`tests/src/browser/helpers.test.ts`](../tests/src/browser/helpers.test.ts) —
727
+ `computeNavigationKey`, `extractHashPath`, `resolveLocationPath` (hash + history, with/without
728
+ `base`), and `findAnchor` (including a click on a styled child inside an
729
+ anchor).
730
+ - [`tests/src/server/validators.test.ts`](../tests/src/server/validators.test.ts) —
731
+ `isEncryptedSocket` on an encrypted socket, a plain record, and every
732
+ off-shape value.
733
+ - [`tests/src/server/helpers.test.ts`](../tests/src/server/helpers.test.ts) —
734
+ `buildRequest` fidelity (method, URL from `Host`, headers including
735
+ multi-value and `set-cookie`, body streaming, the incomplete-request and
736
+ complete-request response-side disconnect aborts, plus normal-response
737
+ signal/listener cleanup) and `sendResponse` (status, headers including
738
+ `set-cookie`, streamed and empty bodies, a destroyed target mid-stream)
739
+ over real `node:http` sockets.
740
+ - [`tests/src/server/handlers.test.ts`](../tests/src/server/handlers.test.ts) —
741
+ `handleListenerRequest` at the transport boundary and `createListener`
742
+ end-to-end round-trips (matched, 404, 405, auto-HEAD, auto-OPTIONS, a
743
+ handler throw, and per-request state) over real `node:http` sockets.
744
+
745
+ ## See also
746
+
747
+ - [`AGENTS.md`](../AGENTS.md) — the rules: the Emitter pattern, the
748
+ contract & validation architecture, one engine with native overrides,
749
+ and documentation as contracts.
750
+ - [`abort.md`](abort.md) — `@orkestrel/abort`, the supersede-safe guard
751
+ cancellation primitive the Navigator composes, and the client-disconnect
752
+ cancellation primitive the Listener composes.
753
+ - [`README.md`](README.md) — the guides index.