routegraph 0.0.0-stage → 0.1.0

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.
package/AGENTS.md ADDED
@@ -0,0 +1,42 @@
1
+ # AGENTS.md
2
+
3
+ Guidance for AI coding agents. Part 1 is for projects that use routegraph. Part 2 is for working on the routegraph repository itself.
4
+
5
+ ## Part 1: Using routegraph in a project
6
+
7
+ routegraph models an app as a directed graph of screens (`.navgraph` text), finds routes, and emits JSON instructions. Read `llms.txt` in this package for the full cheat sheet, and `README.md` for the reference.
8
+
9
+ Checklist when generating or editing a `.navgraph`:
10
+ 1. Start with `NAVGRAPH/2`. Declare every `VAR` that a `$NAME$` reference uses.
11
+ 2. Declare each `SCREEN` once. Edge endpoints must be declared screens.
12
+ 3. Write both directions explicitly. Reverse edges are never inferred.
13
+ 4. Every `DO TYPE` must exist in the active schema, and payloads must satisfy its `required` and `properties`.
14
+ 5. Costs are non-negative numbers. Use cost for "how bad is this transition", not for action counts.
15
+ 6. Quote `if=` expressions that contain commas or brackets.
16
+ 7. Validate with `validateGraph(parseGraph(text))` or `indexTreeFromText(text, { diagnostics: "collect" })` and read `diagnostics` (each has code, line, column).
17
+
18
+ Checklist when writing code against the API:
19
+ - Routes are synchronous. Instruction building is async by default (`buildInstructions`) with a sync twin (`buildInstructionsSync`).
20
+ - React Native: use `indexTreeFromText` with bundled text, or pass `readFile` to `indexTree`.
21
+ - Extensions are local trusted code passed in `buildInstructions(path, { extensions })`. Never place code in a `.navgraph`.
22
+ - Use `tryCalculatePath` when "no route" is an expected outcome.
23
+
24
+ ## Part 2: Working on this repository
25
+
26
+ Commands:
27
+ - `npm install`
28
+ - `npm run typecheck` (strict TS, includes tests)
29
+ - `npm test` (vitest, 88 tests)
30
+ - `npm run build` (tsup, CJS plus ESM plus d.ts into `dist/`)
31
+ - `npm run prepublishOnly` runs all of the above
32
+
33
+ Layout (`src/`): `parser.ts` (navgraph text to Graph IR), `schema.ts` (default schema, inheritance, payload validation), `graph.ts` (validation, indexing, `NavigationTree`), `conditions.ts` (safe expression language), `variables.ts` (scopes and `$VAR$` resolution), `pathfinding.ts` (A*, Dijkstra, BFS, heap), `extensions.ts` and `instructions.ts` (hooks and builder), `drive.ts` (generator drivers), `runtime.ts` (optional executor), `index.ts` (public exports).
34
+
35
+ Rules to preserve:
36
+ - No Node-only APIs in the core. File access goes through `files.ts` and the `readFile` option. No `require`, no dynamic `import()` with variables (Metro rejects them).
37
+ - Sync and async builders share one generator implementation in `drive.ts`. Do not fork logic between them.
38
+ - Parser and pathfinder stay deterministic. Tie breaking is by estimate, heuristic, then discovery order.
39
+ - Output stays plain JSON. Extension output is namespaced and validated.
40
+ - Graph files never execute code.
41
+ - Copy and docs style: plain prose, no em dashes, no emoji, no ASCII art.
42
+ - New behavior needs a test in `tests/`. Keep `npm run typecheck` clean.
package/CHANGELOG.md ADDED
@@ -0,0 +1,18 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The format follows
4
+ Keep a Changelog and the project follows semantic versioning.
5
+
6
+ ## 0.1.0
7
+
8
+ Initial release.
9
+
10
+ - `.navgraph` (NAVGRAPH/2) parser with line and column errors.
11
+ - JSON based `.navschema` with `extends`, explicit `overrides` and `remove`.
12
+ - Typed variables with defaults, route inputs and runtime values.
13
+ - Safe condition language (no eval) with custom word operators.
14
+ - A*, Dijkstra and BFS pathfinding with deterministic tie breaking, custom algorithms, cost functions and edge filters.
15
+ - Instruction generation with action, step and route extension hooks, dependencies, validation and an error policy.
16
+ - Synchronous and asynchronous builders that share one implementation.
17
+ - Optional, opt-in action runtime with timeouts, retries and abort support.
18
+ - Dual CJS and ESM build with bundled type declarations. No Node only APIs in the core, so it runs in React Native.
package/CLAUDE.md ADDED
@@ -0,0 +1,11 @@
1
+ # CLAUDE.md
2
+
3
+ @AGENTS.md
4
+
5
+ ## Notes for Claude
6
+
7
+ - If asked about the `routegraph` package, read `llms.txt` first. It is a complete cheat sheet and is accurate for this version.
8
+ - When writing a `.navgraph`, validate it before presenting it: run `validateGraph(parseGraph(text))` or build the tree with `diagnostics: "collect"` and fix every error.
9
+ - Prefer `indexTreeFromText` and `buildInstructionsSync` in React Native examples unless extensions are async.
10
+ - Do not invent actions. Check the active schema; the default set is TAP, LONG_PRESS, TYPE, SWIPE, SCROLL, BACK, WAIT, SET_STATE, ASSERT.
11
+ - When changing the repo, run `npm run typecheck && npm test` before finishing.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sorren
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,440 @@
1
- # Temporary Holding Version
1
+ # routegraph
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Typed navigation graphs for apps. Describe your screens and the transitions between them in a small text format, ask for a route, and get back a plain JSON instruction tree you can use for tutorials, UI automation, narration, analytics, or anything else.
4
+
5
+ - Compact `.navgraph` format that is easy to read, diff, and generate with an LLM.
6
+ - Deterministic parsing, validation, and pathfinding (A*, Dijkstra, BFS, or your own). No LLM is ever involved in finding a route.
7
+ - Typed variables with `$NAME$` substitution.
8
+ - Extension hooks that add data to actions, steps, or whole routes while the instructions are generated.
9
+ - Works the same in React Native, browsers, and Node. Every entry point that can be synchronous has a synchronous form.
10
+ - Zero runtime dependencies, dual ESM and CommonJS build, full TypeScript declarations.
11
+
12
+ routegraph does not drive a UI by itself. An optional runtime is included if you want to execute instructions through handlers you register.
13
+
14
+ ## Working with AI assistants
15
+
16
+ The package ships `llms.txt` (a complete cheat sheet), `AGENTS.md`, and `CLAUDE.md`. Assistants that read `node_modules/routegraph` pick up the format, API, and common mistakes without further prompting. Point other tools at `node_modules/routegraph/llms.txt`.
17
+
18
+ ## Install
19
+
20
+ ```sh
21
+ npm install routegraph
22
+ ```
23
+
24
+ Requires Node 18 or newer for tooling. The library itself uses only ES2020 features and no Node APIs, so it runs on Hermes and in browsers.
25
+
26
+ ## Quick start
27
+
28
+ `example.navgraph`:
29
+
30
+ ```
31
+ NAVGRAPH/2
32
+ APP "Example Store"
33
+
34
+ VAR PRODUCT_ID : string
35
+ VAR QUANTITY : number = 1
36
+
37
+ SCREEN home "Home"
38
+ SCREEN shop "Shop"
39
+ SCREEN product "Product Details"
40
+ SCREEN cart "Cart"
41
+
42
+ home -> shop { DO TAP { "target": "Shop" } }
43
+ shop -> home { DO BACK {} }
44
+ shop -> product {
45
+ DO TAP { "target": "Product Card", "match": { "productId": "$PRODUCT_ID$" } }
46
+ DO WAIT { "until": "screen:product", "timeoutMs": 5000 }
47
+ }
48
+ product -> cart {
49
+ DO TAP { "target": "Add to Cart" }
50
+ DO SET_STATE { "key": "cart.quantity", "value": "$QUANTITY$" }
51
+ }
52
+ END
53
+ ```
54
+
55
+ ```ts
56
+ import * as AppNav from "routegraph";
57
+
58
+ const tree = await AppNav.indexTree("./example.navgraph");
59
+
60
+ const path = AppNav.calculatePath(tree, "home", "cart", AppNav.ASTAR, {
61
+ variables: { PRODUCT_ID: "PRODUCT_123" },
62
+ });
63
+
64
+ const instructions = await AppNav.buildInstructions(path);
65
+ console.log(JSON.stringify(instructions, null, 2));
66
+ ```
67
+
68
+ Named imports and the namespace object both work:
69
+
70
+ ```ts
71
+ import { indexTree, calculatePath, buildInstructions, AppNav } from "routegraph";
72
+ ```
73
+
74
+ ## React Native
75
+
76
+ React Native has no filesystem API in the JavaScript runtime, so there are two ways to load a graph.
77
+
78
+ **1. Bundle the text and use the synchronous API.** This is the simplest option and needs no async code at all.
79
+
80
+ ```ts
81
+ import { indexTreeFromText, calculatePath, buildInstructionsSync } from "routegraph";
82
+ import graphText from "./app.navgraph.txt"; // or a plain .ts file that exports a string
83
+
84
+ const tree = indexTreeFromText(graphText);
85
+ const path = calculatePath(tree, "home", "cart", "astar", { variables: { PRODUCT_ID: "42" } });
86
+ const instructions = buildInstructionsSync(path);
87
+ ```
88
+
89
+ Metro does not load `.navgraph` files out of the box. Either export the text from a `.ts` file, or add the extension to `metro.config.js` with a small transformer for raw text.
90
+
91
+ **2. Supply a `readFile` function and use the async API.** Any function that returns a promise of a string works.
92
+
93
+ ```ts
94
+ import * as FileSystem from "expo-file-system";
95
+ import { indexTree } from "routegraph";
96
+
97
+ const tree = await indexTree(`${FileSystem.documentDirectory}app.navgraph`, {
98
+ readFile: (p) => FileSystem.readAsStringAsync(p),
99
+ schema: `${FileSystem.documentDirectory}app.navschema`,
100
+ });
101
+ ```
102
+
103
+ `readFile` is also used for schema files and schema `extends` chains.
104
+
105
+ ### Sync and async at a glance
106
+
107
+ | Task | Synchronous | Asynchronous |
108
+ | --- | --- | --- |
109
+ | Load a graph | `indexTreeFromText(text, options)` | `indexTree(path, options)` |
110
+ | Load a schema | `resolveSchema(definition)` | `loadSchema(pathOrDefinition)` |
111
+ | Find a route | `calculatePath`, `tryCalculatePath` | not needed, always synchronous |
112
+ | Build instructions | `buildInstructionsSync(path, options)` | `buildInstructions(path, options)` |
113
+ | Execute instructions | `runtime.executeSync(instructions)` | `runtime.execute(instructions)` |
114
+
115
+ `buildInstructions` always returns a promise, so you never have to guess. `buildInstructionsSync` throws an `AsyncExtensionError` if an extension hook returns a promise. Both are driven by the same generator, so their behavior cannot drift apart.
116
+
117
+ ## The .navgraph format
118
+
119
+ ```
120
+ NAVGRAPH/2 header, required, first non-comment line
121
+ APP "Name" optional
122
+ VAR NAME : type [= json] zero or more
123
+ SCREEN id "Display Name" [{json metadata}]
124
+ src -> dst [attrs] [{ DO TYPE {json} ... }]
125
+ END optional, nothing may follow it
126
+ ```
127
+
128
+ General rules:
129
+
130
+ - Comments start with `#` and run to the end of the line. A `#` inside a string is not a comment.
131
+ - Whitespace and line breaks are free between tokens. `APP`, `VAR`, and `SCREEN` declarations must end at a line break.
132
+ - Strings are JSON strings, so JSON escapes apply. JSON payloads may span many lines.
133
+ - Screen ids match `[A-Za-z_][A-Za-z0-9_.:]*`. Variable names match `[A-Za-z_][A-Za-z0-9_]*`.
134
+ - Edges are directed. Reverse travel needs its own edge. Nothing is ever inferred.
135
+ - An edge with no block has no actions, which is valid. `DO BACK` without braces is the same as `DO BACK {}`.
136
+ - Several edges may connect the same pair of screens. Each edge has a unique id: the one you give it with `id=`, or `src->dst`, with `#2`, `#3` and so on added for repeats.
137
+
138
+ ### Edge attributes
139
+
140
+ ```
141
+ a -> b [cost=2, if=SIGNED_IN, id=hop, label="Go", weight=7]
142
+ ```
143
+
144
+ | Attribute | Meaning |
145
+ | --- | --- |
146
+ | `cost` | Finite number, zero or more. Default is 1, changeable with the `defaultCost` option. |
147
+ | `if` | Condition expression. The edge is only available when it is true. |
148
+ | `id` | Explicit unique edge id. |
149
+ | anything else | Free metadata, stored on `edge.meta`. Can be validated by a schema. |
150
+
151
+ The cost belongs to the edge, not to the actions. An edge with three actions and `cost=3` is still one transition.
152
+
153
+ ### Conditions
154
+
155
+ Conditions use a small, safe expression language. There is no `eval` and no JavaScript.
156
+
157
+ ```
158
+ expr := or
159
+ or := and (("||" | "or") and)*
160
+ and := not (("&&" | "and") not)*
161
+ not := ("!" | "not") not | compare
162
+ compare := primary [op primary] op: == != < <= > >= or a word operator
163
+ primary := "text" | 'text' | number | true | false | null | path | "(" expr ")"
164
+ path := name ("." name)*
165
+ ```
166
+
167
+ Names are looked up in the route variables first, then in the `state` object you pass to `calculatePath`. Dotted paths walk into objects. A flat state key such as `"cart.notEmpty"` also works.
168
+
169
+ Unknown values are `undefined`. Undefined is falsy, every comparison with undefined is false except `!=`, which is true. Pass `strictConditions: true` to throw instead.
170
+
171
+ Built in word operators are `in` and `contains`. Add your own:
172
+
173
+ ```ts
174
+ calculatePath(tree, "home", "admin", "astar", {
175
+ state: { role: "staff" },
176
+ conditionOperators: { startsWith: (l, r) => String(l).startsWith(String(r)) },
177
+ });
178
+ ```
179
+
180
+ Conditions inside `[...]` that contain a comma or square bracket should be quoted: `if="role in roles"`.
181
+
182
+ ## Variables
183
+
184
+ ```
185
+ VAR USER_NAME : string = "Aspen"
186
+ VAR PRODUCT_ID : string
187
+ VAR QUANTITY : number = 3
188
+ ```
189
+
190
+ Types are `string`, `number`, `boolean`, `null`, `object`, `array`, and `any`. A variable without a default is required whenever something needs its value.
191
+
192
+ Substitution happens inside action payload strings:
193
+
194
+ | Payload value | Result with `PRODUCT_ID="SKU-123"`, `QUANTITY=3` |
195
+ | --- | --- |
196
+ | `"$QUANTITY$"` | `3` (the native number) |
197
+ | `"Product $PRODUCT_ID$"` | `"Product SKU-123"` (interpolated text) |
198
+ | `"$$PRODUCT_ID$$"` | `"$PRODUCT_ID$"` (escaped literal) |
199
+ | `"costs $5"` | `"costs $5"` (a lone `$` is left alone) |
200
+
201
+ A string that is exactly one reference gets the variable's native type. Embedded references use the string form, with objects and arrays written as JSON. Only values are substituted, never object keys. Undefined variables and missing required values throw a `VariableError` instead of producing an empty string.
202
+
203
+ Precedence, lowest to highest:
204
+
205
+ 1. Defaults declared in the graph.
206
+ 2. Route inputs (`variables` option).
207
+ 3. Runtime values (`runtime` option).
208
+
209
+ Supplied values for declared variables are type checked. Values for undeclared variables are accepted unless you pass `strictVariables: true`. The graph validator rejects payload references to undeclared variables by default.
210
+
211
+ Extension configuration is a separate scope. It never changes route variables.
212
+
213
+ ## Schemas
214
+
215
+ A schema declares which action types are valid and what their payloads look like. A bundled default covers `TAP`, `LONG_PRESS`, `TYPE`, `SWIPE`, `SCROLL`, `BACK`, `WAIT`, `SET_STATE`, and `ASSERT`. Add your own:
216
+
217
+ ```json
218
+ {
219
+ "schemaVersion": 1,
220
+ "name": "my-app",
221
+ "extends": "default",
222
+ "actions": {
223
+ "SCAN_QR": {
224
+ "description": "Project specific QR scan action",
225
+ "required": ["camera"],
226
+ "properties": {
227
+ "camera": { "type": "string" },
228
+ "timeoutMs": { "type": "number" }
229
+ }
230
+ }
231
+ },
232
+ "edgeMetadata": { "label": { "type": "string" } },
233
+ "screenMetadata": { "icon": { "type": "string" } }
234
+ }
235
+ ```
236
+
237
+ ```ts
238
+ const tree = await indexTree("./app.navgraph", { schema: "./app.navschema" });
239
+ // or, with no filesystem:
240
+ const tree = indexTreeFromText(text, { schema: mySchemaObject });
241
+ ```
242
+
243
+ Inheritance rules:
244
+
245
+ - Adding an action is always allowed.
246
+ - Redefining an inherited action with an identical definition is allowed.
247
+ - Redefining it differently is an error unless the action is listed in `overrides`.
248
+ - Removing an inherited action requires listing it in `remove`.
249
+ - `extends` can be `"default"`, the name of a schema registered with `registerSchema`, or (with `loadSchema`) a file path.
250
+
251
+ Property definitions support `type` (including `integer`, and arrays of types), `enum`, `minimum`, `maximum`, `required`, `properties`, `additionalProperties`, and `items`. Unknown properties are allowed by default so payloads can carry extra data. Set `"additionalProperties": false` to forbid them. A JSON Schema for editor support is shipped at `routegraph/schemas/navschema-v1.schema.json`.
252
+
253
+ A schema describes what is valid. It does not implement anything. Declaring `SCAN_QR` does not give you a camera.
254
+
255
+ ## Validation and diagnostics
256
+
257
+ `indexTree` and `indexTreeFromText` validate by default and throw a `ValidationError` that carries every diagnostic. For tooling, use collect mode or validate directly:
258
+
259
+ ```ts
260
+ const tree = indexTreeFromText(text, { diagnostics: "collect" });
261
+ tree.diagnostics; // errors and warnings with file, line, column, and ids
262
+
263
+ const result = validateGraph(parseGraph(text), schema);
264
+ if (!result.ok) console.log(result.errors);
265
+ ```
266
+
267
+ Checks include: unsupported version, duplicate screens, edges, and variables, unknown screens in edges, invalid or negative costs, invalid conditions and operators, unknown action types, missing or mistyped payload properties, undeclared variables, and variables whose declared type does not fit the payload property.
268
+
269
+ Every thrown error extends `AppNavError` and has `code`, and where possible `file`, `line`, `column`, and `ids`.
270
+
271
+ ## Pathfinding
272
+
273
+ ```ts
274
+ const path = calculatePath(tree, "home", "checkout", AppNav.ASTAR, options);
275
+ ```
276
+
277
+ Algorithms: `ASTAR` (default), `DIJKSTRA`, `BFS` (fewest edges, ignores cost for the search), or any function matching `PathfindingAlgorithm`. Register one by name with `registerAlgorithm`.
278
+
279
+ Options:
280
+
281
+ | Option | Purpose |
282
+ | --- | --- |
283
+ | `variables`, `runtime`, `state` | Context for conditions. |
284
+ | `heuristic(from, goal, tree)` | A* heuristic. Must be admissible for optimal routes. Defaults to 0, which makes A* behave like Dijkstra. |
285
+ | `costFn(edge)` | Replace edge costs, for example to penalise risky transitions. Must be finite and non-negative. |
286
+ | `edgeFilter(edge)` | Veto edges. |
287
+ | `conditionOperators`, `strictConditions` | Condition behavior. |
288
+
289
+ Notes:
290
+
291
+ - `calculatePath` throws `NoPathError` when there is no route. `tryCalculatePath` returns `{ ok: true, path }` or `{ ok: false, error }` instead.
292
+ - Ties are broken deterministically: lower total estimate, then lower heuristic, then the order edges were discovered, which follows declaration order. Equal inputs always give the same route.
293
+ - One call finds one route for one pair of screens. There is no all pairs mode and no hidden caching.
294
+ - `path.variables` stores the route inputs, so `buildInstructions(path)` works without repeating them. Anything passed to `buildInstructions` overrides it.
295
+ - `findReachable(tree, start, options)` lists the screens you can get to.
296
+
297
+ ## Instruction output
298
+
299
+ ```json
300
+ {
301
+ "from": "home",
302
+ "to": "checkout",
303
+ "path": ["home", "shop", "cart", "checkout"],
304
+ "steps": [
305
+ {
306
+ "n": 1,
307
+ "from": "home",
308
+ "to": "shop",
309
+ "edge": "home->shop",
310
+ "cost": 1,
311
+ "actions": [
312
+ { "type": "TAP", "data": { "target": "Shop" }, "extensions": {} }
313
+ ],
314
+ "extensions": {}
315
+ }
316
+ ],
317
+ "cost": 3,
318
+ "extensions": {},
319
+ "meta": { "app": "Example Store", "algorithm": "astar", "generator": "routegraph" }
320
+ }
321
+ ```
322
+
323
+ It is ordinary JSON. Options for `buildInstructions`: `variables`, `runtime`, `extensions`, `extensionVariables`, `onError`, `includeUnresolved` (keeps the original payload on each action as `unresolved`, handy for debugging), and `strictVariables`.
324
+
325
+ Narrow actions with `isAction`:
326
+
327
+ ```ts
328
+ for (const step of instructions.steps) {
329
+ for (const action of step.actions) {
330
+ if (isAction(action, "TAP")) action.data.target; // string
331
+ }
332
+ }
333
+ // For custom actions pass your own map:
334
+ interface MyActions { SCAN_QR: { camera: string } }
335
+ if (isAction<MyActions, "SCAN_QR">(action, "SCAN_QR")) action.data.camera;
336
+ ```
337
+
338
+ ## Extensions
339
+
340
+ Extensions add data while instructions are generated. Their output lands under `extensions[<name>]` so extensions cannot overwrite each other or the core data.
341
+
342
+ ```ts
343
+ import { defineExtension, buildInstructions, getExtensionOutput } from "routegraph";
344
+
345
+ const narration = defineExtension({
346
+ name: "narration",
347
+ variables: { style: "concise" },
348
+ run({ action, from, to, variables }) {
349
+ return { text: `${variables.style}: tap ${String(action.data.target)} to reach ${to.name}.` };
350
+ },
351
+ });
352
+
353
+ const out = await buildInstructions(path, { extensions: [narration] });
354
+ getExtensionOutput(out.steps[0]!.actions[0]!, narration)?.text; // typed as string
355
+ ```
356
+
357
+ The inline form is equivalent and convenient for one off use: `extensions: { narration: { variables: {...}, run(ctx) {...} } }`.
358
+
359
+ Hooks, in the order they run:
360
+
361
+ | Hook | Runs | Context highlights |
362
+ | --- | --- | --- |
363
+ | `run` or `action` | once per action | `action`, `actionIndex`, `step`, `edge`, `from`, `to` |
364
+ | `step` | once per transition, after its actions | `step` including action output |
365
+ | `route` | once, after everything else | `instructions` including all earlier output |
366
+
367
+ For each step the order is: every extension's action hooks for each action, then every extension's step hooks, then after all steps the route hooks. Within a level, extensions run in the order you gave, adjusted only by `dependsOn`.
368
+
369
+ Semantics:
370
+
371
+ - Hooks see variables after substitution, so they get usable values.
372
+ - Context objects are deep frozen copies. Mutating them throws, and a hook cannot change the path, the cost, or other extensions' output. Return data instead.
373
+ - Hooks can be sync or async. Use `buildInstructions` for async hooks and `buildInstructionsSync` when all hooks are sync.
374
+ - `ctx.variables` is the extension's own configuration, optionally overridden per build with `extensionVariables: { narration: { style: "verbose" } }`.
375
+ - Extensions do not see each other by default. Declare `dependsOn: ["other"]` to read that extension's output for the same node from `ctx.outputs`. Missing and circular dependencies are errors.
376
+ - Return `undefined` to attach nothing. Anything else must be JSON compatible (finite numbers, plain objects and arrays, no cycles), otherwise an `ExtensionError` is raised.
377
+ - `validate(output)` can return `true` or a string describing the problem.
378
+ - Errors throw an `ExtensionError` by default. With `onError: "collect"` the failing output is skipped and a warning is added to `instructions.warnings`.
379
+ - Extensions are trusted local code. They are passed in from your program and can never be loaded from a `.navgraph` file.
380
+
381
+ Typing note: `defineExtension` infers the output type from what your hooks return. If different hooks return different shapes, give the output type explicitly, for example `defineExtension<"summary", JsonObject>({...})`.
382
+
383
+ ## Runtime (optional)
384
+
385
+ Core routegraph never executes anything. If you want to run instructions, register handlers explicitly:
386
+
387
+ ```ts
388
+ import { createRuntime } from "routegraph";
389
+
390
+ const runtime = createRuntime<{ driver: MyDriver }>({
391
+ TAP: ({ action, context }) => context.driver.tap(String(action.data.target)),
392
+ WAIT: { handler: ({ action }) => waitFor(action.data), options: { timeoutMs: 8000, retries: 1 } },
393
+ });
394
+
395
+ const result = await runtime.execute(instructions, { context: { driver }, continueOnError: false });
396
+ // or, with only synchronous handlers:
397
+ runtime.executeSync(instructions, { context: { driver } });
398
+ ```
399
+
400
+ Handlers get the validated, resolved payload plus your context. Options include `timeoutMs`, `retries`, `retryDelayMs`, `skipUnhandled`, `continueOnError`, `signal` (an `AbortSignal`), and an `onAction` callback. A global registry is available through `registerActionHandler` for apps that prefer one.
401
+
402
+ ## Typing and extensibility
403
+
404
+ - `indexTree<"home" | "cart">(...)` gives typed `tree.home` and `tree.cart` shortcuts. Without the type argument use `tree.screen("home")`, or `tree.screens.get("home")`, which is the authoritative lookup. Shortcuts exist only for ids that are valid JavaScript identifiers and do not collide with tree members, and they are not enumerable.
405
+ - Everything public is exported as a type: `Graph`, `Edge`, `Screen`, `Path`, `Instructions`, `PathfindingAlgorithm`, `ConditionOperator`, `Extension`, `ActionHandler`, and more.
406
+ - Extension points: custom schemas, schema registry, custom algorithms, heuristics, cost functions, edge filters, condition operators, instruction extensions, action handlers, a pluggable `readFile`.
407
+
408
+ ## Decisions made for the open questions
409
+
410
+ The design document left a number of questions open. These are the choices this release makes.
411
+
412
+ 1. Grammar: documented above. JSON blocks may span lines. Strings use JSON escapes.
413
+ 2. `END` is optional. Nothing may follow it.
414
+ 3. `APP` is optional.
415
+ 4. Variable types: `string`, `number`, `boolean`, `null`, `object`, `array`, `any`.
416
+ 5. Conditions: the grammar above. Unknown values are falsy unless strict mode is on.
417
+ 6. Edge cost defaults to 1, configurable with `defaultCost`.
418
+ 7. No route: `calculatePath` throws `NoPathError`, `tryCalculatePath` returns a result object.
419
+ 8. `buildInstructions` is always async. `buildInstructionsSync` is the synchronous twin.
420
+ 9. Schemas use a small JSON format modelled on a subset of JSON Schema, plus a real JSON Schema for validating the schema files themselves.
421
+ 10. Extension dependencies are explicit through `dependsOn`.
422
+ 11. The action runtime ships in the same package but is opt-in and tree shakeable.
423
+ 12. Imports between graph files are not part of v1. Merge text before indexing if you need it.
424
+ 13. `tree.<id>` shortcuts are defined only for safe identifiers.
425
+ 14. Unresolved payloads are available through `includeUnresolved`.
426
+
427
+ ## Development
428
+
429
+ ```sh
430
+ npm install
431
+ npm run typecheck
432
+ npm test
433
+ npm run build
434
+ ```
435
+
436
+ `npm publish` runs the typecheck, tests, and build first through `prepublishOnly`. The included GitHub workflows run CI on Node 18, 20, and 22 and publish to npm with provenance when a release is created (add an `NPM_TOKEN` secret).
437
+
438
+ ## License
439
+
440
+ MIT