@abide/abide 0.122.0 → 0.123.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,21 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.123.1
4
+
5
+ ### Patch Changes
6
+
7
+ - the args-as-sent walk returns the args it does not copy, walks a cycle once, and keeps a parsed __proto__ a member (RULEBOOK 19.25) ([`ee02cac`](https://github.com/briancray/abide/commit/ee02cacbb64f00374ae98154e788c59c5a4ec49b))
8
+ - bf77c28: `abide check` no longer fails when an inline component is passed to a component that sits inside another component's body: the outer close tag's read in the check's projection now ends with a `;`, so the braces that scope the passed component are not parsed as an arrow function.
9
+ - 7c24094: A no-argument mutation behind `middleware` no longer starts its body before its rungs run. Dropping the previous call's answer built the request's instance of the handler, which started the body ahead of every rung, so a refusing rung (`signedIn`) answered after the body had already run. Calling the same mutation twice in one request now writes twice behind rungs as well (RULEBOOK 16.7, 16.11, 16.16).
10
+ - 94fbea7: A handler's derived args schema reads a member left `undefined` as absent, so an in-process call like `getResearch({ home, topic: undefined })` is answered as the wire answers it rather than refused 422 (RULEBOOK 19.25, 15.8).
11
+
12
+ ## 0.123.0
13
+
14
+ ### Minor Changes
15
+
16
+ - 0e5f83e: an unfinished conversation's log line says why — aborted, failed with the throw, or stopped (RULEBOOK 44.31) ([`042aea3`](https://github.com/briancray/abide/commit/042aea3150ae0fd29959bbe1346d1595b86617bc))
17
+ - 0e5f83e: Breaking: a member the value's type declares is the value's, and a read prop holds a Reactive where one is due (RULEBOOK 31.5, 31.6, 33.27) ([`50b1151`](https://github.com/briancray/abide/commit/50b1151d46389185df0b61d0dd82241566446275))
18
+
3
19
  ## 0.122.0
4
20
 
5
21
  ### Minor Changes
package/docs/REGISTRY.md CHANGED
@@ -168,7 +168,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
168
168
 
169
169
  | Name | Signature | Meaning | Rules |
170
170
  | --- | --- | --- | --- |
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 |
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.13 |
172
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
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 |
@@ -273,7 +273,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
273
273
 
274
274
  | Name | Signature | Meaning | Rules |
275
275
  | --- | --- | --- | --- |
276
- | `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | 19.2, 19.3, 19.4, 19.6, 19.8, 19.9, 19.10, 19.18, 19.11, 19.12 |
276
+ | `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | 19.2, 19.3, 19.4, 19.6, 19.8, 19.9, 19.10, 19.18, 19.25, 19.11, 19.12 |
277
277
  | `JsonValue` | `null \| boolean \| number \| string \| JsonValue[] \| { [k: string]: JsonValue }` | What an `Args` may hold. | — |
278
278
  | `Issues<T>` | `T extends object ? Partial<Record<Paths<T> \| '', string[]>> : string[]` | What was wrong, keyed by where. | 19.4, 19.19 |
279
279
  | `Paths<T>` | `string` | The dot-joined leaf paths of a type. | 19.5 |
@@ -349,7 +349,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
349
349
 
350
350
  | Name | Signature | Meaning | Rules |
351
351
  | --- | --- | --- | --- |
352
- | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | 44.5, 44.13, 44.17, 44.18, 44.19, 44.24, 44.25, 44.27, 44.30, 44.33 |
352
+ | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | 44.5, 44.13, 44.17, 44.18, 44.19, 44.24, 44.25, 44.27, 44.30, 44.33, 44.45, 44.46 |
353
353
  | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. | 44.16, 44.29 |
354
354
  | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the built-in tools it is given. | 44.14, 44.32, 44.33, 44.40, 44.41, 44.42, 44.43, 44.44 |
355
355
  | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | 44.20, 44.22, 44.32, 44.33, 44.34, 44.40 |
@@ -380,11 +380,11 @@ 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, 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 |
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.15 |
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.15 |
385
385
  | `Params` | `Record<string, string \| string[]>` | What a matched route's segments are. | 23.5 |
386
- | `route.name` | `Reactive<string>` | The resolution path, the same under any mount path. | 23.1, 23.6, 23.14, 31.16 |
387
- | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | 23.1, 31.16 |
386
+ | `route.name` | `Reactive<string>` | The resolution path, the same under any mount path. | 23.1, 23.6, 23.14, 31.15 |
387
+ | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | 23.1, 31.15 |
388
388
 
389
389
  ## `online`
390
390
 
@@ -533,12 +533,12 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
533
533
  | `foo` wherever the declared type of its position is not a `Reactive` | a live read | 31.1, 31.2, 31.10 |
534
534
  | `const x: Reactive<…> = foo` / a `Reactive<…>`-typed parameter, return or `bind:` operand | the `Reactive` | 31.3 |
535
535
  | `await foo` | the settling of the `Reactive`, which is its value | 1.4, 2.7, 31.3 |
536
- | `refresh(foo)` / a `Selection`-typed parameter | the `Reactive` | 31.14 |
536
+ | `refresh(foo)` / a `Selection`-typed parameter | the `Reactive` | 31.13 |
537
537
  | `foo = bar` | a write | 31.4, 11.61 |
538
- | `foo.bar = v` | a write through a path | 31.8, 31.15 |
538
+ | `foo.bar = v` | a write through a path | 31.8, 31.14 |
539
539
  | `foo.push(v)` | the same | 31.8, 31.9 |
540
540
  | `const [a, b] = foo` / `...foo` | one read of the value | 31.12 |
541
- | `foo.bar` | the value's `bar`, or the `Reactive`'s own `bar` where it declares one | 31.5, 31.13 |
541
+ | `foo.bar` | the value's `bar`, or the `Reactive`'s own `bar` where it declares one the value does not | 31.5 |
542
542
  | `foo.bar(…)` | the `Reactive` API where `bar` is a member | 31.6 |
543
543
  | `foo()` / `foo.set(v)` | the explicit forms | 31.7, 31.11 |
544
544
 
@@ -596,7 +596,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
596
596
 
597
597
  | Name | Signature | Meaning | Rules |
598
598
  | --- | --- | --- | --- |
599
- | `props` | `<Props>() => { [K in keyof Props]: Props[K] }` | One live accessor per prop the component declares. | 33.1, 33.2, 33.3, 33.4, 33.5, 33.6, 33.7, 33.8, 33.9, 33.10, 33.11, 33.14, 33.15, 33.20, 33.22, 33.25 |
599
+ | `props` | `<Props>() => { [K in keyof Props]: Props[K] }` | One live accessor per prop the component declares. | 33.1, 33.2, 33.3, 33.4, 33.5, 33.6, 33.7, 33.8, 33.9, 33.10, 33.11, 33.14, 33.15, 33.20, 33.22, 33.25, 33.27 |
600
600
  | `Attributes<Tag>` | `Attributes<Tag extends keyof HTMLElementTagNameMap \| keyof SVGElementTagNameMap>` | The attributes an HTML or SVG element takes, typed as a component's props. | 33.23, 33.24, 33.26 |
601
601
  | `children` | always accepted | What a caller passes between the tags. | 33.16, 32.18 |
602
602
 
package/docs/RULEBOOK.md CHANGED
@@ -1335,6 +1335,9 @@ to, and MUST NOT change the type of the value that slot gates.
1335
1335
  19.24 The value a body produces MUST be typed from the body alone, and a `schema` declared beside it
1336
1336
  MUST NOT change that type.
1337
1337
 
1338
+ 19.25 A `Schema` derived per 19.9 MUST read a member left `undefined`, at any depth of a record, as
1339
+ absent — in process as the wire carries it (30.28).
1340
+
1338
1341
  # 20. Generated surfaces
1339
1342
 
1340
1343
  20.1 Every surface `Clients` names MUST be derived from the route table, and MUST NOT be authored
@@ -1871,16 +1874,17 @@ the routes 30.9 ranks after that route.
1871
1874
 
1872
1875
  31.3 A name MUST hold the `Reactive` only where the declared type of its position is a `Reactive`: an
1873
1876
  annotated binding, an annotated return, a `Reactive`-typed parameter, a `bind:` operand, an `await`
1874
- operand — or where 31.14 holds it.
1877
+ operand — or where 31.13 holds it.
1875
1878
 
1876
1879
  31.4 Assignment to such a name MUST be a write, and the assigned value MAY be settled, a load, or a
1877
1880
  `Reactive`.
1878
1881
 
1879
1882
  31.5 A property access on such a name MUST reach the value, except a member declared on the
1880
- `Reactive` type, which MUST reach that member bound to the `Reactive`.
1883
+ `Reactive` type that the value's type does not declare, which MUST reach that member bound to the
1884
+ `Reactive`.
1881
1885
 
1882
- 31.6 A call on such a name MUST reach the `Reactive` API where the member is declared on the
1883
- `Reactive` type, and the value's method otherwise.
1886
+ 31.6 A call on such a name MUST reach the `Reactive` API where 31.5 reaches the `Reactive`'s
1887
+ member, and the value's method otherwise.
1884
1888
 
1885
1889
  31.7 `s` and `s.set` MUST keep compiling inside a `.abide` file.
1886
1890
 
@@ -1896,17 +1900,14 @@ the member name.
1896
1900
 
1897
1901
  31.12 A destructuring pattern or a spread over such a name MUST read it once.
1898
1902
 
1899
- 31.13 The compiler MUST warn where 31.5 reaches a `Reactive` member the value's type also declares,
1900
- naming the explicit read.
1901
-
1902
- 31.14 A name MUST hold the `Reactive` where the declared type of its position is a `Selection`, or a
1903
+ 31.13 A name MUST hold the `Reactive` where the declared type of its position is a `Selection`, or a
1903
1904
  type parameter constrained by one.
1904
1905
 
1905
- 31.15 A write through a member path that reaches a value which is not plain data — an array, or an
1906
+ 31.14 A write through a member path that reaches a value which is not plain data — an array, or an
1906
1907
  object whose prototype is `Object.prototype` or `null` — MUST assign on that value itself, MUST NOT
1907
1908
  copy it or any level above it, and MUST NOT wake a reader.
1908
1909
 
1909
- 31.16 `route.url`, `route.params`, `route.name` and `route.navigating` MUST each be read and written
1910
+ 31.15 `route.url`, `route.params`, `route.name` and `route.navigating` MUST each be read and written
1910
1911
  as a name bound to a `Reactive` is, per 31.1 to 31.7.
1911
1912
 
1912
1913
  # 32. Templating
@@ -2219,6 +2220,10 @@ that element as its `currentTarget`.
2219
2220
  included, under the names SVG spells them, and a tag both HTML and SVG name MUST be typed as the HTML
2220
2221
  element.
2221
2222
 
2223
+ 33.27 A name `props` binds MUST hold a `Reactive` wherever 31.3 or 31.13 has a name hold its
2224
+ `Reactive` or 31.5 has it reach its `Reactive`'s member, and that `Reactive`'s probes MUST answer for
2225
+ every `Reactive` the caller's expression read.
2226
+
2222
2227
  # 34. Script and style blocks
2223
2228
 
2224
2229
  34.1 A `<script>` block MUST be per-instance component setup.
@@ -3355,8 +3360,8 @@ run no turn for 30 seconds.
3355
3360
  44.23 A post to `/__abide/bridge` carrying a line that is not JSON MUST be refused 400, and none of
3356
3361
  its lines MUST be relayed.
3357
3362
 
3358
- 44.24 A bridge MUST answer an MCP request its Claude Code made with a failure where the report
3359
- carrying it was not taken by `/__abide/bridge`, or where no reply arrived within 120 seconds.
3363
+ 44.24 A bridge MUST answer an MCP request its Claude Code made with a failure where
3364
+ `/__abide/bridge` sent no reply within 120 seconds.
3360
3365
 
3361
3366
  44.25 A bridge whose stream from `/__abide/bridge` ends unasked MUST redial it with its pairing's
3362
3367
  token, each wait twice the last, until it is refused or 60 seconds have passed.
@@ -3379,7 +3384,9 @@ before it exits.
3379
3384
 
3380
3385
  44.31 A conversation MUST write one `debug` line on `abide:assistant` when its events end, naming
3381
3386
  its stop reason, the transcript's length, the tool calls it made, its usage and how long it took,
3382
- and naming it `unfinished` where it ended without a `done`.
3387
+ and naming it `unfinished` where it ended without a `done`, followed by why — `aborted` where the
3388
+ request's signal aborted, else `failed` where its provider threw, with the throw's message ending the
3389
+ line, else `stopped` where its caller stopped reading.
3383
3390
 
3384
3391
  44.32 A command `claudeCode.bridgeCommand` or `claudeCode.bridge` mints with `tools` MUST grant
3385
3392
  those built-in tools after its `--` as `--tools`, and one minted without `tools` MUST carry no `--`.
@@ -3427,6 +3434,12 @@ install wrote.
3427
3434
  NAME=VALUE` it is given, and a bridge opening an `abide-bridge` link MUST run Claude Code with those
3428
3435
  options ahead of the link's built-in tools and with those variables set, and a link MUST NOT set either.
3429
3436
 
3437
+ 44.45 A bridge MUST say that `/__abide/bridge` did not take a report once, and not again until a
3438
+ report is taken, and MUST NOT say it while it redials per 44.25.
3439
+
3440
+ 44.46 A bridge MUST end a turn once `/__abide/bridge` did not take a report carrying it, and MUST
3441
+ report that turn failed.
3442
+
3430
3443
  # 45. Offline
3431
3444
 
3432
3445
  45.1 Where `ABIDE_OFFLINE` is on, a page `abide build` built MUST register the worker
package/docs/abide.md CHANGED
@@ -625,7 +625,7 @@ test('typing writes the value, and the value fills the field', async () => {
625
625
  | `const [a, b] = foo` / `...foo` | one read, taken apart |
626
626
  | `foo = bar` | a write |
627
627
  | `foo.bar` | the value's `bar` |
628
- | `foo.bar(…)` | the value's method, or a member of the `Reactive` such as `busy` |
628
+ | `foo.bar(…)` | the value's method where it has one, and otherwise a member of the `Reactive` such as `busy` |
629
629
  | `foo()` / `foo.set(v)` | the explicit read and write |
630
630
  | `await foo` | the value, once it has settled |
631
631
  | `refresh(foo)` / a `Selection`-typed parameter | the `Reactive` itself |
@@ -7482,7 +7482,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
7482
7482
 
7483
7483
  | Name | Signature | Meaning | Rules |
7484
7484
  | --- | --- | --- | --- |
7485
- | `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](/rulebook#13.1), [13.8](/rulebook#13.8), [13.9](/rulebook#13.9), [13.13](/rulebook#13.13), [31.14](/rulebook#31.14) |
7485
+ | `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](/rulebook#13.1), [13.8](/rulebook#13.8), [13.9](/rulebook#13.9), [13.13](/rulebook#13.13), [31.13](/rulebook#31.13) |
7486
7486
 
7487
7487
  ## Taught in
7488
7488
 
@@ -8067,7 +8067,7 @@ Transports: what is written in place rather than imported.
8067
8067
 
8068
8068
  | Name | Signature | Meaning | Rules |
8069
8069
  | --- | --- | --- | --- |
8070
- | `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | [19.2](/rulebook#19.2), [19.3](/rulebook#19.3), [19.4](/rulebook#19.4), [19.6](/rulebook#19.6), [19.8](/rulebook#19.8), [19.9](/rulebook#19.9), [19.10](/rulebook#19.10), [19.18](/rulebook#19.18), [19.11](/rulebook#19.11), [19.12](/rulebook#19.12) |
8070
+ | `Schema<T>` | `((value: unknown) => T) \| StandardSchemaV1<T>` | The two forms a shape may be declared in. | [19.2](/rulebook#19.2), [19.3](/rulebook#19.3), [19.4](/rulebook#19.4), [19.6](/rulebook#19.6), [19.8](/rulebook#19.8), [19.9](/rulebook#19.9), [19.10](/rulebook#19.10), [19.18](/rulebook#19.18), [19.25](/rulebook#19.25), [19.11](/rulebook#19.11), [19.12](/rulebook#19.12) |
8071
8071
  | `JsonSchema` | `JsonValue` | A JSON Schema document, the native form. | [19.7](/rulebook#19.7), [19.14](/rulebook#19.14) |
8072
8072
 
8073
8073
  ## Taught in
@@ -8418,7 +8418,7 @@ Generated surfaces: what is written in place rather than imported.
8418
8418
 
8419
8419
  | Name | Signature | Meaning | Rules |
8420
8420
  | --- | --- | --- | --- |
8421
- | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | [44.5](/rulebook#44.5), [44.13](/rulebook#44.13), [44.17](/rulebook#44.17), [44.18](/rulebook#44.18), [44.19](/rulebook#44.19), [44.24](/rulebook#44.24), [44.25](/rulebook#44.25), [44.27](/rulebook#44.27), [44.30](/rulebook#44.30), [44.33](/rulebook#44.33) |
8421
+ | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | [44.5](/rulebook#44.5), [44.13](/rulebook#44.13), [44.17](/rulebook#44.17), [44.18](/rulebook#44.18), [44.19](/rulebook#44.19), [44.24](/rulebook#44.24), [44.25](/rulebook#44.25), [44.27](/rulebook#44.27), [44.30](/rulebook#44.30), [44.33](/rulebook#44.33), [44.45](/rulebook#44.45), [44.46](/rulebook#44.46) |
8422
8422
  | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the built-in tools it is given. | [44.14](/rulebook#44.14), [44.32](/rulebook#44.32), [44.33](/rulebook#44.33), [44.40](/rulebook#44.40), [44.41](/rulebook#44.41), [44.42](/rulebook#44.42), [44.43](/rulebook#44.43), [44.44](/rulebook#44.44) |
8423
8423
  | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | [44.20](/rulebook#44.20), [44.22](/rulebook#44.22), [44.32](/rulebook#44.32), [44.33](/rulebook#44.33), [44.34](/rulebook#44.34), [44.40](/rulebook#44.40) |
8424
8424
  | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | [36.7](/rulebook#36.7), [44.14](/rulebook#44.14), [44.15](/rulebook#44.15), [44.19](/rulebook#44.19), [44.23](/rulebook#44.23), [44.26](/rulebook#44.26), [16.57](/rulebook#16.57), [16.78](/rulebook#16.78) |
@@ -8529,10 +8529,10 @@ Generated surfaces: what is written in place rather than imported.
8529
8529
 
8530
8530
  | Name | Signature | Meaning | Rules |
8531
8531
  | --- | --- | --- | --- |
8532
- | `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) |
8533
- | `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) |
8534
- | `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) |
8535
- | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | [23.1](/rulebook#23.1), [31.16](/rulebook#31.16) |
8532
+ | `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.15](/rulebook#31.15) |
8533
+ | `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.15](/rulebook#31.15) |
8534
+ | `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.15](/rulebook#31.15) |
8535
+ | `route.navigating` | `Reactive<boolean>` | Whether a navigation is in flight. | [23.1](/rulebook#23.1), [31.15](/rulebook#31.15) |
8536
8536
 
8537
8537
  ## Taught in
8538
8538
 
@@ -9215,12 +9215,12 @@ Generated surfaces: what is written in place rather than imported.
9215
9215
  | `foo` wherever the declared type of its position is not a `Reactive` | a live read | [31.1](/rulebook#31.1), [31.2](/rulebook#31.2), [31.10](/rulebook#31.10) |
9216
9216
  | `const x: Reactive<…> = foo` / a `Reactive<…>`-typed parameter, return or `bind:` operand | the `Reactive` | [31.3](/rulebook#31.3) |
9217
9217
  | `await foo` | the settling of the `Reactive`, which is its value | [1.4](/rulebook#1.4), [2.7](/rulebook#2.7), [31.3](/rulebook#31.3) |
9218
- | `refresh(foo)` / a `Selection`-typed parameter | the `Reactive` | [31.14](/rulebook#31.14) |
9218
+ | `refresh(foo)` / a `Selection`-typed parameter | the `Reactive` | [31.13](/rulebook#31.13) |
9219
9219
  | `foo = bar` | a write | [31.4](/rulebook#31.4), [11.61](/rulebook#11.61) |
9220
- | `foo.bar = v` | a write through a path | [31.8](/rulebook#31.8), [31.15](/rulebook#31.15) |
9220
+ | `foo.bar = v` | a write through a path | [31.8](/rulebook#31.8), [31.14](/rulebook#31.14) |
9221
9221
  | `foo.push(v)` | the same | [31.8](/rulebook#31.8), [31.9](/rulebook#31.9) |
9222
9222
  | `const [a, b] = foo` / `...foo` | one read of the value | [31.12](/rulebook#31.12) |
9223
- | `foo.bar` | the value's `bar`, or the `Reactive`'s own `bar` where it declares one | [31.5](/rulebook#31.5), [31.13](/rulebook#31.13) |
9223
+ | `foo.bar` | the value's `bar`, or the `Reactive`'s own `bar` where it declares one the value does not | [31.5](/rulebook#31.5) |
9224
9224
  | `foo.bar(…)` | the `Reactive` API where `bar` is a member | [31.6](/rulebook#31.6) |
9225
9225
  | `foo()` / `foo.set(v)` | the explicit forms | [31.7](/rulebook#31.7), [31.11](/rulebook#31.11) |
9226
9226
 
@@ -9330,7 +9330,7 @@ Generated surfaces: what is written in place rather than imported.
9330
9330
 
9331
9331
  | Name | Signature | Meaning | Rules |
9332
9332
  | --- | --- | --- | --- |
9333
- | `props` | `<Props>() => { [K in keyof Props]: Props[K] }` | One live accessor per prop the component declares. | [33.1](/rulebook#33.1), [33.2](/rulebook#33.2), [33.3](/rulebook#33.3), [33.4](/rulebook#33.4), [33.5](/rulebook#33.5), [33.6](/rulebook#33.6), [33.7](/rulebook#33.7), [33.8](/rulebook#33.8), [33.9](/rulebook#33.9), [33.10](/rulebook#33.10), [33.11](/rulebook#33.11), [33.14](/rulebook#33.14), [33.15](/rulebook#33.15), [33.20](/rulebook#33.20), [33.22](/rulebook#33.22), [33.25](/rulebook#33.25) |
9333
+ | `props` | `<Props>() => { [K in keyof Props]: Props[K] }` | One live accessor per prop the component declares. | [33.1](/rulebook#33.1), [33.2](/rulebook#33.2), [33.3](/rulebook#33.3), [33.4](/rulebook#33.4), [33.5](/rulebook#33.5), [33.6](/rulebook#33.6), [33.7](/rulebook#33.7), [33.8](/rulebook#33.8), [33.9](/rulebook#33.9), [33.10](/rulebook#33.10), [33.11](/rulebook#33.11), [33.14](/rulebook#33.14), [33.15](/rulebook#33.15), [33.20](/rulebook#33.20), [33.22](/rulebook#33.22), [33.25](/rulebook#33.25), [33.27](/rulebook#33.27) |
9334
9334
  | `children` | always accepted | What a caller passes between the tags. | [33.16](/rulebook#33.16), [32.18](/rulebook#32.18) |
9335
9335
 
9336
9336
  ## Taught in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.122.0",
3
+ "version": "0.123.1",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -686,7 +686,8 @@ export class Lowering {
686
686
  /**
687
687
  * The read's name as emitted: `name()` for a written prop, whose cell its accessor hands back.
688
688
  *
689
- * 33.6 — a written prop's cell is reached through its accessor.
689
+ * 33.6 — a written prop's cell is reached through its accessor. 33.27 — a read prop where a
690
+ * `Reactive` is due, a cell over its accessor.
690
691
  */
691
692
  private nameOf(read: Read): string {
692
693
  const spelled = this.projection.text.slice(read.start, read.end)
@@ -694,6 +695,7 @@ export class Lowering {
694
695
  // reads the one `route` every page shares.
695
696
  if (this.routed !== undefined && read.routeParams)
696
697
  return `(${spelled} as unknown as () => __RouteParams<${JSON.stringify(this.routed.route)}, ${JSON.stringify(this.routed.kind)}>)`
698
+ if (read.held) return `${this.use('heldProp')}(${spelled})`
697
699
  return read.through ? `${spelled}()` : spelled
698
700
  }
699
701
 
@@ -734,7 +736,7 @@ export class Lowering {
734
736
  const name = this.nameOf(read)
735
737
  switch (read.shape) {
736
738
  case 'asWritten':
737
- return read.through ? name : undefined
739
+ return read.through || read.held ? name : undefined
738
740
  // 31.5 — the member BOUND to the `Reactive`, arguments and all.
739
741
  case 'member':
740
742
  return this.checking
@@ -8,9 +8,8 @@
8
8
  * `Reactive` itself, which reaches that. Without the list every `n.foo` reads as the API and the
9
9
  * value's own members are unreachable, which is what `regions.join is not a function` was.
10
10
  *
11
- * 31.9 wants the same question answered from the TYPE rather than from the name, which is a
12
- * checker query this does not make yet. A value whose own type declares `set` or `peek` therefore
13
- * resolves to the `Reactive`'s, and 31.13 asks for a warning naming exactly that case.
11
+ * The list names the candidates; the value's TYPE decides (`declaresMember` in `readsIn`): a value
12
+ * whose own type declares `set` or `error` keeps its own, so a `Map`'s `set` is the map's.
14
13
  *
15
14
  * WHICH IS WHY A NAME THE `Reactive` DOES NOT DECLARE MUST NOT BE HERE: it takes a member the
16
15
  * value really has and resolves it to nothing. This list is exactly `MEMBERS` in
@@ -29,6 +29,8 @@ export const SHARED_NAMES: ReadonlySet<string> = new Set([
29
29
  // 32.6, 33.6, 33.7 — a component's `bind:`, and the cell a child writing that prop builds.
30
30
  'boundProp',
31
31
  'propCell',
32
+ // 33.27 — a read prop where its position holds the `Reactive`.
33
+ 'heldProp',
32
34
  'propCells',
33
35
  // 33.20 — a default answering where the caller's accessor reads `undefined`.
34
36
  'propOr',
@@ -625,7 +625,7 @@ export class TypeSession {
625
625
  * shadow is asked is `readsIn`'s.
626
626
  *
627
627
  * WHAT IT HAS TO SAY THAT IS NOT A TYPE ERROR comes back beside the reads: the compiler's own
628
- * warnings (14.14 to 14.17, 31.13, 32.14), each on the `.abide` line where the source's text
628
+ * warnings (14.14 to 14.17, 32.14), each on the `.abide` line where the source's text
629
629
  * was handed in.
630
630
  */
631
631
  async readsFor(
@@ -52,8 +52,7 @@ export type Read = {
52
52
  * that brackets a binding element's initializer, which together make a default an accessor;
53
53
  * `absent`, which gives a binding with no default an accessor of `undefined` (33.4); and
54
54
  * `restMember`, a member of the rest element, which calls that key's accessor (33.15), beside
55
- * `restWhole`, the rest element handed on as one value (33.22). The
56
- * pair and `absent` are INSERTIONS — zero-width, so what stands between the pair goes on being
55
+ * `restWhole`, the rest element handed on as one value (33.22). The pair and `absent` are INSERTIONS — zero-width, so what stands between the pair goes on being
57
56
  * lowered by the same walk, and a default reading a `Reactive` still reads it.
58
57
  */
59
58
  shape:
@@ -96,6 +95,12 @@ export type Read = {
96
95
  * pair, that the default is one too.
97
96
  */
98
97
  through: boolean
98
+ /**
99
+ * 33.27 — the name is a prop the child only reads, standing where a `Reactive` is due.
100
+ *
101
+ * So the `Reactive` every rewrite reaches is a cell over its accessor, `heldProp(name)`.
102
+ */
103
+ held: boolean
99
104
  /** 32.32 — the name is bound to a `reactive(…)` whose options declare `by`. */
100
105
  byValue: boolean
101
106
  /** 14.8 — the name is declared by a branch-local `<script>`, inside the body it scopes. */
@@ -136,6 +141,7 @@ export function newRead(
136
141
  path: [],
137
142
  value: undefined,
138
143
  through: false,
144
+ held: false,
139
145
  byValue: false,
140
146
  local: false,
141
147
  keys: [],
@@ -235,7 +241,10 @@ export function classifyPropRead(node: Node): Read | undefined {
235
241
  * `undefined` for an occurrence that is not a read at all: the declaration itself, an import or
236
242
  * export specifier, and a `typeof n` in a type.
237
243
  */
238
- export function classifyRead(node: Node): Read | undefined {
244
+ export function classifyRead(
245
+ node: Node,
246
+ valueMember = false,
247
+ ): Read | undefined {
239
248
  const parent = node.parent
240
249
  if (isNotARead(parent)) return undefined
241
250
  if (
@@ -278,7 +287,12 @@ export function classifyRead(node: Node): Read | undefined {
278
287
  if (isPropertyAccessExpression(up) && up.expression === top) {
279
288
  // 31.5 — the `Reactive`'s own member is the API, and no path. Called, it stays as
280
289
  // written; REFERENCED, it is handed over bound, so `promise.then(n.set)` writes `n`.
281
- if (top === node && REACTIVE_MEMBERS.has(up.name.text)) {
290
+ // `valueMember` where the value's own type declares one of that name (31.5).
291
+ if (
292
+ top === node &&
293
+ !valueMember &&
294
+ REACTIVE_MEMBERS.has(up.name.text)
295
+ ) {
282
296
  const outer = up.parent
283
297
  if (
284
298
  (isCallExpression(outer) && outer.expression === up) ||
@@ -391,11 +391,13 @@ function project(file: AbideFile): Projection {
391
391
  }
392
392
  text += '}))\n'
393
393
  // 38.116 — a close tag of its own is a second read of the name, so it is renamed with it.
394
+ // Ended by its `;`: the braces a passed component is scoped in (32.51) can follow, and
395
+ // `(Name)\n{` parses as an arrow function's head.
394
396
  const closed = file.origins.closes.get(node)
395
397
  if (closed !== undefined) {
396
398
  text += ';('
397
399
  copied(node.tag, closed)
398
- text += ')\n'
400
+ text += ');\n'
399
401
  }
400
402
  for (const attribute of refused)
401
403
  slots.set(
@@ -697,6 +699,7 @@ declare function ${BOUND}<Value>(value: ${REACTIVE}<Value, any, any, any>): Valu
697
699
  declare function ${BOUND}<Value>(value: { get(): Value; set(value: Value): unknown }): Value
698
700
  declare function __restValues<Accessors>(record: Accessors): { [Key in keyof Accessors]: NonNullable<Accessors[Key]> extends () => infer Value ? Value : never }
699
701
  declare function __propCell<Value>(read: () => Value): () => ${REACTIVE}<Value>
702
+ declare function __heldProp<Value>(read: () => Value): ${REACTIVE}<Value, Value, any, any>
700
703
  declare function __raw(value: unknown): unknown
701
704
  declare function __writePath<Root>(root: Root, path: readonly PropertyKey[], value: unknown): Root
702
705
  declare type __RouteParams<Pattern extends string, Kind extends string> = Kind extends 'layout' ? ${ROUTE_PARAMS}<Pattern> & { [name: string]: string | string[] | undefined } : ${ROUTE_PARAMS}<Pattern>
@@ -111,7 +111,7 @@ export type Abide = {
111
111
  route: string | undefined
112
112
  reactive: CheckedSymbol | undefined
113
113
  /**
114
- * 31.14 — abide's `Selection` alias, which a position may be typed as rather than as a `Reactive`.
114
+ * 31.13 — abide's `Selection` alias, which a position may be typed as rather than as a `Reactive`.
115
115
  */
116
116
  selection: CheckedSymbol | undefined
117
117
  props: string | undefined
@@ -185,7 +185,7 @@ export async function isReactiveType(
185
185
  /**
186
186
  * Whether a type is abide's `Selection`, or a type parameter constrained by one.
187
187
  *
188
- * 31.14 — A POSITION TYPED AS A `Selection`, or as a type parameter constrained by one (the free
188
+ * 31.13 — A POSITION TYPED AS A `Selection`, or as a type parameter constrained by one (the free
189
189
  * `refresh`, `invalidate`, `busy` and `ready` take `Chosen extends Selection`). A value is never a
190
190
  * selection of itself, so a read there hands `refresh` the invoice rather than the thing to reload.
191
191
  */
@@ -246,27 +246,22 @@ export async function readsIn(
246
246
  )
247
247
  }),
248
248
  )
249
+ const owned = await ownMembers(checker, nodes.flat(), (node) =>
250
+ valueTypeOf(checker, node),
251
+ )
249
252
  const holding: Promise<void>[] = []
250
253
  const located: { node: Node; read: Read }[] = []
251
254
  for (let index = 0; index < nodes.length; index += 1)
252
255
  for (const node of nodes[index] as (Node | undefined)[]) {
253
256
  if (node === undefined) continue
254
- const read = classifyRead(node)
257
+ const read = classifyRead(node, owned.has(node))
255
258
  if (read === undefined) continue
256
259
  read.byValue = byValue[index] as boolean
257
260
  read.local = index >= topLevel
258
261
  reads.push(read)
259
262
  located.push({ node, read })
260
263
  if (read.shape === 'value')
261
- holding.push(
262
- declaredType(checker, node).then(async (type) => {
263
- if (
264
- type !== undefined &&
265
- (await isReactiveType(checker, abide, type, true))
266
- )
267
- read.shape = 'asWritten'
268
- }),
269
- )
264
+ holding.push(holdIn(checker, abide, node, read))
270
265
  }
271
266
  await Promise.all(holding)
272
267
  await props
@@ -392,7 +387,7 @@ async function bindingsOfReads(
392
387
  * The tracking and type warnings a file's reactive reads earn.
393
388
  *
394
389
  * The syntax alone decides 14.14 to 14.16 (`trackingWarnings`);
395
- * 14.17 and 31.13 ask what a read hands back, which is the return of the `Reactive`'s call.
390
+ * 14.17 asks what a read hands back, which is the return of the `Reactive`'s call.
396
391
  */
397
392
  async function warningsFor(
398
393
  checker: Checker,
@@ -424,45 +419,16 @@ async function warningsFor(
424
419
  isBinaryExpression(parent) &&
425
420
  parent.left === node &&
426
421
  parent.operatorToken.kind === SyntaxKind.QuestionQuestionToken
427
- const member =
428
- isPropertyAccessExpression(parent) &&
429
- parent.expression === node &&
430
- isIdentifier(parent.name) &&
431
- REACTIVE_MEMBERS.has(parent.name.text)
432
- ? parent.name.text
433
- : undefined
434
- if (!nullish && member === undefined) continue
422
+ if (!nullish) continue
435
423
  typed.push(
436
424
  valueTypeOf(checker, node).then(async (value) => {
437
- if (value === undefined) return
438
425
  // 14.17 — the fallback of a read that is never nullish is never taken: an unlanded
439
426
  // read is a hole the position fills, not an `undefined` (2.2).
440
- if (nullish && !(await admitsNullish(value)))
427
+ if (value !== undefined && !(await admitsNullish(value)))
441
428
  warnings.push({
442
429
  at: read.start,
443
430
  message: `abide: \`${name} ?? …\` never takes its fallback — a read of \`${name}\` is not undefined while it loads, and its type says it is not undefined after. Ask \`${name}.ready()\` to tell the two apart. RULEBOOK 14.17.`,
444
431
  })
445
- if (member === undefined) return
446
- // Per member, because a union's properties are only those every member shares, and
447
- // `undefined` shares none — so `HTMLVideoElement | undefined` would declare no `error`.
448
- for (const held of await unionMembers(value)) {
449
- if ((held.flags & NULLISH) !== 0) continue
450
- for (const property of await checker.getPropertiesOfType(
451
- held,
452
- ))
453
- if (property.name === member) {
454
- const dot = (
455
- parent as { questionDotToken?: unknown }
456
- ).questionDotToken
457
- ? '?.'
458
- : '.'
459
- warnings.push({
460
- at: read.start,
461
- message: `abide: \`${name}${dot}${member}\` reaches the Reactive's own \`${member}\`, and the value it holds has one too. Write \`${name}()${dot}${member}\` for the value's. RULEBOOK 31.13.`,
462
- })
463
- return
464
- }
465
- }
466
432
  }),
467
433
  )
468
434
  }
@@ -779,7 +745,7 @@ function valueLeaves(node: Node, leaves: Node[]): void {
779
745
  const ROUTE_MEMBERS = new Set(['url', 'params', 'name', 'navigating'])
780
746
 
781
747
  /**
782
- * 31.16 — `route`'s four values, each read by name as a name bound to a `Reactive` is.
748
+ * 31.15 — `route`'s four values, each read by name as a name bound to a `Reactive` is.
783
749
  *
784
750
  * `route.params.id` reads `route.params().id`, and the explicit call keeps compiling (31.7). Told
785
751
  * by the symbol, as `props` is, so an app's own `route` is not abide's.
@@ -1142,13 +1108,18 @@ async function propsReads(
1142
1108
  ),
1143
1109
  ),
1144
1110
  )
1111
+ const owned = await ownMembers(checker, found.flat(), (node) =>
1112
+ checker.getTypeAtLocation(node),
1113
+ )
1114
+ // 33.27 — each prop asked whether its position holds the `Reactive`, all at once.
1115
+ const holding: Promise<void>[] = []
1145
1116
  for (let at = 0; at < found.length; at += 1) {
1146
1117
  const binding = bindings[at] as (typeof bindings)[number]
1147
1118
  if (binding.rest) {
1148
1119
  restReads(found[at] as (Node | undefined)[], reads)
1149
1120
  continue
1150
1121
  }
1151
- const uses: Read[] = []
1122
+ const uses: { node: Node; read: Read }[] = []
1152
1123
  let written = false
1153
1124
  for (const node of found[at] as (Node | undefined)[]) {
1154
1125
  // The binding itself is the pattern's, and not a use of the name.
@@ -1157,15 +1128,16 @@ async function propsReads(
1157
1128
  node.parent?.kind === SyntaxKind.BindingElement
1158
1129
  )
1159
1130
  continue
1160
- const read = classifyRead(node)
1131
+ const read = classifyRead(node, owned.has(node))
1161
1132
  if (read === undefined) continue
1162
1133
  if (
1163
1134
  read.shape === 'write' ||
1164
1135
  read.shape === 'update' ||
1165
- isBindOperand(positions, node)
1136
+ isBindOperand(positions, node) ||
1137
+ writesThrough(node, owned)
1166
1138
  )
1167
1139
  written = true
1168
- uses.push(read)
1140
+ uses.push({ node, read })
1169
1141
  }
1170
1142
  if (!written) {
1171
1143
  for (const node of found[at] as (Node | undefined)[]) {
@@ -1173,18 +1145,141 @@ async function propsReads(
1173
1145
  const read = classifyPropRead(node)
1174
1146
  if (read === undefined) continue
1175
1147
  read.local = binding.local
1176
- reads.push(read)
1148
+ if (read.shape !== 'value') {
1149
+ reads.push(read)
1150
+ continue
1151
+ }
1152
+ holding.push(
1153
+ holdsProp(checker, abide, node, owned.has(node)).then(
1154
+ (held) => {
1155
+ if (!held) return void reads.push(read)
1156
+ // Classified as a page's own name is, reaching a cell over the accessor.
1157
+ const asHeld = classifyRead(node) as Read
1158
+ if (asHeld.shape === 'value')
1159
+ asHeld.shape = 'asWritten'
1160
+ asHeld.held = true
1161
+ asHeld.local = binding.local
1162
+ reads.push(asHeld)
1163
+ },
1164
+ ),
1165
+ )
1177
1166
  }
1178
1167
  continue
1179
1168
  }
1180
1169
  binding.call.path.push({ name: binding.key, start: 0, end: 0 })
1181
1170
  for (const read of binding.defaults) read.through = true
1182
- for (const read of uses) {
1171
+ // A written prop's cell is already a `Reactive`, and a position due one holds it.
1172
+ for (const { node, read } of uses) {
1183
1173
  read.through = true
1184
1174
  read.local = binding.local
1185
1175
  reads.push(read)
1176
+ if (read.shape === 'value')
1177
+ holding.push(holdIn(checker, abide, node, read))
1186
1178
  }
1187
1179
  }
1180
+ await Promise.all(holding)
1181
+ }
1182
+
1183
+ /**
1184
+ * 31.5 — the `Reactive`'s member a property access reaches by name, or `undefined` where the name
1185
+ * is not one — `n.ready`, `n.set`. Whether it is the `Reactive`'s is the value's type's to say
1186
+ * (`ownMembers`).
1187
+ */
1188
+ function memberNamed(node: Node): string | undefined {
1189
+ const parent = node.parent
1190
+ return isPropertyAccessExpression(parent) &&
1191
+ parent.expression === node &&
1192
+ isIdentifier(parent.name) &&
1193
+ REACTIVE_MEMBERS.has(parent.name.text)
1194
+ ? parent.name.text
1195
+ : undefined
1196
+ }
1197
+
1198
+ /**
1199
+ * 31.5 — the occurrences among `nodes` reaching a member the VALUE's own type declares, where the
1200
+ * access is the value's rather than the `Reactive`'s — a `Map`'s `set` is the map's. Asked before
1201
+ * any is classified, so each is classified once.
1202
+ *
1203
+ * Per union member, because a union's properties are only those every member shares, and
1204
+ * `undefined` shares none — so `HTMLVideoElement | undefined` would declare no `error`. A value
1205
+ * typed `any` or `unknown` declares none: its type says nothing of what it holds.
1206
+ */
1207
+ async function ownMembers(
1208
+ checker: Checker,
1209
+ nodes: readonly (Node | undefined)[],
1210
+ valueOf: (node: Node) => Promise<Type | undefined>,
1211
+ ): Promise<Set<Node>> {
1212
+ const owned = new Set<Node>()
1213
+ const asked: Promise<void>[] = []
1214
+ for (const node of nodes) {
1215
+ if (node === undefined) continue
1216
+ const member = memberNamed(node)
1217
+ if (member === undefined) continue
1218
+ asked.push(
1219
+ valueOf(node).then(async (value) => {
1220
+ if (value === undefined) return
1221
+ for (const held of await unionMembers(value))
1222
+ if (
1223
+ (held.flags & NULLISH) === 0 &&
1224
+ (await checker.getPropertyOfType(held, member)) !==
1225
+ undefined
1226
+ )
1227
+ return void owned.add(node)
1228
+ }),
1229
+ )
1230
+ }
1231
+ await Promise.all(asked)
1232
+ return owned
1233
+ }
1234
+
1235
+ /**
1236
+ * 31.7, 33.6, 33.7 — `n.set(v)` and `n.patch(…)` are writes by name, called or handed on, where the
1237
+ * value's own type declares no member of that name — a `Map`'s `set` writes the map, not the prop.
1238
+ */
1239
+ function writesThrough(node: Node, owned: ReadonlySet<Node>): boolean {
1240
+ const member = memberNamed(node)
1241
+ return (member === 'set' || member === 'patch') && !owned.has(node)
1242
+ }
1243
+
1244
+ /**
1245
+ * 33.27 — whether a prop the child only reads stands where its `Reactive` is due, as a page's own
1246
+ * name would: an `await` operand (31.3), a `Reactive`'s member its value does not declare (31.5),
1247
+ * or a position typed as a `Reactive` or a `Selection` (31.3, 31.13).
1248
+ */
1249
+ async function holdsProp(
1250
+ checker: Checker,
1251
+ abide: Abide,
1252
+ node: Node,
1253
+ ownsMember: boolean,
1254
+ ): Promise<boolean> {
1255
+ if (node.parent.kind === SyntaxKind.AwaitExpression) return true
1256
+ if (memberNamed(node) !== undefined) return !ownsMember
1257
+ return typedReactive(checker, abide, node)
1258
+ }
1259
+
1260
+ /**
1261
+ * 31.3, 31.13 — `read` holds the `Reactive` where `node` stands in a position whose declared type
1262
+ * is a `Reactive` or a `Selection`.
1263
+ */
1264
+ async function holdIn(
1265
+ checker: Checker,
1266
+ abide: Abide,
1267
+ node: Node,
1268
+ read: Read,
1269
+ ): Promise<void> {
1270
+ if (await typedReactive(checker, abide, node)) read.shape = 'asWritten'
1271
+ }
1272
+
1273
+ /** 31.3, 31.13 — whether `node`'s position is declared a `Reactive` or a `Selection`. */
1274
+ async function typedReactive(
1275
+ checker: Checker,
1276
+ abide: Abide,
1277
+ node: Node,
1278
+ ): Promise<boolean> {
1279
+ const type = await declaredType(checker, node)
1280
+ return (
1281
+ type !== undefined && (await isReactiveType(checker, abide, type, true))
1282
+ )
1188
1283
  }
1189
1284
 
1190
1285
  /**
@@ -1,3 +1,3 @@
1
1
  // The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
2
2
  // server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
3
- export const BRIDGE_VERSION = '0.5.0'
3
+ export const BRIDGE_VERSION = '0.5.1'
@@ -122,37 +122,58 @@ export async function* assistant(
122
122
  options.mcp === true ? appSurface() : NO_SURFACE,
123
123
  askerOf(request()),
124
124
  )
125
+ const signal = request().signal
125
126
  const events = provider.run(turns, tools, {
126
127
  maxTurns: options.maxTurns ?? MAX_TURNS,
127
- signal: request().signal,
128
+ signal,
128
129
  })
129
130
  // 28.7 — a closed namespace runs the provider's stream untouched.
130
- yield* ASSISTANT_LOG.enabled() ? traced(events, messages.length) : events
131
+ yield* ASSISTANT_LOG.enabled()
132
+ ? traced(events, messages.length, signal)
133
+ : events
131
134
  }
132
135
 
133
- /** `events`, and one line on `abide:assistant` once they end, however they end. 44.31. */
136
+ /**
137
+ * `events`, and one line on `abide:assistant` once they end, however they end. 44.31.
138
+ *
139
+ * AN UNFINISHED CONVERSATION SAYS WHY, an abort read ahead of a throw: an aborted provider may throw
140
+ * as it unwinds, and the abort is what ended it.
141
+ */
134
142
  async function* traced(
135
143
  events: AsyncGenerator<AssistantEvent>,
136
144
  messageCount: number,
145
+ signal: AbortSignal,
137
146
  ): AsyncGenerator<AssistantEvent> {
138
147
  const started = performance.now()
139
148
  let calls = 0
140
149
  let done: Extract<AssistantEvent, { type: 'done' }> | undefined
150
+ let failure: string | undefined
141
151
  try {
142
152
  for await (const event of events) {
143
153
  if (event.type === 'tool') calls += 1
144
154
  else if (event.type === 'done') done = event
145
155
  yield event
146
156
  }
157
+ } catch (thrown) {
158
+ failure = thrown instanceof Error ? thrown.message : String(thrown)
159
+ throw thrown
147
160
  } finally {
161
+ const ending =
162
+ done?.stop ??
163
+ (signal.aborted
164
+ ? 'unfinished aborted'
165
+ : failure === undefined
166
+ ? 'unfinished stopped'
167
+ : 'unfinished failed')
148
168
  ASSISTANT_LOG.debug(
149
- done === undefined ? 'unfinished' : done.stop,
169
+ ending,
150
170
  `${messageCount} messages`,
151
171
  `${calls} tool calls`,
152
172
  done === undefined
153
173
  ? 'no usage'
154
174
  : `${done.usage.input}/${done.usage.output} tokens`,
155
175
  `${(performance.now() - started).toFixed(1)}ms`,
176
+ ...(ending === 'unfinished failed' ? [failure] : []),
156
177
  )
157
178
  }
158
179
  }
@@ -15,12 +15,81 @@ import { argsDocumentOf } from './coerceArgs.ts'
15
15
  * called, whether or not an app mounted it. Not where its `args` gate publishes a document of its
16
16
  * own, which holds the args itself (19.7). Args that did not come are held as an empty record: a
17
17
  * schema is only derived where the handler's args are a required record, so a required member
18
- * missing is refused, not read off nothing.
18
+ * missing is refused, not read off nothing. The args are held as SENT (`asSent`): a call in process
19
+ * passing a member `undefined` is the call the wire carries without it.
19
20
  */
20
21
  export function derivedArgs(
21
22
  handler: HeldToAnnotation & Gated<unknown>,
22
23
  schema: JsonValue,
23
24
  ): void {
24
25
  if (argsDocumentOf(handler) !== undefined) return
25
- handler[DERIVED_ARGS] = (args) => validateJson(schema, args ?? {})
26
+ handler[DERIVED_ARGS] = (args) => validateJson(schema, asSent(args ?? {}))
27
+ }
28
+
29
+ /** The records `asSent` is inside of, so a cyclic value is walked once rather than forever. */
30
+ const WALKING: object[] = []
31
+
32
+ /**
33
+ * `value` as the wire carries it, which drops a member left `undefined` (19.25): a URL omits it
34
+ * (30.28) and JSON leaves it out, at any depth of a body. `value` itself wherever it holds none, so
35
+ * the walk allocates nothing for the call that writes no `undefined` member.
36
+ *
37
+ * AN `undefined` IN A LIST IS KEPT. The two encodings part there — JSON writes `null`, a URL the
38
+ * text `undefined` — so neither is the wire's, and the schema answers the value as given.
39
+ */
40
+ export function asSent(value: unknown): unknown {
41
+ if (value === null || typeof value !== 'object' || WALKING.includes(value))
42
+ return value
43
+ WALKING.push(value)
44
+ try {
45
+ return Array.isArray(value)
46
+ ? listAsSent(value)
47
+ : recordAsSent(value as Record<string, unknown>)
48
+ } finally {
49
+ WALKING.pop()
50
+ }
51
+ }
52
+
53
+ function listAsSent(list: unknown[]): unknown[] {
54
+ let sent: unknown[] | null = null
55
+ for (let index = 0; index < list.length; index += 1) {
56
+ const member = list[index]
57
+ const held = asSent(member)
58
+ if (sent === null && held !== member) sent = list.slice(0, index)
59
+ sent?.push(held)
60
+ }
61
+ return sent ?? list
62
+ }
63
+
64
+ function recordAsSent(
65
+ record: Record<string, unknown>,
66
+ ): Record<string, unknown> {
67
+ let sent: Record<string, unknown> | null = null
68
+ for (const key in record) {
69
+ if (!Object.hasOwn(record, key)) continue
70
+ const member = record[key]
71
+ const held = member === undefined ? undefined : asSent(member)
72
+ if (sent === null && (member === undefined || held !== member))
73
+ sent = membersBefore(record, key)
74
+ if (sent !== null && held !== undefined) sent[key] = held
75
+ }
76
+ return sent ?? record
77
+ }
78
+
79
+ /**
80
+ * A copy of `record`'s own members ahead of `key`, in their order.
81
+ *
82
+ * NO PROTOTYPE, so an own `__proto__` member JSON parsed stays a member the schema reads rather
83
+ * than becoming the copy's prototype.
84
+ */
85
+ function membersBefore(
86
+ record: Record<string, unknown>,
87
+ key: string,
88
+ ): Record<string, unknown> {
89
+ const copy: Record<string, unknown> = Object.create(null)
90
+ for (const earlier in record) {
91
+ if (earlier === key) break
92
+ if (Object.hasOwn(record, earlier)) copy[earlier] = record[earlier]
93
+ }
94
+ return copy
26
95
  }
@@ -54,8 +54,9 @@ export { online } from './online.ts'
54
54
  export { outbox, type QueuedCall } from './outbox.ts'
55
55
  export { onMenu } from './onMenu.ts'
56
56
  export { type Principal, principal } from './principal.ts'
57
- // EMITTED CODE'S: a prop the child writes (33.6, 33.7).
57
+ // EMITTED CODE'S: a prop the child writes (33.6, 33.7), and one it hands where a `Reactive` is due (33.27).
58
58
  export {
59
+ heldProp,
59
60
  propCell,
60
61
  propCells,
61
62
  propOr,
@@ -21,6 +21,24 @@ export function propCell(read: Accessor): () => Reactive<unknown> {
21
21
  return () => cell
22
22
  }
23
23
 
24
+ // Each read prop's `heldProp`, built on the first ask and kept for as long as its accessor is.
25
+ const HELD = new WeakMap<Accessor, Reactive<unknown>>()
26
+
27
+ /**
28
+ * 33.27 — a prop the child only reads, where its position holds the `Reactive` (31.3, 31.13): the
29
+ * one cell `propCell` would have built, over the caller's accessor. So `busy(rows)` asks what the
30
+ * caller's expression read — the caller's own `Reactive` where it bound one — and a plain value's is
31
+ * a cell nothing loads. Built only where one is asked for, and once per accessor.
32
+ */
33
+ export function heldProp(read: Accessor): Reactive<unknown> {
34
+ let cell = HELD.get(read)
35
+ if (cell === undefined) {
36
+ cell = propCell(read)()
37
+ HELD.set(read, cell)
38
+ }
39
+ return cell
40
+ }
41
+
24
42
  /**
25
43
  * 33.20 — a defaulted prop's accessor: the caller's, answering the default wherever it reads
26
44
  * `undefined`, and the default alone where the caller passed nothing. A bound accessor asked to
package/src/shared/rpc.ts CHANGED
@@ -407,6 +407,12 @@ export function declare(method: Method): Declare {
407
407
  rungs.length === 0
408
408
  ? null
409
409
  : compose(rungs, async (ctx: RpcContext) => {
410
+ // 16.7 behind rungs — the handler's spent answer is dropped HERE, once the
411
+ // rungs let the call through: reading an unkeyed value's cell builds this
412
+ // request's instance, which starts its body (1.7), and dropped in `call` that
413
+ // ran the body ahead of every rung (16.11, 16.16).
414
+ if (cacheOptions !== undefined && !keyed)
415
+ dropAnswered(value)
410
416
  const reached = reach(argsOf(ctx))
411
417
  const started = performance.now()
412
418
  try {
@@ -464,8 +470,9 @@ export function declare(method: Method): Declare {
464
470
  }
465
471
  // 16.7 — a mutation's call is a write each time, even where the last one settled this
466
472
  // turn: a keyed value's reach evicts an outlived entry itself, and an unkeyed one's
467
- // spent answer is dropped here.
468
- if (cacheOptions !== undefined && !keyed) dropAnswered(value)
473
+ // spent answer is dropped here — or, behind rungs, in the onion's center.
474
+ if (cacheOptions !== undefined && !keyed && wrapped === null)
475
+ dropAnswered(value)
469
476
  const reached = callLoad(
470
477
  reachThrough,
471
478
  args,
@@ -14,7 +14,7 @@ import { putOwn } from './putOwn.ts'
14
14
  * object where an array was, and `length`, `map` and every later `{#for}` over it go with it. The
15
15
  * shape is decided here, where the value is.
16
16
  *
17
- * 31.15 — ONLY PLAIN DATA IS COPIED. An element, a class instance, a `Map`: anything whose prototype
17
+ * 31.14 — ONLY PLAIN DATA IS COPIED. An element, a class instance, a `Map`: anything whose prototype
18
18
  * is not `Object.prototype`, `null` or `Array.prototype` owns its own state, and its setters say
19
19
  * what a write means — `video.currentTime = t` seeks. A spread of one is a plain object holding a
20
20
  * few own fields, so the write reached nothing and the `Reactive` came to hold the husk. The write