@abide/abide 0.105.1 → 0.106.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/bin/abide.ts +4 -1
  3. package/docs/REGISTRY.md +156 -44
  4. package/docs/RULEBOOK.md +375 -87
  5. package/docs/abide.md +7243 -10256
  6. package/package.json +4 -1
  7. package/scaffold/AGENTS.md +2 -0
  8. package/scaffold/src/getGreeting.ts +6 -0
  9. package/scaffold/src/index.abide +3 -1
  10. package/scaffold/src/tests/getGreeting.test.ts +6 -0
  11. package/scaffold/src/tests/index.test.ts +18 -0
  12. package/src/compiler/DOCS_SITE.ts +2 -0
  13. package/src/compiler/Lowering.ts +234 -14
  14. package/src/compiler/PositionTable.ts +14 -7
  15. package/src/compiler/PositionedRefusal.ts +13 -0
  16. package/src/compiler/Refusals.ts +18 -11
  17. package/src/compiler/SHARED_NAMES.ts +2 -0
  18. package/src/compiler/ShadowMap.ts +37 -0
  19. package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
  20. package/src/compiler/TypeSession.ts +410 -31
  21. package/src/compiler/appLayout.ts +18 -2
  22. package/src/compiler/argsSchemaOf.ts +55 -9
  23. package/src/compiler/checkApp.ts +33 -0
  24. package/src/compiler/checkedShadow.ts +36 -22
  25. package/src/compiler/citedClauses.ts +14 -0
  26. package/src/compiler/classifyRead.ts +6 -0
  27. package/src/compiler/classifyTemplate.ts +2 -1
  28. package/src/compiler/compileApp.ts +2 -2
  29. package/src/compiler/declaredExports.ts +27 -6
  30. package/src/compiler/emitClient.ts +304 -103
  31. package/src/compiler/handlersUnder.ts +4 -0
  32. package/src/compiler/invocationProps.ts +1 -1
  33. package/src/compiler/isIgnored.ts +27 -0
  34. package/src/compiler/parseAbide.ts +176 -76
  35. package/src/compiler/projectShadow.ts +30 -6
  36. package/src/compiler/readSources.ts +1 -1
  37. package/src/compiler/readsIn.ts +100 -1
  38. package/src/compiler/renderMarkup.ts +36 -5
  39. package/src/compiler/scaffold.ts +19 -6
  40. package/src/compiler/serveLanguage.ts +156 -21
  41. package/src/compiler/templateContext.ts +150 -0
  42. package/src/compiler/tokenize.ts +6 -0
  43. package/src/server/BRIDGE_VERSION.ts +1 -1
  44. package/src/server/Inbox.ts +8 -0
  45. package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
  46. package/src/server/MOUNTABLE.ts +6 -0
  47. package/src/server/abideModules.ts +8 -1
  48. package/src/server/answerRequest.ts +3 -1
  49. package/src/server/answerRpc.ts +26 -12
  50. package/src/server/answerSocket.ts +26 -1
  51. package/src/server/argsFor.ts +4 -3
  52. package/src/server/assistant.ts +19 -13
  53. package/src/server/bridgeRelay.ts +108 -20
  54. package/src/server/claudeApi.ts +7 -7
  55. package/src/server/claudeCode.ts +11 -11
  56. package/src/server/commands.ts +73 -9
  57. package/src/server/createApp.ts +41 -7
  58. package/src/server/devWorker.ts +3 -3
  59. package/src/server/generatedSurfaces.ts +37 -4
  60. package/src/server/muxUpgrade.ts +47 -0
  61. package/src/server/preload.ts +13 -2
  62. package/src/server/render.ts +4 -1
  63. package/src/server/runCompiled.ts +1 -0
  64. package/src/server/seedBuffer.ts +31 -1
  65. package/src/server/serveDev.ts +83 -72
  66. package/src/server/surfaceOf.ts +3 -0
  67. package/src/server/testMount.ts +53 -0
  68. package/src/shared/ADDRESSES.ts +1 -1
  69. package/src/shared/ERROR_META.ts +3 -0
  70. package/src/shared/Schema.ts +1 -1
  71. package/src/shared/WORK.ts +14 -12
  72. package/src/shared/applyPatch.ts +1 -1
  73. package/src/shared/boundValue.ts +81 -0
  74. package/src/shared/index.ts +7 -2
  75. package/src/shared/jsonSchema.ts +25 -2
  76. package/src/shared/log.ts +11 -1
  77. package/src/shared/reactive/Keyed.ts +30 -6
  78. package/src/shared/reactive/PATCHES.ts +1 -1
  79. package/src/shared/reactive/Reactive.ts +118 -60
  80. package/src/shared/reactive/canonicalKey.ts +1 -1
  81. package/src/shared/reactive/index.ts +13 -5
  82. package/src/shared/reactive/recordWrites.ts +1 -1
  83. package/src/shared/reactive/watch.ts +26 -4
  84. package/src/shared/rpc.ts +6 -1
  85. package/src/shared/url.ts +7 -3
  86. package/src/shared/validateJson.ts +1 -1
  87. package/src/ui/ArmRegion.ts +2 -2
  88. package/src/ui/List.ts +9 -9
  89. package/src/ui/RawRun.ts +1 -1
  90. package/src/ui/Router.ts +194 -36
  91. package/src/ui/Scope.ts +29 -0
  92. package/src/ui/adoptWalk.ts +3 -3
  93. package/src/ui/applyHead.ts +25 -3
  94. package/src/ui/bindProperty.ts +70 -22
  95. package/src/ui/index.ts +1 -1
  96. package/src/ui/listenForRoutes.ts +14 -3
  97. package/src/ui/reconcile.ts +23 -28
  98. package/src/ui/regions.ts +9 -2
  99. package/src/ui/rows.ts +108 -28
@@ -32,13 +32,18 @@ export function listenForRoutes(router: Router): () => void {
32
32
  // BACK AND FORWARD HAVE ALREADY MOVED THE ADDRESS BAR by the time this runs, so the page is
33
33
  // mounted where the bar already points and nothing is pushed for it. 30.20 — a step within the
34
34
  // standing page, back over an in-page `#anchor` jump, is `go`'s to keep: nothing asked, nothing
35
- // remounted.
35
+ // remounted. 30.29 — the scroll is the router's to put back, once the page it belongs to is
36
+ // mounted: the browser's own restoration runs before that page exists.
36
37
  const onPop = (): void => {
37
38
  if (router.match(location.pathname) === undefined) {
38
39
  location.reload()
39
40
  return
40
41
  }
41
- void router.go(location.href, { replace: true, keepScroll: true })
42
+ void router.go(
43
+ location.href,
44
+ { replace: true, keepScroll: true },
45
+ router.stepped(),
46
+ )
42
47
  }
43
48
 
44
49
  const listening = new AbortController()
@@ -48,7 +53,13 @@ export function listenForRoutes(router: Router): () => void {
48
53
  document.addEventListener('pointerdown', onIntent, options)
49
54
  document.addEventListener('focusin', onIntent, options)
50
55
  addEventListener('popstate', onPop, options)
51
- return () => listening.abort()
56
+ // A reload leaves the document by way of `pagehide`, the last moment the entry can be written.
57
+ addEventListener('pagehide', () => router.remember(), options)
58
+ history.scrollRestoration = 'manual'
59
+ return () => {
60
+ listening.abort()
61
+ history.scrollRestoration = 'auto'
62
+ }
52
63
  }
53
64
 
54
65
  /**
@@ -4,9 +4,9 @@ import type { List } from './List.ts'
4
4
  import { HYDRATE_LOG } from './Mismatch.ts'
5
5
  import { beginLayer, endLayer } from './measureLayer.ts'
6
6
  import { emptyRegion } from './regions.ts'
7
- import { dropRow, makeRow, moveRow, type Row } from './rows.ts'
7
+ import { dropRow, makeRow, moveRow, type Row, updateRow } from './rows.ts'
8
8
 
9
- // 41.8 — a KEYED list, reconciled by moving the nodes that are already there rather than by
9
+ // 41.8, 32.67 — a KEYED list, reconciled by moving the nodes that are already there rather than by
10
10
  // rebuilding them. A rebuild produces the right screen and the wrong document: focus, selection,
11
11
  // scroll position and anything a row was in the middle of go with the nodes that were replaced.
12
12
  //
@@ -19,7 +19,7 @@ import { dropRow, makeRow, moveRow, type Row } from './rows.ts'
19
19
  * Reconciles a keyed list's rows to `data`, moving survivors rather than remaking them.
20
20
  *
21
21
  * A key it has not seen gets a row from the builders `list.setUp` answers for it (14.8). Rows that
22
- * survive are MOVED, never remade.
22
+ * survive are MOVED, never remade, and take the value now under their key (32.67).
23
23
  */
24
24
  export function reconcile(list: List, data: unknown[]): void {
25
25
  if (data.length === 0) {
@@ -98,33 +98,28 @@ export function reconcile(list: List, data: unknown[]): void {
98
98
  for (let at = 0; at < wanted; at += 1) {
99
99
  const key = keys[at] as unknown
100
100
  const existing = held.get(key)
101
- // `Object.is` RATHER THAN `===`, for NaN: `NaN === NaN` is false, so a row whose value is
102
- // NaN never matched itself and was stopped, removed and remade on every reconcile —
103
- // taking its focus, selection, scroll position and any transition mid-flight with it —
104
- // while the text on screen stayed identical, so nothing about the output could show it.
105
- if (
106
- existing !== undefined &&
107
- Object.is(existing.data, data[at]) &&
108
- !patchedSince(existing)
109
- ) {
110
- held.delete(key)
111
- existing.seen = PATCHES.epoch
112
- survivor[at] = existing
113
- from[at] = existing.at
114
- if (existing.at < lastSurvivor) ordered = false
115
- lastSurvivor = existing.at
101
+ if (existing === undefined) {
102
+ survivor[at] = null
103
+ from[at] = -1
116
104
  continue
117
105
  }
118
- // Either a key never seen, or one whose value is a different object or one a patch wrote
119
- // to — each is a row this document does not have, and each is made. A stale one's nodes go NOW: the placement
120
- // walk anchors on nodes that are still attached, and a node about to be replaced is not
121
- // one of them.
122
- if (existing !== undefined) {
123
- held.delete(key)
124
- dropRow(existing)
125
- }
126
- survivor[at] = null
127
- from[at] = -1
106
+ held.delete(key)
107
+ // 32.67 — A KEPT KEY KEEPS ITS ROW, whatever value now stands under it: a different one
108
+ // is written into the row's item and its bindings update in place, so focus, selection
109
+ // and an open `<details>` stay with nodes that were never replaced. 32.33 — so is the
110
+ // same object a patch wrote inside.
111
+ //
112
+ // `Object.is` RATHER THAN `===`, for NaN: `NaN === NaN` is false, so a row whose value is
113
+ // NaN was written on every reconcile and woke every binding it holds for a value that
114
+ // never moved — and nothing about the output could show it.
115
+ const value = data[at]
116
+ if (!Object.is(existing.data, value) || patchedSince(existing))
117
+ updateRow(existing, value)
118
+ existing.seen = PATCHES.epoch
119
+ survivor[at] = existing
120
+ from[at] = existing.at
121
+ if (existing.at < lastSurvivor) ordered = false
122
+ lastSurvivor = existing.at
128
123
  }
129
124
 
130
125
  // THE ROWS THAT NEED NOT MOVE ARE THE LONGEST RUN ALREADY IN ORDER. Every survivor outside it
package/src/ui/regions.ts CHANGED
@@ -54,7 +54,7 @@ export function pageRegion(host: Node): Region {
54
54
  if (close !== null) return { open: node as Comment, close }
55
55
  }
56
56
  throw new Mismatch(
57
- "abide: the host holds no page region, so the client and the server disagree about this document's shape (41.10).",
57
+ "abide: the host holds no page region, so the client and the server disagree about this document's shape. RULEBOOK 41.10.",
58
58
  )
59
59
  }
60
60
 
@@ -95,7 +95,7 @@ export function fillRegion(
95
95
  const parent = region.close.parentNode
96
96
  if (parent === null)
97
97
  throw new Mismatch(
98
- `abide: this region is not in a document, so the client and the server disagree about this region's shape (41.10).`,
98
+ `abide: this region is not in a document, so the client and the server disagree about this region's shape. RULEBOOK 41.10.`,
99
99
  )
100
100
  emptyRegion(region, parent)
101
101
  if (build === undefined) return
@@ -135,6 +135,12 @@ export function fillEmpty(
135
135
  *
136
136
  * NODE BY NODE WHILE ANYTHING IS LEAVING (32.38), because a node holding a leaving element has to
137
137
  * stay where it stands, and only `leave` asks.
138
+ *
139
+ * 32.66 — AND WHAT STAYS IS PUT OUTSIDE THE ANCHORS by moving the OPEN anchor past it, so the
140
+ * region is empty to `isEmptyRegion` and a hydration walk from `open.nextSibling` starts at what
141
+ * the next fill builds. The comment moves rather than the held nodes: it has no box, and moving an
142
+ * element mid-exit would restart a CSS animation on it. Everything left between the anchors after
143
+ * the removal is held, so the move puts all of it before `open`, where it already stood on screen.
138
144
  */
139
145
  export function emptyRegion(region: Region, parent: ParentNode): void {
140
146
  const { open, close } = region
@@ -144,6 +150,7 @@ export function emptyRegion(region: Region, parent: ParentNode): void {
144
150
  parent.lastChild !== close
145
151
  ) {
146
152
  removeUntil(open.nextSibling, close)
153
+ if (open.nextSibling !== close) parent.insertBefore(open, close)
147
154
  return
148
155
  }
149
156
  if (open.nextSibling === close) return
package/src/ui/rows.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { REGION_CLOSE, ROW_OPEN } from '#shared/ANCHORS.ts'
2
+ import { moveItem, rowItem } from '#shared/reactive/index.ts'
2
3
  import { PATCHES } from '#shared/reactive/PATCHES.ts'
3
4
  import { leave } from './leave.ts'
4
5
  import { moveNode } from './moveNode.ts'
@@ -18,13 +19,24 @@ import { EMPTY_SCOPE, type Scope } from './Scope.ts'
18
19
  export type Row = {
19
20
  key: unknown
20
21
  /**
21
- * The value the row was built from.
22
+ * The value the row shows.
22
23
  *
23
- * A row that survives a reorder keeps its NODES, which is the whole point — but if the value
24
- * behind that key is a different object, its content is stale and the row is remade. Identity
25
- * is the signal here, the inverse of the case where a freshly built wrapper defeats one.
24
+ * A KEPT KEY KEEPS ITS ROW (32.67): a different value under it is written here and into the
25
+ * row's bindings rather than remaking the row, so identity decides whether there is anything to
26
+ * write, not whether the row survives.
26
27
  */
27
28
  data: unknown
29
+ /**
30
+ * The row's item as a cell (`rowCell`), or null where nothing the row binds reads it through
31
+ * one yet.
32
+ *
33
+ * MADE ONLY WHERE A BINDING NEEDS IT: a slot that reads the item and nothing else is written by
34
+ * `update`, and a binding at the row's top level reads the record until the row's first new
35
+ * item (`updateRow`), so a row that never takes one allocates nothing for 32.67.
36
+ */
37
+ item: (() => unknown) | null
38
+ /** Rewrites the row's slots that read its item alone (`RowBuilders.update`). */
39
+ update: (row: Row, at: number) => void
28
40
  /**
29
41
  * The row's first node: its element, or its `ROW_OPEN` anchor where the body has several —
30
42
  * the start of a range, not the nodes `make` handed back.
@@ -43,7 +55,7 @@ export type Row = {
43
55
  */
44
56
  at: number
45
57
  /**
46
- * The patch epoch the row was made at (32.33).
58
+ * The patch epoch the row last took its item at (32.33).
47
59
  *
48
60
  * Its item written past this is content the row never saw, under the same object a kept row is
49
61
  * matched by.
@@ -51,19 +63,28 @@ export type Row = {
51
63
  seen: number
52
64
  }
53
65
 
66
+ /**
67
+ * A row's builders, each handed the row's RECORD — whose `data` is the item — so the one record
68
+ * carries the item to all four, and a rebuilt row (41.10) writes its new first node back there.
69
+ */
54
70
  export type RowBuilders = {
55
71
  /**
56
72
  * A row's nodes, BUILT in the document the list stands in — handed over by the reconcile
57
- * rather than found by the row, which is the one place a row's document is decided.
73
+ * rather than found by the row, which is the one place a row's document is decided. Every slot
74
+ * reading the item alone is written here, before the nodes are inserted.
58
75
  */
59
- make: (document: Document, item: unknown, at: number) => Node[]
60
- /**
61
- * Handed the row's RECORD rather than its nodes: a row it rebuilds (41.10) writes its new
62
- * first node back there.
63
- */
64
- mount: (row: Row, item: unknown, at: number) => Scope
76
+ make: (document: Document, row: Row, at: number) => Node[]
77
+ mount: (row: Row, at: number) => Scope
78
+ /** 32.67 — rewrites, from `row.data`, the slots `make` wrote from the item alone. */
79
+ update: (row: Row, at: number) => void
65
80
  }
66
81
 
82
+ /** Answers the builders a row is made and mounted by; called once per row (14.8). */
83
+ export type RowSetUp = (row: Row, at: number) => RowBuilders
84
+
85
+ /** The `update` of a row that has no slot reading its item alone. */
86
+ export function noUpdate(): void {}
87
+
67
88
  /**
68
89
  * The anchor ending the anchored row whose first node is `first`: the next `ROW_OPEN` or
69
90
  * `REGION_CLOSE` at the row's own depth, or null where there is none in this parent.
@@ -167,37 +188,98 @@ export function replaceRow(row: Row, next: Node[]): void {
167
188
 
168
189
  /** A new row, built and inserted before `before`. */
169
190
  export function makeRow(
170
- setUp: (item: unknown, at: number) => RowBuilders,
191
+ setUp: RowSetUp,
171
192
  parent: Node,
172
193
  before: Node,
173
194
  key: unknown,
174
- item: unknown,
195
+ data: unknown,
175
196
  at: number,
176
197
  ): Row {
198
+ // `before` STANDS IN FOR THE ROW'S FIRST NODE until `make` hands it over, so the field holds a
199
+ // node for the record's whole life.
200
+ const row = newRow(key, data, before as ChildNode, at)
177
201
  // 14.8 — set up ONCE for the row, and the same builders make it and mount it.
178
- const builders = setUp(item, at)
179
- const nodes = builders.make(parent.ownerDocument as Document, item, at)
202
+ const builders = setUp(row, at)
203
+ row.update = builders.update
204
+ const nodes = builders.make(parent.ownerDocument as Document, row, at)
180
205
  for (const node of nodes) parent.insertBefore(node, before)
181
- return rowOf(builders, key, item, nodes[0] as ChildNode, at)
206
+ row.node = nodes[0] as ChildNode
207
+ row.scope = builders.mount(row, at)
208
+ return row
182
209
  }
183
210
 
184
211
  /**
185
- * A row record over nodes already in the document, mounted.
212
+ * A row over nodes the server painted, set up and mounted.
186
213
  *
187
- * The one place a row's record is written, so its shape is fixed once for every path that makes one
188
- * — a made row and a hydrated one alike. The mount is handed the RECORD, because a row it rebuilds
189
- * (41.10) has a new first node to write back.
214
+ * 41.8 — the hydration half of `makeRow`: the nodes are already in the document.
190
215
  */
191
- export function rowOf(
192
- builders: RowBuilders,
216
+ export function paintedRow(
217
+ setUp: RowSetUp,
193
218
  key: unknown,
194
- item: unknown,
219
+ data: unknown,
195
220
  node: ChildNode,
196
221
  at: number,
197
222
  ): Row {
198
- const row: Row = {
223
+ const row = newRow(key, data, node, at)
224
+ const builders = setUp(row, at)
225
+ row.update = builders.update
226
+ row.scope = builders.mount(row, at)
227
+ return row
228
+ }
229
+
230
+ /**
231
+ * The row's item as a cell its bindings read, made on the first ask (32.67).
232
+ *
233
+ * Asked by a row whose binding reads the item inside a region of its own, or a branch-local setup.
234
+ */
235
+ export function rowCell(row: Row): () => unknown {
236
+ let item = row.item
237
+ if (item === null) {
238
+ item = rowItem(row.data)
239
+ row.item = item
240
+ }
241
+ return item
242
+ }
243
+
244
+ /**
245
+ * The row's item as a binding at its own top level reads it (`Scope.itemWatch`): off the record
246
+ * until the row has a cell, and through the cell from then on (32.67).
247
+ */
248
+ export function itemOf(row: Row): unknown {
249
+ const item = row.item
250
+ return item === null ? row.data : item()
251
+ }
252
+
253
+ /**
254
+ * Writes a kept key's new value into its row, which keeps its nodes (32.67).
255
+ *
256
+ * 32.33 — the same object a patch wrote inside is written too, and wakes the row's readers as a
257
+ * different one would.
258
+ *
259
+ * THE CELL IS MADE ON THE FIRST NEW ITEM, not when the row is: a row's item watches read the record
260
+ * until then (`itemOf`), so a row that never takes one allocates nothing for it. Made here, they
261
+ * are rerun to read it, which is also what subscribes them to whatever the new item has them read.
262
+ */
263
+ export function updateRow(row: Row, data: unknown): void {
264
+ row.data = data
265
+ if (row.item !== null) moveItem(row.item, data)
266
+ else if (row.scope.items !== 0) {
267
+ row.item = rowItem(data)
268
+ row.scope.rerunItems()
269
+ }
270
+ row.update(row, row.at)
271
+ }
272
+
273
+ /**
274
+ * A row's record, its shape fixed once for every path that makes one — a made row and a hydrated
275
+ * one alike.
276
+ */
277
+ function newRow(key: unknown, data: unknown, node: ChildNode, at: number): Row {
278
+ return {
199
279
  key,
200
- data: item,
280
+ data,
281
+ item: null,
282
+ update: noUpdate,
201
283
  node,
202
284
  // A `Scope` rather than `undefined` until the mount returns, so the field holds one class
203
285
  // for the record's whole life.
@@ -205,8 +287,6 @@ export function rowOf(
205
287
  at,
206
288
  seen: PATCHES.epoch,
207
289
  }
208
- row.scope = builders.mount(row, item, at)
209
- return row
210
290
  }
211
291
 
212
292
  /**