@abide/abide 0.117.0 → 0.117.2

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,20 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.117.2
4
+
5
+ ### Patch Changes
6
+
7
+ - ea4e27f: a value opening at a member of a name read by name — `reactive(route.url.searchParams.get('book') ?? '')`, `reactive(name.toUpperCase())` — is now a `Reactive` the file reads and writes by name, where before `abide check` refused every write as a string assigned to the `Reactive` and the build rebound the name instead of calling `set`
8
+ - ea4e27f: a value a `refresh({ tags })` marked while nothing read it loads when the next page adopts it — `reactive(() => getMeetings())` on a page visited after the write sends the request and serves the new answer, where it served the answer held before the write and asked nothing (RULEBOOK 13.7)
9
+
10
+ ## 0.117.1
11
+
12
+ ### Patch Changes
13
+
14
+ - 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))
15
+ - 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))
16
+ - 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)
17
+
3
18
  ## 0.117.0
4
19
 
5
20
  ### Minor Changes
package/docs/REGISTRY.md CHANGED
@@ -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,7 +856,7 @@ 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, 38.122 |
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
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 |
855
862
  | `abide routes [--json]` | Every address the app answers, the file answering it, and where each hook is filed. | 38.108, 38.123 |
package/docs/RULEBOOK.md CHANGED
@@ -1549,7 +1549,7 @@ error naming the mount.
1549
1549
  `layout.abide`, an `error.abide` and the root's `launcher.abide`.
1550
1550
 
1551
1551
  23.22 A file of the app's own directly in a directory under no fence that is neither a `.abide`, a
1552
- 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
1553
1553
  beside the `package.json` of a root that holds it, nor a `.ts`.
1554
1554
 
1555
1555
  23.23 A server file of the app's own under no `lib` directory MUST be a build error naming the file
@@ -2784,6 +2784,8 @@ answered in the test's process by the handler the component's server module decl
2784
2784
  is outside the range abide's `engines.bun` names, and the refusal MUST name that bun's version and
2785
2785
  the range.
2786
2786
 
2787
+ 38.124 The project `abide scaffold` writes MUST carry a `bench` script that runs `harness-bench`.
2788
+
2787
2789
  # 39. Head and view transitions
2788
2790
 
2789
2791
  39.1 A `<head>` element in a `.abide` file MUST contribute its children to the document head.
@@ -3198,6 +3200,20 @@ runner wrote no report as `broken` rather than as a result.
3198
3200
  43.28 The DOM `@abide/harness/preload` registers MUST leave the runtime's own `fetch`, `Request`,
3199
3201
  `Response`, `Headers`, `Blob`, `File`, `FormData`, `AbortController` and `AbortSignal` the globals.
3200
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
+
3201
3217
  # 44. The assistant
3202
3218
 
3203
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`
@@ -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
 
@@ -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,7 +10411,7 @@ 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), [38.122](/rulebook#38.122) |
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
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) |
10381
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) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.117.0",
3
+ "version": "0.117.2",
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)
@@ -167,7 +167,7 @@ function copiedFrom(
167
167
  * markup, or a read the lowering rewrote. Runs are ascending by where they stand, not by where they
168
168
  * came from, so this is a scan.
169
169
  */
170
- function mapForward(
170
+ export function mapForward(
171
171
  origins: readonly Origin[],
172
172
  position: number,
173
173
  ): number | undefined {
@@ -38,10 +38,15 @@ import { CHECKED_VALUES } from './Lowering.ts'
38
38
  import { diagnosisAt, lineOf } from './lineOf.ts'
39
39
  import type { AbideFile } from './parseAbide.ts'
40
40
  import { projectShadow, type Span } from './projectShadow.ts'
41
- import { abideIn } from './reactiveTypes.ts'
41
+ import { type Abide, abideIn, isReactiveType } from './reactiveTypes.ts'
42
42
  import type { ParsedSource, ReadSource } from './readSources.ts'
43
43
  import { readsIn, type ShadowSpans } from './readsIn.ts'
44
- import { CHECKED_SUFFIX, SHADOW_SUFFIX, ShadowMap } from './ShadowMap.ts'
44
+ import {
45
+ CHECKED_SUFFIX,
46
+ mapForward,
47
+ SHADOW_SUFFIX,
48
+ ShadowMap,
49
+ } from './ShadowMap.ts'
45
50
  import { SOURCE_SUFFIX } from './splitImports.ts'
46
51
  import { type TemplateContext, templateContext } from './templateContext.ts'
47
52
  import { templateWarnings } from './templateWarnings.ts'
@@ -711,56 +716,144 @@ export class TypeSession {
711
716
  if (queries.every((query) => query === undefined))
712
717
  return { answers, warnings }
713
718
 
714
- const project = await this.currentProject(created, changed)
719
+ let project = await this.currentProject(created, changed)
715
720
  if (project === undefined) return { answers, warnings }
716
- const checker = project.checker
717
- const abide = await abideIn(project)
718
-
719
- await Promise.all(
720
- queries.map(async (query, at) => {
721
- if (query === undefined) return
722
- const source = sources[at] as (typeof sources)[number]
723
- const sourceFile = await project.program.getSourceFile(
721
+ // Per source, its compiler warnings, the bindings the projection could not type
722
+ // (`bindingsOfReads`), and those the lowered program proved a `Reactive`.
723
+ const warned: Diagnosis[][] = sources.map(() => [])
724
+ const suspects: number[][] = sources.map(() => [])
725
+ const held: Set<number>[] = sources.map(() => new Set())
726
+ const answer = async (
727
+ asked: Project,
728
+ abide: Abide,
729
+ at: number,
730
+ ): Promise<void> => {
731
+ const query = queries[at]
732
+ if (query === undefined) return
733
+ const source = sources[at] as (typeof sources)[number]
734
+ const sourceFile = await asked.program.getSourceFile(
735
+ query.map.shadowPath,
736
+ )
737
+ // The shadow has to be INSIDE the opened project or the checker never reads it.
738
+ // Refusing loudly rather than answering nothing: no reads is a file whose every
739
+ // name lowers as written, which renders `[object Object]` where a value was due —
740
+ // a wrong page, with nothing about it saying so.
741
+ if (sourceFile === undefined)
742
+ throw new Error(
743
+ `abide: ${source.path} is not inside the project ${this.tsconfig}, so no name in it can be typed (RULEBOOK 31.3). Give the app a tsconfig whose include reaches it.`,
744
+ )
745
+ const read = await readsIn(
746
+ asked.checker,
747
+ abide,
748
+ sourceFile,
749
+ query.map.shadowPath,
750
+ query.spans,
751
+ source.file,
752
+ held[at] as Set<number>,
753
+ )
754
+ answers[at] = read.answer
755
+ suspects[at] = read.suspects
756
+ const placed: Diagnosis[] = []
757
+ for (const warning of read.warnings) {
758
+ const offset = query.map.toSource(
724
759
  query.map.shadowPath,
760
+ warning.at,
761
+ )
762
+ if (
763
+ source.text === undefined ||
764
+ !isIgnored(source.text, offset, warning.message)
725
765
  )
726
- // The shadow has to be INSIDE the opened project or the checker never reads it.
727
- // Refusing loudly rather than answering nothing: no reads is a file whose every
728
- // name lowers as written, which renders `[object Object]` where a value was due —
729
- // a wrong page, with nothing about it saying so.
730
- if (sourceFile === undefined)
731
- throw new Error(
732
- `abide: ${source.path} is not inside the project ${this.tsconfig}, so no name in it can be typed (RULEBOOK 31.3). Give the app a tsconfig whose include reaches it.`,
766
+ placed.push(
767
+ diagnosisAt(
768
+ source.path,
769
+ source.text,
770
+ offset,
771
+ 'warning',
772
+ warning.message,
773
+ ),
733
774
  )
734
- const read = await readsIn(
735
- checker,
736
- abide,
737
- sourceFile,
738
- query.map.shadowPath,
739
- query.spans,
775
+ }
776
+ warned[at] = placed
777
+ }
778
+ const every = queries.map((_, at) => at)
779
+ const first = project
780
+ const firstAbide = await abideIn(first)
781
+ await Promise.all(every.map((at) => answer(first, firstAbide, at)))
782
+ // 31.1 — A BINDING THE PROJECTION TYPED FROM A `Reactive` IS ASKED OF THE PROGRAM IT RUNS
783
+ // AS: each file holding one is lowered with what it has answered so far, and a binding that
784
+ // is a `Reactive` there is one in the projection too. A binding typed from one that just
785
+ // turned out to be a `Reactive` is asked again, until a round turns none. Nothing is asked,
786
+ // and no second snapshot taken, where no file holds one (`bindingsOfReads`).
787
+ for (;;) {
788
+ const asking = every.filter(
789
+ (at) => (suspects[at] as number[]).length > 0,
790
+ )
791
+ if (asking.length === 0) break
792
+ const loweredCreated: string[] = []
793
+ const loweredChanged: string[] = []
794
+ const lowered = asking.map((at) => {
795
+ const source = sources[at] as (typeof sources)[number]
796
+ const query = queries[at] as { map: ShadowMap }
797
+ const shadow = checkedShadow(
740
798
  source.file,
799
+ answers[at] as CheckerAnswer,
741
800
  )
742
- answers[at] = read.answer
743
- for (const warning of read.warnings) {
744
- const offset = query.map.toSource(
745
- query.map.shadowPath,
746
- warning.at,
747
- )
748
- if (
749
- source.text === undefined ||
750
- !isIgnored(source.text, offset, warning.message)
751
- )
752
- warnings.push(
753
- diagnosisAt(
754
- source.path,
755
- source.text,
756
- offset,
757
- 'warning',
758
- warning.message,
759
- ),
760
- )
801
+ this.register(
802
+ query.map.checkedPath,
803
+ shadow.text,
804
+ loweredCreated,
805
+ loweredChanged,
806
+ )
807
+ this.openOutside(
808
+ source.path,
809
+ query.map.checkedPath,
810
+ loweredChanged,
811
+ )
812
+ return {
813
+ at,
814
+ path: query.map.checkedPath,
815
+ origins: shadow.origins,
761
816
  }
762
- }),
763
- )
817
+ })
818
+ project = await this.currentProject(loweredCreated, loweredChanged)
819
+ if (project === undefined) return { answers, warnings }
820
+ const asked = project
821
+ const abide = await abideIn(asked)
822
+ const turned = new Set<number>()
823
+ await Promise.all(
824
+ lowered.map(async ({ at, path, origins }) => {
825
+ const sourceFile = await asked.program.getSourceFile(path)
826
+ if (sourceFile === undefined) return
827
+ for (const start of suspects[at] as number[]) {
828
+ const position = mapForward(origins, start)
829
+ if (position === undefined) continue
830
+ const symbol = await asked.checker.getSymbolAtLocation(
831
+ getTouchingPropertyName(sourceFile, position),
832
+ )
833
+ const type =
834
+ symbol === undefined
835
+ ? undefined
836
+ : await asked.checker.getTypeOfSymbol(symbol)
837
+ if (
838
+ type !== undefined &&
839
+ (await isReactiveType(
840
+ asked.checker,
841
+ abide,
842
+ type,
843
+ false,
844
+ ))
845
+ ) {
846
+ ;(held[at] as Set<number>).add(start)
847
+ turned.add(at)
848
+ }
849
+ }
850
+ }),
851
+ )
852
+ if (turned.size === 0) break
853
+ await Promise.all([...turned].map((at) => answer(asked, abide, at)))
854
+ for (const at of asking) if (!turned.has(at)) suspects[at] = []
855
+ }
856
+ for (const placed of warned) warnings.push(...placed)
764
857
  return { answers, warnings }
765
858
  }
766
859
 
@@ -28,6 +28,7 @@ import {
28
28
  import {
29
29
  type Symbol as CheckedSymbol,
30
30
  type Checker,
31
+ SignatureKind,
31
32
  SymbolFlags,
32
33
  type Type,
33
34
  TypeFlags,
@@ -129,7 +130,9 @@ export async function readsIn(
129
130
  shadowPath: string,
130
131
  spans: ShadowSpans,
131
132
  file: AbideFile,
132
- ): Promise<{ answer: CheckerAnswer; warnings: Finding[] }> {
133
+ /** Where each name the lowered program proved a `Reactive` starts (`bindingsOfReads`). */
134
+ held: ReadonlySet<number>,
135
+ ): Promise<{ answer: CheckerAnswer; warnings: Finding[]; suspects: number[] }> {
133
136
  // A destructuring pattern binds names this does not see, and that is 31.12's case.
134
137
  const names: Node[] = []
135
138
  const spelled = new Set<string>()
@@ -210,6 +213,12 @@ export async function readsIn(
210
213
  ),
211
214
  )
212
215
  await dropValueHolders(checker, names, reactive)
216
+ for (let at = 0; at < names.length; at += 1)
217
+ if (
218
+ reactive[at] === undefined &&
219
+ held.has((names[at] as Node).getStart())
220
+ )
221
+ reactive[at] = symbols[at]
213
222
  // 32.32 — A VALUE WHOSE `reactive(…)` DECLARES `by` keys a `{#for}` over it,
214
223
  // wherever the value is declared.
215
224
  const byValue = await Promise.all(
@@ -296,7 +305,86 @@ export async function readsIn(
296
305
  spans.scripts,
297
306
  ))
298
307
  warnings.push(warning)
299
- return { answer: new CheckerAnswer(reads, statics), warnings }
308
+ const answer = new CheckerAnswer(reads, statics)
309
+ return {
310
+ answer,
311
+ warnings,
312
+ suspects: await bindingsOfReads(
313
+ checker,
314
+ abide,
315
+ names,
316
+ reactive,
317
+ answer,
318
+ ),
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Where each binding starts whose type the projection cannot vouch for, for the lowered program to
324
+ * answer (`TypeSession.classify`).
325
+ *
326
+ * 31.1 — A BINDING TYPED FROM A READ IS TYPED FROM THE `Reactive` IN THE PROJECTION, and the read is
327
+ * of its value. `reactive(route.url.pathname)` asks a `Reactive<URL>` for a `pathname` it does not
328
+ * declare; the argument is the error type, which `reactive`'s overloads resolve to a `Keyed`, and
329
+ * the binding then lowered as no `Reactive` at all — `bookId = 'y'` rebinding the name and nothing
330
+ * waking. Lowered, the argument is `route.url().pathname` and the binding a `Reactive<string>`.
331
+ *
332
+ * ONLY A CALL THAT CAN HAND BACK A `Reactive` IS ASKED: an un-annotated binding, not one already,
333
+ * initialized by a call holding a read, to a callee one of whose signatures returns a `Reactive`.
334
+ * Anything else is a value however its reads are typed, and asking would cost every page that
335
+ * computes from one a second snapshot.
336
+ */
337
+ async function bindingsOfReads(
338
+ checker: Checker,
339
+ abide: Abide,
340
+ names: readonly Node[],
341
+ reactive: readonly (CheckedSymbol | undefined)[],
342
+ answer: CheckerAnswer,
343
+ ): Promise<number[]> {
344
+ const suspects: number[] = []
345
+ const asked: Promise<void>[] = []
346
+ for (let at = 0; at < names.length; at += 1) {
347
+ const name = names[at] as Node
348
+ const declaration = name.parent
349
+ if (
350
+ reactive[at] !== undefined ||
351
+ !isVariableDeclaration(declaration) ||
352
+ declaration.type !== undefined ||
353
+ declaration.initializer === undefined
354
+ )
355
+ continue
356
+ let call: Node = declaration.initializer
357
+ while (TRANSPARENT.has(call.kind))
358
+ call = (call as unknown as { expression: Node }).expression
359
+ if (
360
+ !isCallExpression(call) ||
361
+ !answer.holdsRead({ start: call.getStart(), end: call.end })
362
+ )
363
+ continue
364
+ const callee = call.expression
365
+ asked.push(
366
+ (async () => {
367
+ const type = await checker.getTypeAtLocation(callee)
368
+ if (type === undefined) return
369
+ for (const signature of await checker.getSignaturesOfType(
370
+ type,
371
+ SignatureKind.Call,
372
+ )) {
373
+ const returned =
374
+ await checker.getReturnTypeOfSignature(signature)
375
+ if (
376
+ returned !== undefined &&
377
+ (await isReactiveType(checker, abide, returned, false))
378
+ ) {
379
+ suspects.push(name.getStart())
380
+ return
381
+ }
382
+ }
383
+ })(),
384
+ )
385
+ }
386
+ await Promise.all(asked)
387
+ return suspects
300
388
  }
301
389
 
302
390
  /**
@@ -62,6 +62,8 @@ function filesFor(name: string): Record<string, string> {
62
62
  routes: 'abide routes',
63
63
  // 38.112 — the starter's own test, under the 38.109 preload.
64
64
  test: 'bun test',
65
+ // 38.124 — every `*.bench.ts` under `src`, timed, and failed over its `CEILINGS`.
66
+ bench: 'harness-bench',
65
67
  },
66
68
  // ONE ALIAS FOR THE SOURCE, and two targets for it, because an extensionless
67
69
  // specifier is how an import is written and only bun probes for the extension:
@@ -74,7 +76,8 @@ function filesFor(name: string): Record<string, string> {
74
76
  dependencies: { '@abide/abide': ABIDE_DEPENDENCY },
75
77
  // `Bun`, `bun:sqlite` and the rest of the runtime a server module reaches for are
76
78
  // typed by these, and `abide check` reports every one untyped without them. The
77
- // 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).
78
81
  devDependencies: {
79
82
  '@abide/harness': HARNESS_DEPENDENCY,
80
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. */
@@ -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')
@@ -2219,6 +2219,10 @@ function mirror(cell: Cell, inner: Cell): void {
2219
2219
  // 7.13 through the adoption: the re-settle a rerun of the adopter makes reaches a reload the
2220
2220
  // adopted value owes, rather than finding it unservable and giving up.
2221
2221
  if (inner.owes !== OWES_NOTHING) settleOwed(inner)
2222
+ // 13.7 THROUGH THE ADOPTION TOO: a sweep's mark on a value nothing read is `DIRTY`, and an
2223
+ // adoption is that value's next read. A mirror reached through a rerun finds it resolved
2224
+ // already; the first one `adopt` runs does not, and copied the held value over the mark.
2225
+ if (inner.state !== CLEAN) resolve(inner)
2222
2226
  const adopted = inner.stream
2223
2227
  if (adopted !== null) {
2224
2228
  forwardStream(cell, inner, adopted)