@abide/abide 0.116.4 → 0.117.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.117.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 05ba855: a ceiling naming no bench fails the run, and a bench file sits beside any code it times (RULEBOOK 23.22, 43.32-43.34) ([`d5f3d3c`](https://github.com/briancray/abide/commit/d5f3d3cc6368057f8437559aafc7113abd2b3269))
8
+ - 05ba855: a refusal and the starter's notes say a lib/ or public/ fence may sit in any directory above the file (RULEBOOK 23.22, 23.23, 37.1) ([`d9d95df`](https://github.com/briancray/abide/commit/d9d95df07fb8cb92a295ffc9a316da9b5bc6141e))
9
+ - 9e24653: `runBenches` (`@abide/harness/runner`) and the `harness-bench` command run every `*.bench.ts` under an app's `src`, print each arm's time and the claim between them, and fail the run where an arm goes over its `CEILINGS` entry or a ceiling names no arm (RULEBOOK 43.29-43.33); `abide scaffold` writes a `bench` script that runs it, and the starter's agent notes say to measure with the harness (RULEBOOK 38.124)
10
+
11
+ ## 0.117.0
12
+
13
+ ### Minor Changes
14
+
15
+ - 3bb5cc9: `abide dev`, `build`, `start`, `check`, `routes`, `compile`, `bundle`, `openapi`, `mcp` and the shell now refuse to run on a bun outside abide's `engines.bun`, naming the bun running and the range it needs (RULEBOOK 38.123)
16
+
17
+ ### Patch Changes
18
+
19
+ - 3bb5cc9: a component whose markup reads a bare `busy()` or `ready()` is now built on the server after the turn, once the siblings after it have started their loads, so it paints the arm the browser chooses at hydration rather than one that is rebuilt (RULEBOOK 41.28, 41.31)
20
+ - 3bb5cc9: the rulebook now states that the effects one turn wakes, a slot's own update among them, run in no guaranteed order (12.15) — a `watch` that post-processes what a sibling `{raw(...)}` slot renders cannot count on the slot having updated first
21
+ - 3bb5cc9: markdown imported `with { type: 'text' }` into an app's `.ts` now checks as a `string` under `abide check`, as it already did in a `.abide` — no more hand-written `declare module '*.md'`
22
+ - 3bb5cc9: the `bunfig.toml` `abide scaffold` writes now sets `[install] peer = false`, so a dependency's peer on `bun` (`bun-plugin-tailwind`'s) cannot install an older bun that shadows the system one inside `bun run` (RULEBOOK 38.122)
23
+ - 3bb5cc9: the rulebook states three things that held but were never written down: a `raw` slot writes its markup unchecked (32.77), a fragment-only step moves `route.url` (30.33), and a `refresh` over `tags` reruns no synchronous derivation that carries none of them (13.20) — and the registry says `route.params` is `Params` outside a page or layout
24
+
3
25
  ## 0.116.4
4
26
 
5
27
  ### Patch Changes
package/bin/abide.ts CHANGED
@@ -41,12 +41,17 @@ import { executableName } from '#server/executableName.ts'
41
41
  import { iconAccent } from '#server/iconAccent.ts'
42
42
  import { namesUnder } from '#server/namesUnder.ts'
43
43
  import { DEFAULT_DOCUMENT, intoHead } from '#server/render.ts'
44
+ import { runtimeRefusal } from '#server/runtimeRefusal.ts'
44
45
  import { pathsUnder, serveBuild } from '#server/serveBuild.ts'
45
46
  import { serveDev } from '#server/serveDev.ts'
46
47
  import { escapeText } from '#shared/escapeText.ts'
47
48
  import { ICON_ADDRESSES, LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
48
49
  import abidePackage from '../package.json' with { type: 'json' }
49
50
 
51
+ // 38.123 — the bun this process runs on, held to abide's `engines.bun` before anything is built or
52
+ // booted. A compiled app carries the bun `abide compile` ran on, which this held already.
53
+ const RUNTIME_REFUSAL = runtimeRefusal(Bun.version, abidePackage.engines.bun)
54
+
50
55
  /** Where the app a command builds or serves is, and where its build goes — both absolute. */
51
56
  type AppDirectories = { root: string; out: string }
52
57
 
@@ -59,14 +64,18 @@ const DISK: Host = {
59
64
  version: abidePackage.version,
60
65
  // 38.100 — built in this process rather than a child, where nobody built it, and said on
61
66
  // stderr: the command's stdout is its own — `openapi`'s document, `mcp`'s messages.
62
- app: ({ values }) =>
63
- appOnDisk(directoriesOf(values), async (root, out) =>
67
+ app: ({ values }) => {
68
+ const handle = appOnDisk(directoriesOf(values), async (root, out) =>
64
69
  (await serveBuild(root, out, (root, out) =>
65
70
  writeBuild({ root, out }, console.error),
66
71
  ))
67
72
  ? out
68
73
  : undefined,
69
- ),
74
+ )
75
+ return RUNTIME_REFUSAL === undefined
76
+ ? handle
77
+ : { ...handle, boot: async () => RUNTIME_REFUSAL }
78
+ },
70
79
  source: {
71
80
  scaffold: scaffoldCommand,
72
81
  dev: inApp(dev),
@@ -645,6 +654,10 @@ function inApp<Answer>(
645
654
  body: (app: AppDirectories, given: Given) => Promise<Answer>,
646
655
  ): (given: Given) => Promise<Answer | number> {
647
656
  return async (given) => {
657
+ if (RUNTIME_REFUSAL !== undefined) {
658
+ console.error(`abide ${given.positionals[0]}: ${RUNTIME_REFUSAL}`)
659
+ return 1
660
+ }
648
661
  const app = directoriesOf(given.values)
649
662
  if (app === undefined) {
650
663
  console.error(
package/docs/REGISTRY.md CHANGED
@@ -160,7 +160,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
160
160
  | --- | --- | --- | --- |
161
161
  | `Disposer` | `() => void` | What tears down the previous run. | 12.1 |
162
162
  | `Effect` | `() => void \| Disposer` | What runs on change. | 12.2, 12.8, 14.6 |
163
- | `watch` | `(effect: Effect) => () => void` | Begins a watch over whatever the effect reads. | 12.2, 12.4, 12.5, 12.6, 12.7, 12.10, 12.13 |
163
+ | `watch` | `(effect: Effect) => () => void` | Begins a watch over whatever the effect reads. | 12.2, 12.4, 12.5, 12.6, 12.7, 12.10, 12.13, 12.15 |
164
164
  | `watch` | `(sources: Reactive<unknown, never, unknown, unknown> \| Reactive<unknown, never, unknown, unknown>[], effect: Effect) => () => void` | The narrowed form, over named sources. | 12.3 |
165
165
  | `flushSync` | `() => void` | Ends the turn: every `Effect` a write woke runs now. | 9.10, 12.11, 12.12 |
166
166
 
@@ -169,10 +169,10 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
169
169
  | Name | Signature | Meaning | Rules |
170
170
  | --- | --- | --- | --- |
171
171
  | `Selection` | `Reactive<any, any, any, any> \| Keyed<any, any, any, any> \| Rpc<any, any, any, any> \| { tags: string[] }` | What is being asked about. | 13.1, 13.8, 13.9, 13.13, 31.14 |
172
- | `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | 13.2, 13.3, 13.5, 13.12, 46.11 |
173
- | `ready` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection has a value to serve. | 13.3, 13.5, 13.11, 13.12 |
172
+ | `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | 13.2, 13.3, 13.5, 13.12, 41.28, 41.31, 46.11 |
173
+ | `ready` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection has a value to serve. | 13.3, 13.5, 13.11, 13.12, 41.28, 41.31 |
174
174
  | `synced` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection holds what its producer made. | 13.12, 13.18 |
175
- | `refresh` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Stale-while-revalidate over the selection. | 13.4, 13.7, 13.10, 13.12, 13.14, 13.15, 13.16, 13.17 |
175
+ | `refresh` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Stale-while-revalidate over the selection. | 13.4, 13.7, 13.10, 13.12, 13.14, 13.15, 13.16, 13.17, 13.20 |
176
176
  | `invalidate` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Marks the selection stale. | 13.4, 13.6, 13.10, 13.12, 13.15, 13.17 |
177
177
 
178
178
 
@@ -380,8 +380,8 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
380
380
 
381
381
  | Name | Signature | Meaning | Rules |
382
382
  | --- | --- | --- | --- |
383
- | `route.url` | `Reactive<URL>` | Where we are, fragment included. | 23.1, 23.2, 23.3, 23.4, 30.20, 30.21, 30.23, 19.16, 31.16 |
384
- | `route.params` | `Reactive<Params>` | The matched route's segments. | 23.1, 23.5, 23.9, 30.23, 31.16 |
383
+ | `route.url` | `Reactive<URL>` | Where we are, fragment included. | 23.1, 23.2, 23.3, 23.4, 30.20, 30.21, 30.23, 30.33, 19.16, 31.16 |
384
+ | `route.params` | `Reactive<Params>` | The matched route's segments — typed by its own route in a page or a layout, and `Params` in any other file. | 23.1, 23.5, 23.9, 30.23, 31.16 |
385
385
  | `Params` | `Record<string, string \| string[]>` | What a matched route's segments are. | 23.5 |
386
386
  | `route.name` | `Reactive<string>` | The resolution path, the same under any mount path. | 23.1, 23.6, 23.14, 31.16 |
387
387
  | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | 23.1, 31.16 |
@@ -571,7 +571,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
571
571
 
572
572
  | Name | Signature | Meaning | Rules |
573
573
  | --- | --- | --- | --- |
574
- | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | 32.2, 32.24, 32.26, 32.30, 32.31, 32.70 |
574
+ | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | 32.2, 32.24, 32.26, 32.30, 32.31, 32.70, 32.77 |
575
575
  | `Raw` | `class Raw { html: string }` | What `raw` hands back. | 32.24 |
576
576
 
577
577
  ### Control flow
@@ -629,7 +629,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
629
629
  | Name | Signature | Meaning | Rules |
630
630
  | --- | --- | --- | --- |
631
631
  | `Component` | `interface Component {}` | A component bound to its props. | 32.17, 34.16, 35.4 |
632
- | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | 29.10, 35.4, 35.5, 35.6, 35.15, 35.16, 41.1, 41.2, 41.3, 41.4, 41.9, 41.13, 41.16, 41.17, 41.18, 41.19, 41.20, 41.28, 35.11, 41.29, 41.30 |
632
+ | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | 29.10, 35.4, 35.5, 35.6, 35.15, 35.16, 41.1, 41.2, 41.3, 41.4, 41.9, 41.13, 41.16, 41.17, 41.18, 41.19, 41.20, 41.28, 35.11, 41.29, 41.30, 41.31 |
633
633
 
634
634
  ## Sinks
635
635
 
@@ -677,6 +677,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
677
677
  | `**/lib/**` | The app's code that is no address: components and modules. | 23.15, 23.16, 23.18, 23.22, 37.1 |
678
678
  | `**/lib/**/<name>.server.<extension>` | A server file: source the browser never loads. | 16.29, 23.23, 34.20, 36.10 |
679
679
  | `**/<name>.test.ts` | A test file: one of the app's tests, wherever it lies. | 23.22, 38.109 |
680
+ | `**/<name>.bench.ts` | A bench file: benches `bun run bench` runs, wherever it lies. | 23.22, 43.29 |
680
681
  | `**/<name>.d.ts` | A declaration file: types for the checker, wherever it lies. | 23.22 |
681
682
  | `**/public/**` | The files answered as authored, at their path below that directory. | 23.15, 23.16, 23.18, 36.11, 36.12, 36.13, 36.14, 36.15, 36.17, 21.12, 21.13, 21.14, 38.18, 38.19 |
682
683
  | `src/public/favicon.ico` | The file answered at `/favicon.ico`. | 36.11 |
@@ -824,13 +825,19 @@ rows a runner prints, and an adapter for an abide app.
824
825
  | `inScope` | `(item: RowItem, scope: string) => boolean` | Whether an item falls in a `--scope`. | 43.26 |
825
826
  | `matchesFilter` | `(item: RowItem, filter: string) => boolean` | Whether an item matches a `--filter`. | 43.26 |
826
827
 
827
- ## `@abide/harness/runner` — runners that print rows
828
+ ## `@abide/harness/runner` — runners that print rows, and the bench runner
828
829
 
829
830
  | Name | Signature | Meaning | Rules |
830
831
  | --- | --- | --- | --- |
831
832
  | `rowsCommand` | `<Item extends RowItem, Row>(items: Item[], run: (selected: Item[], emit: (row: Row) => void) => Promise<void>) => Promise<void>` | A runner's command line: its catalog under `--list`, else the rows of what it selects. | 43.26 |
832
833
  | `testRows` | `(suites: TestSuite[], cwd: string) => Promise<void>` | `rowsCommand` over the test files of `suites`, one `TestRow` per file. | 43.27 |
833
834
  | `TestSuite` | `{ section: string; root: string; runner: 'bun'; pattern?: string } \| { section: string; root: string; runner: 'playwright'; projects: string[]; pattern?: string }` | One directory of test files, listed under one section. | 43.27 |
835
+ | `runBenches` | `(root: string, only?: string) => Promise<{ file: string; name: string; case: string; n: number; floor: number; arms: Record<string, number>; claim: string; over: string[] }[]>` | Every bench under `root`'s `src`, run, with each arm's time per operation and the ceilings it went over. | 43.29, 43.30, 43.31, 43.32, 43.34 |
836
+ | `CEILINGS` | `Record<string, Record<string, number>>` | A bench file's export naming the most each arm of each of its benches may take, in milliseconds. | 43.29, 43.31, 43.32 |
837
+
838
+ | Name | Meaning | Rules |
839
+ | --- | --- | --- |
840
+ | `harness-bench [word]` | The command that runs `runBenches` over the directory it runs in and prints each bench. | 43.33, 43.34 |
834
841
 
835
842
  ## `@abide/harness/abide` — the adapter for an abide app
836
843
 
@@ -849,20 +856,20 @@ rows a runner prints, and an adapter for an abide app.
849
856
 
850
857
  | Command | Meaning | Rules |
851
858
  | --- | --- | --- |
852
- | `abide scaffold <name>` | Writes a starter project. | 38.1, 38.42, 38.103, 38.104, 38.106, 38.112 |
859
+ | `abide scaffold <name>` | Writes a starter project. | 38.1, 38.42, 38.103, 38.104, 38.106, 38.112, 38.122, 38.124 |
853
860
  | `abide run <file> [args…]` | Runs a script under the abide runtime. | 38.2, 38.57 |
854
- | `abide check [dir…]` | The type-checker over a `.abide` tree. | 38.3, 38.31, 38.32, 38.33, 38.40, 38.105 |
855
- | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | 38.108 |
856
- | `abide dev [--port <n>]` | Watches the project and keeps the app up. | 16.23, 16.56, 37.8, 37.9, 38.4, 38.14, 38.15, 38.16, 38.17, 38.19, 38.24, 38.25, 38.26, 38.28, 38.30, 38.35, 38.36, 38.37, 38.38, 38.41, 38.43, 38.58, 38.63, 38.64, 38.65, 38.94, 38.96, 38.97 |
857
- | `abide build` | Builds the client into content-hashed chunks and a manifest. | 38.43, 38.5, 38.18, 38.25, 38.26, 38.27, 38.30, 38.66, 38.94 |
858
- | `abide start [--port <n>]` | Boots the built app. | 37.8, 37.10, 37.11, 37.12, 38.6, 38.100, 38.20, 38.21, 38.22, 38.23, 38.25, 38.26, 38.27, 38.28, 38.29, 38.58, 38.67, 38.68, 38.95, 38.97, 38.110 |
859
- | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. | 37.8, 37.10, 37.11, 37.12, 38.6, 38.7, 38.53, 38.54, 38.55, 38.58, 38.59, 38.60, 38.62 |
861
+ | `abide check [dir…]` | The type-checker over a `.abide` tree. | 38.3, 38.31, 38.32, 38.33, 38.40, 38.105, 38.123 |
862
+ | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | 38.108, 38.123 |
863
+ | `abide dev [--port <n>]` | Watches the project and keeps the app up. | 16.23, 16.56, 37.8, 37.9, 38.4, 38.14, 38.15, 38.16, 38.17, 38.19, 38.24, 38.25, 38.26, 38.28, 38.30, 38.35, 38.36, 38.37, 38.38, 38.41, 38.43, 38.58, 38.63, 38.64, 38.65, 38.94, 38.96, 38.97, 38.123 |
864
+ | `abide build` | Builds the client into content-hashed chunks and a manifest. | 38.43, 38.5, 38.18, 38.25, 38.26, 38.27, 38.30, 38.66, 38.94, 38.123 |
865
+ | `abide start [--port <n>]` | Boots the built app. | 37.8, 37.10, 37.11, 37.12, 38.6, 38.100, 38.20, 38.21, 38.22, 38.23, 38.25, 38.26, 38.27, 38.28, 38.29, 38.58, 38.67, 38.68, 38.95, 38.97, 38.110, 38.123 |
866
+ | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. | 37.8, 37.10, 37.11, 37.12, 38.6, 38.7, 38.53, 38.54, 38.55, 38.58, 38.59, 38.60, 38.62, 38.123 |
860
867
  | `abide call <address> [args]` | One call to one handler. | 38.8, 38.61, 38.110 |
861
- | `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. | 38.9, 38.25, 38.110, 38.111 |
862
- | `abide mcp [--url <origin>]` | The app's MCP server over stdio. | 38.9, 38.13, 38.25, 38.110 |
868
+ | `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. | 38.9, 38.25, 38.110, 38.111, 38.123 |
869
+ | `abide mcp [--url <origin>]` | The app's MCP server over stdio. | 38.9, 38.13, 38.25, 38.110, 38.123 |
863
870
  | `abide watch <address> [args]` | A live subscription to one of the app's followed `GET`s. | 38.69 |
864
- | `abide compile [--target] [--out] [--platforms]` | One standalone executable. | 37.8, 37.10, 37.11, 37.12, 38.10, 38.25, 38.26, 38.44, 38.45, 38.46, 38.47, 38.48, 38.49, 38.50, 38.51, 38.52, 38.56, 38.93, 38.97 |
865
- | `abide bundle [--out]` | A desktop app for the host platform, with a window on the app. | 38.10, 38.25, 38.26, 38.70, 38.71, 38.72, 38.73, 38.74, 38.75, 38.76, 38.77, 38.78, 38.79, 38.80, 38.81, 38.82, 38.83, 38.84, 38.85, 38.86, 38.87, 38.88, 38.89, 38.90, 38.91, 38.92, 38.101, 38.102 |
871
+ | `abide compile [--target] [--out] [--platforms]` | One standalone executable. | 37.8, 37.10, 37.11, 37.12, 38.10, 38.25, 38.26, 38.44, 38.45, 38.46, 38.47, 38.48, 38.49, 38.50, 38.51, 38.52, 38.56, 38.93, 38.97, 38.123 |
872
+ | `abide bundle [--out]` | A desktop app for the host platform, with a window on the app. | 38.10, 38.25, 38.26, 38.70, 38.71, 38.72, 38.73, 38.74, 38.75, 38.76, 38.77, 38.78, 38.79, 38.80, 38.81, 38.82, 38.83, 38.84, 38.85, 38.86, 38.87, 38.88, 38.89, 38.90, 38.91, 38.92, 38.101, 38.102, 38.123 |
866
873
  | `abide lsp` | The `.abide` language server, over stdio. | 38.11, 38.34, 38.39, 38.98, 38.116, 38.117, 38.118 |
867
874
  | `--help` · `-h` · `abide help` | Usage, generated from the command list. | 38.12, 38.113, 38.114 |
868
875
  | `--version` · `-v` | The version of abide, or of the app a compiled executable holds. | 38.115 |
package/docs/RULEBOOK.md CHANGED
@@ -700,6 +700,9 @@ holding that arm or row is removed.
700
700
  `watch`, MUST stop reading its sources once that owner stops and nothing else reads it, and MUST
701
701
  answer a later read with what its body computes then.
702
702
 
703
+ 12.15 The `Effect` values one turn wakes, a slot's own update among them, MAY run in any order,
704
+ save where a clause orders them.
705
+
703
706
  # 13. Selections
704
707
 
705
708
  13.1 A `Selection` MUST be one `Reactive`, one `Keyed`, one `Rpc`, or a set of `tags`.
@@ -757,6 +760,9 @@ where every value it sweeps is.
757
760
  current scope MUST answer as over a `Selection` with no entries, and reading `busy`, `ready` or
758
761
  `synced` there MUST subscribe the reader to the first call that reaches it in that scope.
759
762
 
763
+ 13.20 `refresh` over a set of `tags` MUST NOT rerun a synchronous derivation that carries none of
764
+ them, which MUST recompute from the load it reads.
765
+
760
766
  # 14. Tracking
761
767
 
762
768
  14.1 A `Reactive` MUST be tracked only where something downstream will be rebuilt from it.
@@ -1543,7 +1549,7 @@ error naming the mount.
1543
1549
  `layout.abide`, an `error.abide` and the root's `launcher.abide`.
1544
1550
 
1545
1551
  23.22 A file of the app's own directly in a directory under no fence that is neither a `.abide`, a
1546
- handler file, a test file nor a declaration file MUST be a build error naming the file and a `lib` directory, save one
1552
+ handler file, a test file, a bench file nor a declaration file MUST be a build error naming the file and a `lib` directory, save one
1547
1553
  beside the `package.json` of a root that holds it, nor a `.ts`.
1548
1554
 
1549
1555
  23.23 A server file of the app's own under no `lib` directory MUST be a build error naming the file
@@ -1819,6 +1825,9 @@ to `middleware`.
1819
1825
  30.32 A `navigate` that is not a history step, whose request gets no answer while `online` is false,
1820
1826
  MUST leave the page and the address where they stand, save where 45.12 holds.
1821
1827
 
1828
+ 30.33 A history step under 30.20, and a `navigate` under 30.21, MUST move `route.url` to the address
1829
+ it steps to.
1830
+
1822
1831
  # 31. Reading and writing by name
1823
1832
 
1824
1833
  31.1 Inside a `.abide` file a name bound to a `Reactive` MUST be read wherever 31.3 does not hold it.
@@ -2099,6 +2108,9 @@ element, written or spread, runs for the same event, wherever the two are writte
2099
2108
  32.76 A `bind:` whose operand is a path into a value that takes an optional step (`?.`) MUST be a
2100
2109
  build error.
2101
2110
 
2111
+ 32.77 A slot holding a value branded by `raw` MUST write its markup as given, with no element or
2112
+ attribute in it removed and no URL in it scheme-checked.
2113
+
2102
2114
  # 33. Props
2103
2115
 
2104
2116
  33.1 `props` MUST be callable from a `<script>` block only, and calling it from a `<script module>`
@@ -2766,6 +2778,14 @@ supplies, and `abide start` MUST NOT supply that secret.
2766
2778
  38.121 Under `mount`, a call the component's browser module makes to a private handler MUST be
2767
2779
  answered in the test's process by the handler the component's server module declares.
2768
2780
 
2781
+ 38.122 The project `abide scaffold` writes MUST NOT install a dependency's peer dependencies.
2782
+
2783
+ 38.123 A command `abide` builds, checks or boots the app for MUST be refused where the bun running it
2784
+ is outside the range abide's `engines.bun` names, and the refusal MUST name that bun's version and
2785
+ the range.
2786
+
2787
+ 38.124 The project `abide scaffold` writes MUST carry a `bench` script that runs `harness-bench`.
2788
+
2769
2789
  # 39. Head and view transitions
2770
2790
 
2771
2791
  39.1 A `<head>` element in a `.abide` file MUST contribute its children to the document head.
@@ -3031,13 +3051,18 @@ has answered or failed.
3031
3051
  adopts the page, however early its answer arrived.
3032
3052
 
3033
3053
  41.28 A `render` MUST let the turn's queued microtasks run between setting up a `<Name/>` whose setup
3034
- made an `Rpc` call and building that component's markup.
3054
+ made an `Rpc` call, or whose markup reads a bare `busy` or `ready`, and building that component's
3055
+ markup.
3035
3056
 
3036
3057
  41.29 A `render` answering a request whose `sec-fetch-dest` is `empty` MUST NOT fill the seed buffer,
3037
3058
  save one whose `abide-keep` header names `open`.
3038
3059
 
3039
3060
  41.30 A `render` answering a request carrying `abide-keep` MUST write 41.20's script.
3040
3061
 
3062
+ 41.31 On a server, a bare `busy` or `ready` MUST answer over the loads set up before its component's
3063
+ markup is built, and a load a component set up later, one only a pending value places, MAY have it
3064
+ choose another arm at hydration under 41.24.
3065
+
3041
3066
  # 42. The app object
3042
3067
 
3043
3068
  42.1 `createApp` MUST build the route table and compose every `Middleware` rung, per 16.14.
@@ -3175,6 +3200,20 @@ runner wrote no report as `broken` rather than as a result.
3175
3200
  43.28 The DOM `@abide/harness/preload` registers MUST leave the runtime's own `fetch`, `Request`,
3176
3201
  `Response`, `Headers`, `Blob`, `File`, `FormData`, `AbortController` and `AbortSignal` the globals.
3177
3202
 
3203
+ 43.29 `runBenches` MUST run, as one bench each, every function a bench file exports other than
3204
+ `CEILINGS`.
3205
+
3206
+ 43.30 `runBenches` MUST NOT report a bench's A/A control as one of its arms.
3207
+
3208
+ 43.31 `runBenches` MUST report an arm whose time per operation is over its `CEILINGS` entry.
3209
+
3210
+ 43.32 `runBenches` MUST report a `CEILINGS` entry naming an arm its bench does not have, and one
3211
+ naming a bench its file does not export.
3212
+
3213
+ 43.33 `harness-bench` MUST exit non-zero where `runBenches` reports anything under 43.31 or 43.32.
3214
+
3215
+ 43.34 `runBenches` given `only` MUST run only the bench files whose path below `root` holds it.
3216
+
3178
3217
  # 44. The assistant
3179
3218
 
3180
3219
  44.1 `assistant` MUST answer only through a handler that yields it, and MUST NOT mount an address
package/docs/abide.md CHANGED
@@ -196,6 +196,7 @@ at its address with `.md` on the end, and the links below point at those files.
196
196
  - `rowsCommand` — `reference/harness-runner/rowsCommand.md`
197
197
  - `testRows` — `reference/harness-runner/testRows.md`
198
198
  - `TestSuite` — `reference/harness-runner/types/TestSuite.md`
199
+ - `runBenches` — `reference/harness-runner/runBenches.md`
199
200
  - `measure` — `reference/harness-abide/measure.md`
200
201
  - `AbideWork` — `reference/harness-abide/types/AbideWork.md`
201
202
  - CLI — `reference/cli.md`
@@ -773,7 +774,7 @@ A hole is `{}` around an expression, and it updates the one thing it names and n
773
774
  | You write | Where it lands |
774
775
  | --- | --- |
775
776
  | `{expr}` | a text node, escaped |
776
- | `{raw(...)}` | markup, unescaped: `raw` brands a string as a `Raw` |
777
+ | `{raw(...)}` | markup, unescaped and unchecked (32.77): `raw` brands a string as a `Raw`, so hand it markup you wrote, never a model's reply or a fetched page as it came |
777
778
  | `name={expr}` | one attribute, whole |
778
779
  | `name="…{expr}…"` | one attribute, interpolated |
779
780
  | `class:name={cond}` | one class, there while the condition holds |
@@ -2996,7 +2997,7 @@ test('one file answers for every company, reading its id', async () => {
2996
2997
  | --- | --- |
2997
2998
  | `route.name` | the pattern that matched, `/companies/[id]` for every company, so a log line keys on the page rather than on each address |
2998
2999
  | `Params` | what the segments matched; a query string is never in it |
2999
- | `RouteParams<P>` | the type `route.params` has in a page filed under `P`, so `id` is text and a key the route lacks is an error |
3000
+ | `RouteParams<P>` | the type `route.params` has in a page or layout filed under `P`, so `id` is text and a key the route lacks is an error. A component under `lib/` has no route of its own and reads the loose `Params`: pass it the value as a prop, or narrow it with `RouteParams<'/…'>` |
3000
3001
 
3001
3002
  ## A layout holds what the companies pages share
3002
3003
 
@@ -3888,7 +3889,7 @@ test('the server paints the density the cookie holds', async () => {
3888
3889
 
3889
3890
  `cookies()` is the request's cookies on the server and the document's in the browser, as a `CookieMap`. Reading a cookie by name follows it, so the list changes on the click. The server render reads the same cookie off the request, so a reload paints compact rather than flashing comfortable first.
3890
3891
 
3891
- The principal cookie is `HttpOnly`, so script never sees the seal. Use `cookies()` for the ones that are yours: a density, a theme, a dismissed banner.
3892
+ `cookies()` reads and writes any cookie the request carries: a density, a theme, or another system's session cookie your server checks. The principal cookie is `HttpOnly`, so in the browser `cookies()` never sees the seal.
3892
3893
 
3893
3894
  ## A handler decides which fields a caller may read
3894
3895
 
@@ -6258,6 +6259,9 @@ function pipeline(count: number): () => void {
6258
6259
  }
6259
6260
  }
6260
6261
 
6262
+ // The most an advance may take among 1,000 deals, in milliseconds.
6263
+ export const CEILINGS = { priced: { 'among 1,000': 1 } }
6264
+
6261
6265
  /** Advancing one deal among 100, and among 1,000. */
6262
6266
  export function priced(): Batched {
6263
6267
  return batch({
@@ -6285,8 +6289,13 @@ A millisecond describes the machine it was taken on. A ratio between two sizes o
6285
6289
  | `emulatedDom` | whether this process's DOM is an emulator |
6286
6290
  | `time` | `batch` with `emulated` filled in, refusing to run while the counters are armed |
6287
6291
  | `THRESHOLDS` | a frame and an interaction, the two lines a cost is judged against |
6292
+ | `runBenches` | runs every bench under `src`, and reports each arm over its ceiling |
6293
+ | `CEILINGS` | the most each arm of a bench may take, in milliseconds, by bench and then by arm |
6294
+ | `harness-bench` | the `bench` script: `runBenches` printed, failing where a ceiling is not held |
6295
+
6296
+ `batch` throws on a duration whose op touched an emulated DOM, and on a sample taken in a parallel test worker. That is why the bench builds no component: it times the derived value the component reads. Run the benches with `bun run bench`, never inside `bun test`.
6288
6297
 
6289
- `batch` throws on a duration whose op touched an emulated DOM, and on a sample taken in a parallel test worker. That is why the bench builds no component: it times the derived value the component reads. Run a bench under `bun run`, never inside `bun test`.
6298
+ The `bench` script is `harness-bench`, which hands `runBenches` every `*.bench.ts` under `src`, wherever one sits beside the code it times, and prints each arm's time and the claim between them. A bench file's `CEILINGS` names the most each arm may take, in milliseconds, and the run fails where an arm takes more.
6290
6299
 
6291
6300
  ## Find which layer a click spends its time in
6292
6301
 
@@ -6806,6 +6815,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
6806
6815
  | [`rowsCommand`](/reference/harness-runner/rowsCommand.md) | `@abide/harness/runner` |
6807
6816
  | [`testRows`](/reference/harness-runner/testRows.md) | `@abide/harness/runner` |
6808
6817
  | [`TestSuite`](/reference/harness-runner/types/TestSuite.md) | `@abide/harness/runner` |
6818
+ | [`runBenches`](/reference/harness-runner/runBenches.md) | `@abide/harness/runner` |
6809
6819
  | [`measure`](/reference/harness-abide/measure.md) | `@abide/harness/abide` |
6810
6820
  | [`AbideWork`](/reference/harness-abide/types/AbideWork.md) | `@abide/harness/abide` |
6811
6821
 
@@ -7112,7 +7122,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7112
7122
 
7113
7123
  | Name | Signature | Meaning | Rules |
7114
7124
  | --- | --- | --- | --- |
7115
- | `watch` | `(effect: Effect) => () => void` | Begins a watch over whatever the effect reads. | [12.2](/rulebook#12.2), [12.4](/rulebook#12.4), [12.5](/rulebook#12.5), [12.6](/rulebook#12.6), [12.7](/rulebook#12.7), [12.10](/rulebook#12.10), [12.13](/rulebook#12.13) |
7125
+ | `watch` | `(effect: Effect) => () => void` | Begins a watch over whatever the effect reads. | [12.2](/rulebook#12.2), [12.4](/rulebook#12.4), [12.5](/rulebook#12.5), [12.6](/rulebook#12.6), [12.7](/rulebook#12.7), [12.10](/rulebook#12.10), [12.13](/rulebook#12.13), [12.15](/rulebook#12.15) |
7116
7126
  | `watch` | `(sources: Reactive<unknown, never, unknown, unknown> \| Reactive<unknown, never, unknown, unknown>[], effect: Effect) => () => void` | The narrowed form, over named sources. | [12.3](/rulebook#12.3) |
7117
7127
 
7118
7128
  ## Taught in
@@ -7167,7 +7177,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7167
7177
 
7168
7178
  | Name | Signature | Meaning | Rules |
7169
7179
  | --- | --- | --- | --- |
7170
- | `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.12](/rulebook#13.12), [46.11](/rulebook#46.11) |
7180
+ | `busy` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether any entry in the selection has work in flight. | [13.2](/rulebook#13.2), [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.12](/rulebook#13.12), [41.28](/rulebook#41.28), [41.31](/rulebook#41.31), [46.11](/rulebook#46.11) |
7171
7181
 
7172
7182
  ## Taught in
7173
7183
 
@@ -7185,7 +7195,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7185
7195
 
7186
7196
  | Name | Signature | Meaning | Rules |
7187
7197
  | --- | --- | --- | --- |
7188
- | `ready` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection has a value to serve. | [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.11](/rulebook#13.11), [13.12](/rulebook#13.12) |
7198
+ | `ready` | `(selection?: Selection, pattern?: Partial<Args>) => boolean` | Whether every entry in the selection has a value to serve. | [13.3](/rulebook#13.3), [13.5](/rulebook#13.5), [13.11](/rulebook#13.11), [13.12](/rulebook#13.12), [41.28](/rulebook#41.28), [41.31](/rulebook#41.31) |
7189
7199
 
7190
7200
  ## Taught in
7191
7201
 
@@ -7221,7 +7231,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7221
7231
 
7222
7232
  | Name | Signature | Meaning | Rules |
7223
7233
  | --- | --- | --- | --- |
7224
- | `refresh` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Stale-while-revalidate over the selection. | [13.4](/rulebook#13.4), [13.7](/rulebook#13.7), [13.10](/rulebook#13.10), [13.12](/rulebook#13.12), [13.14](/rulebook#13.14), [13.15](/rulebook#13.15), [13.16](/rulebook#13.16), [13.17](/rulebook#13.17) |
7234
+ | `refresh` | `(selection?: Selection, pattern?: Partial<Args>) => void` | Stale-while-revalidate over the selection. | [13.4](/rulebook#13.4), [13.7](/rulebook#13.7), [13.10](/rulebook#13.10), [13.12](/rulebook#13.12), [13.14](/rulebook#13.14), [13.15](/rulebook#13.15), [13.16](/rulebook#13.16), [13.17](/rulebook#13.17), [13.20](/rulebook#13.20) |
7225
7235
 
7226
7236
  ## Taught in
7227
7237
 
@@ -8196,8 +8206,8 @@ Generated surfaces: what is written in place rather than imported.
8196
8206
 
8197
8207
  | Name | Signature | Meaning | Rules |
8198
8208
  | --- | --- | --- | --- |
8199
- | `route.url` | `Reactive<URL>` | Where we are, fragment included. | [23.1](/rulebook#23.1), [23.2](/rulebook#23.2), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [30.20](/rulebook#30.20), [30.21](/rulebook#30.21), [30.23](/rulebook#30.23), [19.16](/rulebook#19.16), [31.16](/rulebook#31.16) |
8200
- | `route.params` | `Reactive<Params>` | The matched route's segments. | [23.1](/rulebook#23.1), [23.5](/rulebook#23.5), [23.9](/rulebook#23.9), [30.23](/rulebook#30.23), [31.16](/rulebook#31.16) |
8209
+ | `route.url` | `Reactive<URL>` | Where we are, fragment included. | [23.1](/rulebook#23.1), [23.2](/rulebook#23.2), [23.3](/rulebook#23.3), [23.4](/rulebook#23.4), [30.20](/rulebook#30.20), [30.21](/rulebook#30.21), [30.23](/rulebook#30.23), [30.33](/rulebook#30.33), [19.16](/rulebook#19.16), [31.16](/rulebook#31.16) |
8210
+ | `route.params` | `Reactive<Params>` | The matched route's segments — typed by its own route in a page or a layout, and `Params` in any other file. | [23.1](/rulebook#23.1), [23.5](/rulebook#23.5), [23.9](/rulebook#23.9), [30.23](/rulebook#30.23), [31.16](/rulebook#31.16) |
8201
8211
  | `route.name` | `Reactive<string>` | The resolution path, the same under any mount path. | [23.1](/rulebook#23.1), [23.6](/rulebook#23.6), [23.14](/rulebook#23.14), [31.16](/rulebook#31.16) |
8202
8212
  | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | [23.1](/rulebook#23.1), [31.16](/rulebook#31.16) |
8203
8213
 
@@ -8961,7 +8971,7 @@ Generated surfaces: what is written in place rather than imported.
8961
8971
 
8962
8972
  | Name | Signature | Meaning | Rules |
8963
8973
  | --- | --- | --- | --- |
8964
- | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | [32.2](/rulebook#32.2), [32.24](/rulebook#32.24), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31), [32.70](/rulebook#32.70) |
8974
+ | `raw` | `(value: unknown) => Raw` | The branding a value takes to render as markup rather than as text. | [32.2](/rulebook#32.2), [32.24](/rulebook#32.24), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31), [32.70](/rulebook#32.70), [32.77](/rulebook#32.77) |
8965
8975
 
8966
8976
  ## Taught in
8967
8977
 
@@ -9084,7 +9094,7 @@ Pages and routing: what is written in place rather than imported.
9084
9094
 
9085
9095
  | Name | Signature | Meaning | Rules |
9086
9096
  | --- | --- | --- | --- |
9087
- | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | [29.10](/rulebook#29.10), [35.4](/rulebook#35.4), [35.5](/rulebook#35.5), [35.6](/rulebook#35.6), [35.15](/rulebook#35.15), [35.16](/rulebook#35.16), [41.1](/rulebook#41.1), [41.2](/rulebook#41.2), [41.3](/rulebook#41.3), [41.4](/rulebook#41.4), [41.9](/rulebook#41.9), [41.13](/rulebook#41.13), [41.16](/rulebook#41.16), [41.17](/rulebook#41.17), [41.18](/rulebook#41.18), [41.19](/rulebook#41.19), [41.20](/rulebook#41.20), [41.28](/rulebook#41.28), [35.11](/rulebook#35.11), [41.29](/rulebook#41.29), [41.30](/rulebook#41.30) |
9097
+ | `render` | `(component: Component) => Promise<Uint8Array>` | What produces a document. | [29.10](/rulebook#29.10), [35.4](/rulebook#35.4), [35.5](/rulebook#35.5), [35.6](/rulebook#35.6), [35.15](/rulebook#35.15), [35.16](/rulebook#35.16), [41.1](/rulebook#41.1), [41.2](/rulebook#41.2), [41.3](/rulebook#41.3), [41.4](/rulebook#41.4), [41.9](/rulebook#41.9), [41.13](/rulebook#41.13), [41.16](/rulebook#41.16), [41.17](/rulebook#41.17), [41.18](/rulebook#41.18), [41.19](/rulebook#41.19), [41.20](/rulebook#41.20), [41.28](/rulebook#41.28), [35.11](/rulebook#35.11), [41.29](/rulebook#41.29), [41.30](/rulebook#41.30), [41.31](/rulebook#41.31) |
9088
9098
 
9089
9099
  ## Taught in
9090
9100
 
@@ -9133,6 +9143,7 @@ Configuration: what is written in place rather than imported.
9133
9143
  | `**/lib/**` | The app's code that is no address: components and modules. | [23.15](/rulebook#23.15), [23.16](/rulebook#23.16), [23.18](/rulebook#23.18), [23.22](/rulebook#23.22), [37.1](/rulebook#37.1) |
9134
9144
  | `**/lib/**/<name>.server.<extension>` | A server file: source the browser never loads. | [16.29](/rulebook#16.29), [23.23](/rulebook#23.23), [34.20](/rulebook#34.20), [36.10](/rulebook#36.10) |
9135
9145
  | `**/<name>.test.ts` | A test file: one of the app's tests, wherever it lies. | [23.22](/rulebook#23.22), [38.109](/rulebook#38.109) |
9146
+ | `**/<name>.bench.ts` | A bench file: benches `bun run bench` runs, wherever it lies. | [23.22](/rulebook#23.22), [43.29](/rulebook#43.29) |
9136
9147
  | `**/<name>.d.ts` | A declaration file: types for the checker, wherever it lies. | [23.22](/rulebook#23.22) |
9137
9148
  | `**/public/**` | The files answered as authored, at their path below that directory. | [23.15](/rulebook#23.15), [23.16](/rulebook#23.16), [23.18](/rulebook#23.18), [36.11](/rulebook#36.11), [36.12](/rulebook#36.12), [36.13](/rulebook#36.13), [36.14](/rulebook#36.14), [36.15](/rulebook#36.15), [36.17](/rulebook#36.17), [21.12](/rulebook#21.12), [21.13](/rulebook#21.13), [21.14](/rulebook#21.14), [38.18](/rulebook#38.18), [38.19](/rulebook#38.19) |
9138
9149
  | `src/public/favicon.ico` | The file answered at `/favicon.ico`. | [36.11](/rulebook#36.11) |
@@ -9633,6 +9644,12 @@ The measurement harness: what is written in place rather than imported.
9633
9644
  | --- | --- | --- |
9634
9645
  | `preload = ["@abide/harness/preload"]` | The `[test]` preload that registers a DOM with the measure lane's patches installed. | [43.4](/rulebook#43.4), [43.28](/rulebook#43.28) |
9635
9646
 
9647
+ ## `@abide/harness/runner` — runners that print rows, and the bench runner
9648
+
9649
+ | Name | Meaning | Rules |
9650
+ | --- | --- | --- |
9651
+ | `harness-bench [word]` | The command that runs `runBenches` over the directory it runs in and prints each bench. | [43.33](/rulebook#43.33), [43.34](/rulebook#43.34) |
9652
+
9636
9653
  ## `@abide/harness/abide` — the adapter for an abide app
9637
9654
 
9638
9655
  | Name | Meaning | Rules |
@@ -10281,11 +10298,12 @@ The measurement harness: what is written in place rather than imported.
10281
10298
 
10282
10299
  `import { rowsCommand } from '@abide/harness/runner'`
10283
10300
 
10284
- ## `@abide/harness/runner` — runners that print rows
10301
+ ## `@abide/harness/runner` — runners that print rows, and the bench runner
10285
10302
 
10286
10303
  | Name | Signature | Meaning | Rules |
10287
10304
  | --- | --- | --- | --- |
10288
10305
  | `rowsCommand` | `<Item extends RowItem, Row>(items: Item[], run: (selected: Item[], emit: (row: Row) => void) => Promise<void>) => Promise<void>` | A runner's command line: its catalog under `--list`, else the rows of what it selects. | [43.26](/rulebook#43.26) |
10306
+ | `CEILINGS` | `Record<string, Record<string, number>>` | A bench file's export naming the most each arm of each of its benches may take, in milliseconds. | [43.29](/rulebook#43.29), [43.31](/rulebook#43.31), [43.32](/rulebook#43.32) |
10289
10307
 
10290
10308
  ## Taught in
10291
10309
 
@@ -10299,7 +10317,7 @@ The measurement harness: what is written in place rather than imported.
10299
10317
 
10300
10318
  `import { testRows } from '@abide/harness/runner'`
10301
10319
 
10302
- ## `@abide/harness/runner` — runners that print rows
10320
+ ## `@abide/harness/runner` — runners that print rows, and the bench runner
10303
10321
 
10304
10322
  | Name | Signature | Meaning | Rules |
10305
10323
  | --- | --- | --- | --- |
@@ -10317,7 +10335,7 @@ The measurement harness: what is written in place rather than imported.
10317
10335
 
10318
10336
  `import type { TestSuite } from '@abide/harness/runner'`
10319
10337
 
10320
- ## `@abide/harness/runner` — runners that print rows
10338
+ ## `@abide/harness/runner` — runners that print rows, and the bench runner
10321
10339
 
10322
10340
  | Name | Signature | Meaning | Rules |
10323
10341
  | --- | --- | --- | --- |
@@ -10329,6 +10347,24 @@ The measurement harness: what is written in place rather than imported.
10329
10347
 
10330
10348
  ---
10331
10349
 
10350
+ <!-- reference/harness-runner/runBenches.md -->
10351
+
10352
+ # `runBenches`
10353
+
10354
+ `import { runBenches } from '@abide/harness/runner'`
10355
+
10356
+ ## `@abide/harness/runner` — runners that print rows, and the bench runner
10357
+
10358
+ | Name | Signature | Meaning | Rules |
10359
+ | --- | --- | --- | --- |
10360
+ | `runBenches` | `(root: string, only?: string) => Promise<{ file: string; name: string; case: string; n: number; floor: number; arms: Record<string, number>; claim: string; over: string[] }[]>` | Every bench under `root`'s `src`, run, with each arm's time per operation and the ceilings it went over. | [43.29](/rulebook#43.29), [43.30](/rulebook#43.30), [43.31](/rulebook#43.31), [43.32](/rulebook#43.32), [43.34](/rulebook#43.34) |
10361
+
10362
+ ## Taught in
10363
+
10364
+ - [Test, measure, ship](/guide/test-measure-and-ship.md)
10365
+
10366
+ ---
10367
+
10332
10368
  <!-- reference/harness-abide/measure.md -->
10333
10369
 
10334
10370
  # `measure`
@@ -10375,20 +10411,20 @@ CLI: what is written in place rather than imported.
10375
10411
 
10376
10412
  | Command | Meaning | Rules |
10377
10413
  | --- | --- | --- |
10378
- | `abide scaffold <name>` | Writes a starter project. | [38.1](/rulebook#38.1), [38.42](/rulebook#38.42), [38.103](/rulebook#38.103), [38.104](/rulebook#38.104), [38.106](/rulebook#38.106), [38.112](/rulebook#38.112) |
10414
+ | `abide scaffold <name>` | Writes a starter project. | [38.1](/rulebook#38.1), [38.42](/rulebook#38.42), [38.103](/rulebook#38.103), [38.104](/rulebook#38.104), [38.106](/rulebook#38.106), [38.112](/rulebook#38.112), [38.122](/rulebook#38.122), [38.124](/rulebook#38.124) |
10379
10415
  | `abide run <file> [args…]` | Runs a script under the abide runtime. | [38.2](/rulebook#38.2), [38.57](/rulebook#38.57) |
10380
- | `abide check [dir…]` | The type-checker over a `.abide` tree. | [38.3](/rulebook#38.3), [38.31](/rulebook#38.31), [38.32](/rulebook#38.32), [38.33](/rulebook#38.33), [38.40](/rulebook#38.40), [38.105](/rulebook#38.105) |
10381
- | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | [38.108](/rulebook#38.108) |
10382
- | `abide dev [--port <n>]` | Watches the project and keeps the app up. | [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [37.8](/rulebook#37.8), [37.9](/rulebook#37.9), [38.4](/rulebook#38.4), [38.14](/rulebook#38.14), [38.15](/rulebook#38.15), [38.16](/rulebook#38.16), [38.17](/rulebook#38.17), [38.19](/rulebook#38.19), [38.24](/rulebook#38.24), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.28](/rulebook#38.28), [38.30](/rulebook#38.30), [38.35](/rulebook#38.35), [38.36](/rulebook#38.36), [38.37](/rulebook#38.37), [38.38](/rulebook#38.38), [38.41](/rulebook#38.41), [38.43](/rulebook#38.43), [38.58](/rulebook#38.58), [38.63](/rulebook#38.63), [38.64](/rulebook#38.64), [38.65](/rulebook#38.65), [38.94](/rulebook#38.94), [38.96](/rulebook#38.96), [38.97](/rulebook#38.97) |
10383
- | `abide build` | Builds the client into content-hashed chunks and a manifest. | [38.43](/rulebook#38.43), [38.5](/rulebook#38.5), [38.18](/rulebook#38.18), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.30](/rulebook#38.30), [38.66](/rulebook#38.66), [38.94](/rulebook#38.94) |
10384
- | `abide start [--port <n>]` | Boots the built app. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.100](/rulebook#38.100), [38.20](/rulebook#38.20), [38.21](/rulebook#38.21), [38.22](/rulebook#38.22), [38.23](/rulebook#38.23), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.28](/rulebook#38.28), [38.29](/rulebook#38.29), [38.58](/rulebook#38.58), [38.67](/rulebook#38.67), [38.68](/rulebook#38.68), [38.95](/rulebook#38.95), [38.97](/rulebook#38.97), [38.110](/rulebook#38.110) |
10385
- | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.7](/rulebook#38.7), [38.53](/rulebook#38.53), [38.54](/rulebook#38.54), [38.55](/rulebook#38.55), [38.58](/rulebook#38.58), [38.59](/rulebook#38.59), [38.60](/rulebook#38.60), [38.62](/rulebook#38.62) |
10416
+ | `abide check [dir…]` | The type-checker over a `.abide` tree. | [38.3](/rulebook#38.3), [38.31](/rulebook#38.31), [38.32](/rulebook#38.32), [38.33](/rulebook#38.33), [38.40](/rulebook#38.40), [38.105](/rulebook#38.105), [38.123](/rulebook#38.123) |
10417
+ | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | [38.108](/rulebook#38.108), [38.123](/rulebook#38.123) |
10418
+ | `abide dev [--port <n>]` | Watches the project and keeps the app up. | [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [37.8](/rulebook#37.8), [37.9](/rulebook#37.9), [38.4](/rulebook#38.4), [38.14](/rulebook#38.14), [38.15](/rulebook#38.15), [38.16](/rulebook#38.16), [38.17](/rulebook#38.17), [38.19](/rulebook#38.19), [38.24](/rulebook#38.24), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.28](/rulebook#38.28), [38.30](/rulebook#38.30), [38.35](/rulebook#38.35), [38.36](/rulebook#38.36), [38.37](/rulebook#38.37), [38.38](/rulebook#38.38), [38.41](/rulebook#38.41), [38.43](/rulebook#38.43), [38.58](/rulebook#38.58), [38.63](/rulebook#38.63), [38.64](/rulebook#38.64), [38.65](/rulebook#38.65), [38.94](/rulebook#38.94), [38.96](/rulebook#38.96), [38.97](/rulebook#38.97), [38.123](/rulebook#38.123) |
10419
+ | `abide build` | Builds the client into content-hashed chunks and a manifest. | [38.43](/rulebook#38.43), [38.5](/rulebook#38.5), [38.18](/rulebook#38.18), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.30](/rulebook#38.30), [38.66](/rulebook#38.66), [38.94](/rulebook#38.94), [38.123](/rulebook#38.123) |
10420
+ | `abide start [--port <n>]` | Boots the built app. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.100](/rulebook#38.100), [38.20](/rulebook#38.20), [38.21](/rulebook#38.21), [38.22](/rulebook#38.22), [38.23](/rulebook#38.23), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.27](/rulebook#38.27), [38.28](/rulebook#38.28), [38.29](/rulebook#38.29), [38.58](/rulebook#38.58), [38.67](/rulebook#38.67), [38.68](/rulebook#38.68), [38.95](/rulebook#38.95), [38.97](/rulebook#38.97), [38.110](/rulebook#38.110), [38.123](/rulebook#38.123) |
10421
+ | `abide [--url <origin>] [--port <n>]` | The interactive shell — on the built app it boots, or on the running app `--url` names. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.6](/rulebook#38.6), [38.7](/rulebook#38.7), [38.53](/rulebook#38.53), [38.54](/rulebook#38.54), [38.55](/rulebook#38.55), [38.58](/rulebook#38.58), [38.59](/rulebook#38.59), [38.60](/rulebook#38.60), [38.62](/rulebook#38.62), [38.123](/rulebook#38.123) |
10386
10422
  | `abide call <address> [args]` | One call to one handler. | [38.8](/rulebook#38.8), [38.61](/rulebook#38.61), [38.110](/rulebook#38.110) |
10387
- | `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. | [38.9](/rulebook#38.9), [38.25](/rulebook#38.25), [38.110](/rulebook#38.110), [38.111](/rulebook#38.111) |
10388
- | `abide mcp [--url <origin>]` | The app's MCP server over stdio. | [38.9](/rulebook#38.9), [38.13](/rulebook#38.13), [38.25](/rulebook#38.25), [38.110](/rulebook#38.110) |
10423
+ | `abide openapi [--url <origin>]` | The OpenAPI document printed rather than served. | [38.9](/rulebook#38.9), [38.25](/rulebook#38.25), [38.110](/rulebook#38.110), [38.111](/rulebook#38.111), [38.123](/rulebook#38.123) |
10424
+ | `abide mcp [--url <origin>]` | The app's MCP server over stdio. | [38.9](/rulebook#38.9), [38.13](/rulebook#38.13), [38.25](/rulebook#38.25), [38.110](/rulebook#38.110), [38.123](/rulebook#38.123) |
10389
10425
  | `abide watch <address> [args]` | A live subscription to one of the app's followed `GET`s. | [38.69](/rulebook#38.69) |
10390
- | `abide compile [--target] [--out] [--platforms]` | One standalone executable. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.44](/rulebook#38.44), [38.45](/rulebook#38.45), [38.46](/rulebook#38.46), [38.47](/rulebook#38.47), [38.48](/rulebook#38.48), [38.49](/rulebook#38.49), [38.50](/rulebook#38.50), [38.51](/rulebook#38.51), [38.52](/rulebook#38.52), [38.56](/rulebook#38.56), [38.93](/rulebook#38.93), [38.97](/rulebook#38.97) |
10391
- | `abide bundle [--out]` | A desktop app for the host platform, with a window on the app. | [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.70](/rulebook#38.70), [38.71](/rulebook#38.71), [38.72](/rulebook#38.72), [38.73](/rulebook#38.73), [38.74](/rulebook#38.74), [38.75](/rulebook#38.75), [38.76](/rulebook#38.76), [38.77](/rulebook#38.77), [38.78](/rulebook#38.78), [38.79](/rulebook#38.79), [38.80](/rulebook#38.80), [38.81](/rulebook#38.81), [38.82](/rulebook#38.82), [38.83](/rulebook#38.83), [38.84](/rulebook#38.84), [38.85](/rulebook#38.85), [38.86](/rulebook#38.86), [38.87](/rulebook#38.87), [38.88](/rulebook#38.88), [38.89](/rulebook#38.89), [38.90](/rulebook#38.90), [38.91](/rulebook#38.91), [38.92](/rulebook#38.92), [38.101](/rulebook#38.101), [38.102](/rulebook#38.102) |
10426
+ | `abide compile [--target] [--out] [--platforms]` | One standalone executable. | [37.8](/rulebook#37.8), [37.10](/rulebook#37.10), [37.11](/rulebook#37.11), [37.12](/rulebook#37.12), [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.44](/rulebook#38.44), [38.45](/rulebook#38.45), [38.46](/rulebook#38.46), [38.47](/rulebook#38.47), [38.48](/rulebook#38.48), [38.49](/rulebook#38.49), [38.50](/rulebook#38.50), [38.51](/rulebook#38.51), [38.52](/rulebook#38.52), [38.56](/rulebook#38.56), [38.93](/rulebook#38.93), [38.97](/rulebook#38.97), [38.123](/rulebook#38.123) |
10427
+ | `abide bundle [--out]` | A desktop app for the host platform, with a window on the app. | [38.10](/rulebook#38.10), [38.25](/rulebook#38.25), [38.26](/rulebook#38.26), [38.70](/rulebook#38.70), [38.71](/rulebook#38.71), [38.72](/rulebook#38.72), [38.73](/rulebook#38.73), [38.74](/rulebook#38.74), [38.75](/rulebook#38.75), [38.76](/rulebook#38.76), [38.77](/rulebook#38.77), [38.78](/rulebook#38.78), [38.79](/rulebook#38.79), [38.80](/rulebook#38.80), [38.81](/rulebook#38.81), [38.82](/rulebook#38.82), [38.83](/rulebook#38.83), [38.84](/rulebook#38.84), [38.85](/rulebook#38.85), [38.86](/rulebook#38.86), [38.87](/rulebook#38.87), [38.88](/rulebook#38.88), [38.89](/rulebook#38.89), [38.90](/rulebook#38.90), [38.91](/rulebook#38.91), [38.92](/rulebook#38.92), [38.101](/rulebook#38.101), [38.102](/rulebook#38.102), [38.123](/rulebook#38.123) |
10392
10428
  | `abide lsp` | The `.abide` language server, over stdio. | [38.11](/rulebook#38.11), [38.34](/rulebook#38.34), [38.39](/rulebook#38.39), [38.98](/rulebook#38.98), [38.116](/rulebook#38.116), [38.117](/rulebook#38.117), [38.118](/rulebook#38.118) |
10393
10429
  | `--help` · `-h` · `abide help` | Usage, generated from the command list. | [38.12](/rulebook#38.12), [38.113](/rulebook#38.113), [38.114](/rulebook#38.114) |
10394
10430
  | `--version` · `-v` | The version of abide, or of the app a compiled executable holds. | [38.115](/rulebook#38.115) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.116.4",
3
+ "version": "0.117.1",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -20,8 +20,14 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
20
20
  Where the read is meant, `// abide-ignore` and the clause the warning cites, on the line above
21
21
  (`// abide-ignore 14.15`), silences that one (RULEBOOK 14.19); a read hoisted out of its place
22
22
  only to quiet a warning is the bug it named.
23
- - `bun run test` — runs `bun test`. `src/getGreeting.test.ts` is the pattern: a test imports a
24
- handler or a `.abide` file and calls it.
23
+ - `bun run test` — runs `bun test`. `src/getGreeting.test.ts` is the pattern for a handler: a test
24
+ imports it and calls it. `src/index.test.ts` is the pattern for a page: `mount` it, then count
25
+ the work a click did with `measure`, both from `@abide/harness/abide` (RULEBOOK 38.119, 43.23).
26
+ - `bun run bench` — runs `harness-bench`: every `*.bench.ts` under `src/`, kept beside the code it
27
+ times. Each export is a function returning `batch()` from `@abide/harness/report`, an op at two
28
+ sizes; a `CEILINGS` export, `{ [bench]: { [arm]: milliseconds } }`, names the most each arm may
29
+ take, and an arm over it, or a ceiling naming no bench or arm, fails the run (RULEBOOK 43.29,
30
+ 43.31, 43.32, 43.33).
25
31
  - `bun run dev` — serves the app on a local port and rebuilds when a file changes.
26
32
  - `bun run build` then `bun run start` — the production build, and the app served from it.
27
33
 
@@ -35,10 +41,15 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
35
41
  scripts, the CLI and MCP alike.
36
42
  - A `.abide` cannot export. A `<script server>` in one holds that file's private handlers
37
43
  (`const save = POST(…)`), callable by that file alone, and its hooks (`onStart`, `middleware`, …).
38
- - A directory holds addresses, tests, `.d.ts` declarations and two fenced directories alone: `lib/` for components and
39
- modules, and `public/` for files served as they are (`src/blog/public/cover.png` answers
40
- `/blog/cover.png`). Anything else beside the pages is a build error.
41
- - A test is a `.test.ts`, kept beside the file it tests — a page, a handler, or a module in `lib/`.
44
+ - A directory holds addresses, tests, benches, `.d.ts` declarations and two fenced directories alone: `lib/` for components and
45
+ modules, and `public/` for files served as they are. Anything else beside the pages is a build error.
46
+ - Either fence may sit in any directory, at the root or beside the pages that use it.
47
+ `src/lib/` and `src/invoices/lib/` import alike (`#src/…`); the one difference is a lifecycle
48
+ hook a `lib/` file calls, filed at the route of the directory holding that `lib/` (RULEBOOK 37.1).
49
+ A `public/` file answers at the route of the directory holding it: `src/public/logo.png` answers
50
+ `/logo.png`, and `src/blog/public/cover.png` answers `/blog/cover.png`.
51
+ - A test is a `.test.ts`, and a bench a `.bench.ts`, kept beside the file it tests or times — a
52
+ page, a handler, or a module in `lib/` (RULEBOOK 23.22).
42
53
  A browser test (playwright) is a `.test.ts` too, under `e2e/` beside `src/`: `bun test` reads
43
54
  `src/` alone (`root` in `bunfig.toml`), so it never runs one.
44
55
  - A module the browser never loads is named `<name>.server.ts` and lives in `lib/`
@@ -50,6 +61,13 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
50
61
 
51
62
  ## Habits
52
63
 
64
+ - Measure with `@abide/harness`, not a hand-written timer, bench runner or Chrome DevTools Protocol
65
+ script. Its three lanes: `measure` from `@abide/harness/abide` is the DOM work and wakes an op
66
+ did, under `bun test`; `batch` from `@abide/harness/report` is time, in a bench; `traceOp` and
67
+ `shares` from `@abide/harness/engine` are what Chromium did, from a playwright test. The page
68
+ `guide/test-measure-and-ship.md` in `abide.md`, and "The measurement harness" in `REGISTRY.md`,
69
+ document it.
70
+
53
71
  - Export a `Reactive` itself, never wrapped in an object: an `.abide` file reads and writes it by
54
72
  name (RULEBOOK 31.1), and one reached through a property is left to `x()` and `x.set(v)` (31.7).
55
73
  - Model output and fetched HTML are untrusted. Never hand them to `raw()` (RULEBOOK 32.2)
@@ -915,8 +915,9 @@ export class TypeSession {
915
915
  checked.push({ source, map, refusals: shadow.refusals })
916
916
  }
917
917
  // 34.6 — WHAT THE BUNDLER RESOLVES AND THE CHECKER CANNOT: a stylesheet imported for its
918
- // effect. Declared in a file of its own at the app's root, which is a script, because a
919
- // pattern module can only be declared outside a module.
918
+ // effect, and (34.21) markdown imported as text into a `.ts`, which bun's own types leave
919
+ // out where they declare `.txt`. Declared in a file of its own at the app's root, which is
920
+ // a script, because a pattern module can only be declared outside a module.
920
921
  this.register(
921
922
  `${this.canonical(within)}/${AMBIENT}`,
922
923
  AMBIENT_DECLARATIONS,
@@ -1484,6 +1485,10 @@ function routesDeclaration(addresses: readonly string[]): string {
1484
1485
  return `export {}\ndeclare module ${JSON.stringify(URL_SOURCE)} {\n interface Routes {\n${keys} }\n}\n`
1485
1486
  }
1486
1487
  const AMBIENT_DECLARATIONS = `declare module '*.css' {}
1488
+ declare module '*.md' {
1489
+ const text: string
1490
+ export default text
1491
+ }
1487
1492
  `
1488
1493
 
1489
1494
  /**
@@ -1,9 +1,9 @@
1
1
  import type { JsonValue } from '#shared/JsonValue.ts'
2
- import { SERVER_SPECIFIER } from '#shared/SPECIFIERS.ts'
2
+ import { SERVER_SPECIFIER, SHARED_SPECIFIER } from '#shared/SPECIFIERS.ts'
3
3
  import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
4
4
  import { Cursor, placeTemplate } from './Cursor.ts'
5
5
  import { type LoweredChunk, Lowering, PROPS } from './Lowering.ts'
6
- import type { AbideFile } from './parseAbide.ts'
6
+ import { type AbideFile, scriptBodies } from './parseAbide.ts'
7
7
  import { privateExports } from './projectShadow.ts'
8
8
  import {
9
9
  concat,
@@ -15,7 +15,7 @@ import {
15
15
  SERVER_LANE,
16
16
  } from './renderMarkup.ts'
17
17
  import { insideShare, SETTLED_AT, shareScope } from './shareScope.ts'
18
- import { splitImports, withoutProps } from './splitImports.ts'
18
+ import { importStatements, splitImports, withoutProps } from './splitImports.ts'
19
19
  import type { TemplateNode } from './TemplateNode.ts'
20
20
 
21
21
  // The name the body is bound to while a component's head contributions land (`HEADS`).
@@ -41,7 +41,8 @@ const CALLS_BEFORE = '__callsBefore'
41
41
  * `settles` IS A PAGE OR A LAYOUT, which `render` builds a level at a time (41.19): its setup runs
42
42
  * at the call and its markup is built on the first read of `head` or `body`, so the calls the
43
43
  * setup started are given the turn to answer. Every other component answers the same way only where
44
- * its setup made a call (41.28), and builds where it is called otherwise.
44
+ * its setup made a call or its markup reads a bare `busy` or `ready` (41.28), and builds where it is
45
+ * called otherwise.
45
46
  */
46
47
  export function emitComponent(
47
48
  file: AbideFile,
@@ -144,16 +145,22 @@ ${heldToAnnotations(lowering, derived)}${privateExports(file.servers.join('\n'))
144
145
  // made a call, and then answers as a page does, for `renderComponent` to build after the turn.
145
146
  // THE BUILD IS WRITTEN TWICE rather than always run through `__build`: the closure measured ~4%
146
147
  // a row on `bench:component`'s card under JSC, the second copy nothing but server module bytes.
147
- // One with no setup cannot call, and is written once.
148
- const waits = !settles && componentBody.trim() !== ''
149
- const level = settles
150
- ? deferred()
151
- : waits
152
- ? ` if (${lowering.use('callsInRender')}() !== ${CALLS_BEFORE}) {
148
+ // One with no setup cannot call, and is written once. One whose markup reads a bare `busy` or
149
+ // `ready` answers as a page does always: built where it is called, it would answer before the
150
+ // siblings after it had started a load, where the browser's answers once every setup has run.
151
+ const probes =
152
+ !settles &&
153
+ readsBareProbe(file, `${head}\n${declared}\n${body.join('\n')}`)
154
+ const waits = !settles && !probes && componentBody.trim() !== ''
155
+ const level =
156
+ settles || probes
157
+ ? deferred()
158
+ : waits
159
+ ? ` if (${lowering.use('callsInRender')}() !== ${CALLS_BEFORE}) {
153
160
  ${deferred()}
154
161
  }
155
162
  ${build}`
156
- : build
163
+ : build
157
164
  return `${moduleImports(lowering, () => SERVER_SPECIFIER, [moduleSetup, serverSetup, setup])}
158
165
  ${moduleSetup.body === '' ? '' : `${moduleSetup.body}\n`}${serverBody}export default function Component(${lowering.takesProps() ? `${PROPS} = {}` : ''}) {
159
166
  ${waits ? ` const ${CALLS_BEFORE} = ${lowering.use('callsInRender')}()\n` : ''}${opened}${componentBody}
@@ -162,6 +169,29 @@ ${level}${closed}
162
169
  `
163
170
  }
164
171
 
172
+ /**
173
+ * Whether `markup`, a component's emitted head and body, calls `busy` or `ready` with no argument,
174
+ * under any name the file's scripts import either by from `@abide/abide`. A member's (`s.busy()`)
175
+ * is not bare.
176
+ */
177
+ function readsBareProbe(file: AbideFile, markup: string): boolean {
178
+ let names = ''
179
+ for (const script of scriptBodies(file))
180
+ for (const statement of importStatements(script))
181
+ if (statement.specifier === SHARED_SPECIFIER && !statement.typeOnly)
182
+ for (const binding of statement.bindings)
183
+ if (
184
+ !binding.typeOnly &&
185
+ (binding.imported === 'busy' ||
186
+ binding.imported === 'ready')
187
+ )
188
+ names += `${names === '' ? '' : '|'}${binding.local}`
189
+ return (
190
+ names !== '' &&
191
+ new RegExp(`(?<![\\w$.])(?:${names})\\(\\s*\\)`).test(markup)
192
+ )
193
+ }
194
+
165
195
  /**
166
196
  * 15.8, 19.9 — each private handler held to the schema its annotation derives, once its server block
167
197
  * has built it: the module is what the server mounts and what a component under `mount` calls
@@ -35,15 +35,17 @@ const SOURCE_ALIAS = { '#src/*': ['./src/*', './src/*.ts'] }
35
35
  * 38.109, 38.119 — the starter's `bunfig.toml`: the harness's abide preload, which is a DOM a test
36
36
  * can mount into and measure, then abide's own — the app's `.abide` files imported as modules, with
37
37
  * nothing booted first. Its test root is `src`, so a browser test under `e2e` is playwright's alone.
38
+ * 38.122 — and no peer installed: `bun-plugin-tailwind` names `bun` as one, and an installed copy
39
+ * lands first on the PATH inside `bun run`, shadowing the bun that runs everything else.
38
40
  */
39
- export const TEST_PRELOAD =
40
- '# 38.109, 38.119 — a DOM to mount into and measure, then the app\'s `.abide` files as modules.\n[test]\nroot = "src"\npreload = ["@abide/harness/abide/preload"]\n'
41
+ export const BUNFIG =
42
+ '# 38.122 — a peer is not installed: an installed `bun` would shadow this one inside `bun run`.\n[install]\npeer = false\n\n# 38.109, 38.119 — a DOM to mount into and measure, then the app\'s `.abide` files as modules.\n[test]\nroot = "src"\npreload = ["@abide/harness/abide/preload"]\n'
41
43
 
42
44
  // WRITTEN, NOT COPIED, beside the manifests: a packer drops a `bunfig.toml` or a `.gitignore` from
43
45
  // the tarball (`bun pm pack` both, `npm pack` the second), so neither survives as a starter file.
44
46
  function filesFor(name: string): Record<string, string> {
45
47
  return {
46
- 'bunfig.toml': TEST_PRELOAD,
48
+ 'bunfig.toml': BUNFIG,
47
49
  '.gitignore': 'node_modules\n.abide\n',
48
50
  'package.json': `${JSON.stringify(
49
51
  {
@@ -60,6 +62,8 @@ function filesFor(name: string): Record<string, string> {
60
62
  routes: 'abide routes',
61
63
  // 38.112 — the starter's own test, under the 38.109 preload.
62
64
  test: 'bun test',
65
+ // 38.124 — every `*.bench.ts` under `src`, timed, and failed over its `CEILINGS`.
66
+ bench: 'harness-bench',
63
67
  },
64
68
  // ONE ALIAS FOR THE SOURCE, and two targets for it, because an extensionless
65
69
  // specifier is how an import is written and only bun probes for the extension:
@@ -72,7 +76,8 @@ function filesFor(name: string): Record<string, string> {
72
76
  dependencies: { '@abide/abide': ABIDE_DEPENDENCY },
73
77
  // `Bun`, `bun:sqlite` and the rest of the runtime a server module reaches for are
74
78
  // typed by these, and `abide check` reports every one untyped without them. The
75
- // harness is what the starter's tests measure the work a click does with (38.119).
79
+ // harness is what the starter's tests measure the work a click does with (38.119), and
80
+ // what `bench` runs (38.124).
76
81
  devDependencies: {
77
82
  '@abide/harness': HARNESS_DEPENDENCY,
78
83
  '@types/bun': 'latest',
@@ -23,6 +23,7 @@ import {
23
23
  DIRECTORY_FILES,
24
24
  isDeclarationFile,
25
25
  isServerFile,
26
+ isBenchFile,
26
27
  isTestFile,
27
28
  placeOf,
28
29
  routeOf,
@@ -111,7 +112,7 @@ function walk(
111
112
  refusals.at(
112
113
  path,
113
114
  new Error(
114
- `abide: ${path} is a server file under no lib directory. Move it into a lib directory beside it. RULEBOOK 23.23.`,
115
+ `abide: ${path} is a server file under no lib directory. Move it into a lib directory, beside it or in any directory above it. RULEBOOK 23.23.`,
115
116
  ),
116
117
  )
117
118
  continue
@@ -119,14 +120,19 @@ function walk(
119
120
  const abide = path.endsWith('.abide')
120
121
  if (abide) files.abide.push(path)
121
122
  if (place === 'address') {
122
- if (isTestFile(path) || isDeclarationFile(path)) continue
123
+ if (
124
+ isTestFile(path) ||
125
+ isBenchFile(path) ||
126
+ isDeclarationFile(path)
127
+ )
128
+ continue
123
129
  if (!abide && !isModule(path)) {
124
130
  if (packaged && relative === '' && !path.endsWith('.ts'))
125
131
  continue
126
132
  refusals.at(
127
133
  path,
128
134
  new Error(
129
- `abide: ${path} is directly in an address directory, which holds pages, layouts, error pages, handler files, test files and declaration files alone. Move it into a lib directory beside it. RULEBOOK 23.22.`,
135
+ `abide: ${path} is directly in an address directory, which holds pages, layouts, error pages, handler files, test files, bench files and declaration files alone. Move it into a lib directory, beside it or in any directory above it. RULEBOOK 23.22.`,
130
136
  ),
131
137
  )
132
138
  continue
@@ -152,9 +158,14 @@ function walk(
152
158
  }
153
159
  }
154
160
 
155
- /** Whether `path` names a module the app runs — neither a declaration nor a test. */
161
+ /** Whether `path` names a module the app runs — neither a declaration, a test nor a bench. */
156
162
  function isModule(path: string): boolean {
157
- return path.endsWith('.ts') && !isDeclarationFile(path) && !isTestFile(path)
163
+ return (
164
+ path.endsWith('.ts') &&
165
+ !isDeclarationFile(path) &&
166
+ !isTestFile(path) &&
167
+ !isBenchFile(path)
168
+ )
158
169
  }
159
170
 
160
171
  /** The refusal of `source` where `route` lies in the framework's own namespace, or `undefined`. 23.17. */
@@ -1,4 +1,5 @@
1
- // 41.28 — A CHILD WHOSE SETUP MADE A CALL IS BUILT AS A PAGE IS: after the turn.
1
+ // 41.28 — A CHILD WHOSE SETUP MADE A CALL IS BUILT AS A PAGE IS: after the turn — and so is one whose
2
+ // markup reads a bare `busy` or `ready`, which the compiler writes to answer as a page does always.
2
3
  // A component is set up where its parent's markup invokes it, so its build cannot wait inline; it is
3
4
  // left as a marker in the parent's head and body, built once the turn has run, and written in where
4
5
  // its marker stands. A child whose setup made no call builds where it stands, as before, and pays a
@@ -109,6 +109,11 @@ export function isTestFile(path: string): boolean {
109
109
  return path.endsWith('.test.ts')
110
110
  }
111
111
 
112
+ /** Whether `path` names a bench file, which `bun run bench` reads and the app never runs. */
113
+ export function isBenchFile(path: string): boolean {
114
+ return path.endsWith('.bench.ts')
115
+ }
116
+
112
117
  /** Whether `path` names a declaration file, which the checker reads and nothing runs. */
113
118
  export function isDeclarationFile(path: string): boolean {
114
119
  return path.endsWith('.d.ts')
@@ -30,8 +30,9 @@ export function renderComponent(
30
30
  const rendered = (
31
31
  component as (props: Record<string, () => unknown>) => Rendered
32
32
  )(props)
33
- // 41.28 — A CHILD WHOSE SETUP MADE A CALL ANSWERS AS A PAGE DOES, with a `scope` and its build
34
- // left for the first read, and is built after the turn where a page is being built.
33
+ // 41.28 — A CHILD WHOSE SETUP MADE A CALL, OR WHOSE MARKUP READS A BARE PROBE, ANSWERS AS A PAGE
34
+ // DOES, with a `scope` and its build left for the first read, and is built after the turn where a
35
+ // page is being built.
35
36
  if ('scope' in rendered) {
36
37
  const marker = deferBuild(rendered, heads, callsFrom)
37
38
  if (marker !== undefined) return marker
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Why the bun at `version` cannot run abide, which asks for `range` — or `undefined` where it can.
3
+ *
4
+ * 38.123 — named in full, both versions and the way out, since an older bun fails later and
5
+ * somewhere unrelated, if at all.
6
+ */
7
+ export function runtimeRefusal(
8
+ version: string,
9
+ range: string,
10
+ ): string | undefined {
11
+ if (Bun.semver.satisfies(version, range)) return undefined
12
+ return `this is bun ${version}, and abide needs bun ${range}. Run \`bun upgrade\`, or install a bun in that range. RULEBOOK 38.123.`
13
+ }