@abide/abide 0.105.1 → 0.107.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 (100) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/bin/abide.ts +4 -1
  3. package/docs/REGISTRY.md +156 -44
  4. package/docs/RULEBOOK.md +392 -88
  5. package/docs/abide.md +7245 -10248
  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 +7 -4
  28. package/src/compiler/compileApp.ts +2 -2
  29. package/src/compiler/declaredExports.ts +27 -6
  30. package/src/compiler/emitClient.ts +306 -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 +21 -14
  53. package/src/server/bridgeRelay.ts +108 -20
  54. package/src/server/claudeApi.ts +19 -11
  55. package/src/server/claudeCode.ts +19 -14
  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/bindElement.ts +4 -0
  95. package/src/ui/bindProperty.ts +70 -22
  96. package/src/ui/index.ts +1 -1
  97. package/src/ui/listenForRoutes.ts +14 -3
  98. package/src/ui/reconcile.ts +23 -28
  99. package/src/ui/regions.ts +9 -2
  100. package/src/ui/rows.ts +108 -28
@@ -30,6 +30,11 @@ type Schema = { [keyword: string]: JsonValue }
30
30
  * class graph instead is wide enough to walk for minutes, and widening it is 19.11's answer.
31
31
  */
32
32
  export type Walk<Seen = Map<number, Schema>> = { seen: Seen; depth: number }
33
+ /**
34
+ * A schema walk, and which side of the wire it describes: what a caller SENDS, or what a handler
35
+ * ANSWERS (19.21).
36
+ */
37
+ type SchemaWalk = Walk & { result: boolean }
33
38
  /** The depth a walk stops at: an object type this deep or deeper is not walked. */
34
39
  export const DEEPEST = 8
35
40
 
@@ -51,7 +56,11 @@ export async function argsSchemaOf(
51
56
  const args =
52
57
  call === undefined ? undefined : await checker.getParameterType(call, 0)
53
58
  if (args === undefined) return undefined
54
- const schema = await schemaOf(checker, args, { seen: new Map(), depth: 0 })
59
+ const schema = await schemaOf(checker, args, {
60
+ seen: new Map(),
61
+ depth: 0,
62
+ result: false,
63
+ })
55
64
  if (schema.type !== 'object') return undefined
56
65
  const required = schema.required as string[] | undefined
57
66
  if (required !== undefined && defaulted.length > 0) {
@@ -62,10 +71,30 @@ export async function argsSchemaOf(
62
71
  return schema
63
72
  }
64
73
 
74
+ /**
75
+ * The JSON Schema of what a handler answers, or `undefined` where it says nothing true of it.
76
+ *
77
+ * 19.21 — `value` is what the handler's `Rpc` reads, refusals already out of it. A value that may be
78
+ * nothing answers 204 rather than JSON, and a value the walk can say nothing of — `unknown`, a
79
+ * `Response`, a stream, a class instance — publishes no schema rather than `{}`.
80
+ */
81
+ export async function resultSchemaOf(
82
+ checker: Checker,
83
+ value: Type | undefined,
84
+ ): Promise<Schema | undefined> {
85
+ if (value === undefined || (await holdsUndefined(value))) return undefined
86
+ const schema = await schemaOf(checker, value, {
87
+ seen: new Map(),
88
+ depth: 0,
89
+ result: true,
90
+ })
91
+ return Object.keys(schema).length === 0 ? undefined : schema
92
+ }
93
+
65
94
  async function schemaOf(
66
95
  checker: Checker,
67
96
  type: Type,
68
- walk: Walk,
97
+ walk: SchemaWalk,
69
98
  ): Promise<Schema> {
70
99
  const flags = type.flags
71
100
  if (flags & TypeFlags.String) return { type: 'string' }
@@ -89,7 +118,11 @@ async function schemaOf(
89
118
  if (walk.depth >= DEEPEST) return {}
90
119
  const pending: Schema = {}
91
120
  walk.seen.set(type.id, pending)
92
- const inner: Walk = { seen: walk.seen, depth: walk.depth + 1 }
121
+ const inner: SchemaWalk = {
122
+ seen: walk.seen,
123
+ depth: walk.depth + 1,
124
+ result: walk.result,
125
+ }
93
126
  // Three questions of one type, asked together rather than one round trip after another.
94
127
  const [array, tuple, calls] = await Promise.all([
95
128
  checker.isArrayType(type),
@@ -113,7 +146,7 @@ async function schemaOf(
113
146
  async function objectOf(
114
147
  checker: Checker,
115
148
  type: Type,
116
- walk: Walk,
149
+ walk: SchemaWalk,
117
150
  ): Promise<Schema> {
118
151
  const properties: Schema = {}
119
152
  const required: string[] = []
@@ -124,6 +157,14 @@ async function objectOf(
124
157
  const property = members[at] as (typeof members)[number]
125
158
  const value = types[at]
126
159
  if (value === undefined) continue
160
+ // 19.22 — AN ANSWER WITH A METHOD IS AN INSTANCE, and what `JSON.stringify` makes of one is
161
+ // its own `toJSON`'s or its fields', neither of which its members say.
162
+ if (
163
+ walk.result &&
164
+ (await checker.getSignaturesOfType(value, SignatureKind.Call))
165
+ .length > 0
166
+ )
167
+ return {}
127
168
  properties[property.name] = await schemaOf(checker, value, walk)
128
169
  // A member JSON can leave out is not required, whether `?` said so or `| undefined` did.
129
170
  if (
@@ -136,8 +177,9 @@ async function objectOf(
136
177
  if (required.length > 0) schema.required = required
137
178
  // 19.18 — A MEMBER THE TYPE DOES NOT DECLARE IS REFUSED, where no index signature declares the
138
179
  // rest: a caller outside the process sends whatever it likes, and a body writing its args to a
139
- // row would write that too. An author who wants the rest declares a schema of their own.
140
- schema.additionalProperties = false
180
+ // row would write that too. An author who wants the rest declares a schema of their own. 19.22 —
181
+ // an answer is wider than its type wherever the value handed back was.
182
+ if (!walk.result) schema.additionalProperties = false
141
183
  for (const index of await checker.getIndexInfosOfType(type))
142
184
  if (index.keyType.flags & TypeFlags.String)
143
185
  schema.additionalProperties = await schemaOf(
@@ -156,7 +198,7 @@ async function objectOf(
156
198
  async function unionOf(
157
199
  checker: Checker,
158
200
  union: UnionOrIntersectionType,
159
- walk: Walk,
201
+ walk: SchemaWalk,
160
202
  ): Promise<Schema> {
161
203
  const all = await union.getTypes()
162
204
  const members: Type[] = []
@@ -182,8 +224,12 @@ async function unionOf(
182
224
  type: 'boolean',
183
225
  const: (member as LiteralType).value as boolean,
184
226
  })
185
- for (const member of members)
186
- schemas.push(await schemaOf(checker, member, walk))
227
+ for (const member of members) {
228
+ const schema = await schemaOf(checker, member, walk)
229
+ // An arm that says nothing leaves the union saying nothing.
230
+ if (Object.keys(schema).length === 0) return schema
231
+ schemas.push(schema)
232
+ }
187
233
  if (schemas.length === 1) return schemas[0] as Schema
188
234
  const kinds: string[] = []
189
235
  for (const schema of schemas) {
@@ -1,5 +1,6 @@
1
1
  import { relative } from 'node:path' // bun exposes no path helpers of its own
2
2
  import { type Placement, placement } from '#server/placement.ts'
3
+ import type { PlacedSource } from './appLayout.ts'
3
4
  import { type AppChecker, compiled } from './compileApp.ts'
4
5
  import { abideImportsOf } from './componentGraph.ts'
5
6
  import { hooksIn } from './hookCalls.ts'
@@ -48,6 +49,7 @@ export async function checkSource(
48
49
  const { session, root } = checker
49
50
  const layout = await checker.layout()
50
51
  // 38.33 — WHAT THE TREE REFUSES OF IT, off the layout the checker keeps until a move is heard.
52
+ // What the parser refuses is the held text's, below, never the disk's (`layout.unparsed`).
51
53
  const tree: Diagnosis[] = []
52
54
  for (const refused of layout.refused)
53
55
  if (refused.path === path) tree.push(refused)
@@ -116,6 +118,37 @@ export async function checkSource(
116
118
  return [...tree, ...own, ...diagnoses]
117
119
  }
118
120
 
121
+ /**
122
+ * Lowers every `.abide` of the app into the checker, quietly, as an editor holds it.
123
+ *
124
+ * 38.116 — A NAME'S REFERENCES ARE THE WHOLE APP'S, and `checkSource` lowers one page and what it
125
+ * imports: a page this one never imports, reading the same export, is in no program until it is
126
+ * lowered. A file the parser refuses is left out, as `checkApp` leaves it.
127
+ */
128
+ export async function lowerApp(
129
+ checker: AppChecker,
130
+ held: (path: string) => string | undefined,
131
+ ): Promise<void> {
132
+ const layout = await checker.layout()
133
+ const refusals = new Refusals(checker.root, true)
134
+ const sources: (PlacedSource & { quiet: true })[] = []
135
+ for (const source of layout.sources) {
136
+ const editing = held(source.path)
137
+ if (editing === undefined || editing === source.text) {
138
+ sources.push({ ...source, quiet: true })
139
+ continue
140
+ }
141
+ const [parsed] = parseSources(
142
+ [{ path: source.path, text: editing, quiet: true }],
143
+ refusals,
144
+ )
145
+ if (parsed !== undefined)
146
+ sources.push({ ...source, ...parsed, quiet: true })
147
+ }
148
+ const { read } = await readSources(checker.session, sources, refusals)
149
+ await checker.session.diagnose(read, layout, false)
150
+ }
151
+
119
152
  /** Whether a `.abide` an editor holds calls `onConfig`, where its text parses. */
120
153
  function filesConfig(text: string): boolean {
121
154
  try {
@@ -43,24 +43,36 @@ export type CheckedShadow = {
43
43
  */
44
44
  export const COMPONENT_STAND_IN = '__component'
45
45
 
46
+ /** 34.16 — the arguments a component is called with: none where no prop is required of it. */
47
+ const COMPONENT_RECORD = '__record'
48
+
46
49
  /**
47
50
  * The component a checked module exports as its default: called with the record its tag passes
48
51
  * (`projectShadow`'s `invocation`), and answering the `Component` a server module's call of it
49
52
  * renders (34.16). What its blocks export by name is beside it.
53
+ *
54
+ * `props` is spelled ONCE, between `head` and `tail`, so the one spelling is the one a tag's keys are
55
+ * contextually typed by, and a caller can copy it from the author's call (38.116).
50
56
  */
51
- function componentModule(props: string, generics?: string): string {
57
+ function componentModule(generics?: string): { head: string; tail: string } {
52
58
  // 34.13 — generic over what the file's `<script>` declares, so a caller's props infer it, the
53
59
  // signature's own parameters shadowing the opaque stand-ins the body was checked against. 34.16 —
54
60
  // a component no prop is required of is called with none, as the runtime's bound face takes it.
55
61
  const parameters = generics === undefined ? '' : `<${generics}>`
56
- return `
57
- declare const ${COMPONENT_STAND_IN}: ${parameters}(...record: {} extends ${props} ? [record?: ${props}] : [record: ${props}]) => import('${SERVER_SPECIFIER}').Component
62
+ return {
63
+ head: `
64
+ type ${COMPONENT_RECORD}<Props> = {} extends Props ? [record?: Props] : [record: Props]
65
+ declare const ${COMPONENT_STAND_IN}: ${parameters}(...record: ${COMPONENT_RECORD}<`,
66
+ tail: `>) => import('${SERVER_SPECIFIER}').Component
58
67
  export default ${COMPONENT_STAND_IN}
59
- `
68
+ `,
69
+ }
60
70
  }
61
71
 
72
+ const STAND_IN = componentModule()
73
+
62
74
  /** A component nothing has lowered takes anything, which is no finding rather than a wrong one. */
63
- export const COMPONENT_MODULE = componentModule('any')
75
+ export const COMPONENT_MODULE = `${STAND_IN.head}any${STAND_IN.tail}`
64
76
 
65
77
  /** The file lowered the way the emitters lower it, for the checker to report on. */
66
78
  export function checkedShadow(
@@ -103,7 +115,25 @@ export function checkedShadow(
103
115
  }
104
116
  origins.push({ at: text.length, source: at, length: source.length - at })
105
117
  text += source.slice(at)
106
- text += componentModule(propsOf(lowering), file.generics)
118
+ // 33.4 — WHAT THE CHILD DECLARES IS WHAT A CALLER IS HELD TO: the type `props<X>()` hands back,
119
+ // read off the call as the author wrote it. 33.3 — `props()` with no argument hands back the
120
+ // untyped record, and takes anything. 38.116 — AND THE COPY IS PLACED where it was copied from,
121
+ // so a rename of a prop asked at a tag's key reaches the declaration it is a copy of.
122
+ const module = componentModule(file.generics)
123
+ text += module.head
124
+ const read = lowering.ownPropsRead()
125
+ if (read === undefined) text += NO_PROPS
126
+ else {
127
+ text += 'ReturnType<typeof '
128
+ const called = source.slice(read.from, read.to).replace(/\(\s*\)$/, '')
129
+ origins.push({
130
+ at: text.length,
131
+ source: read.from,
132
+ length: called.length,
133
+ })
134
+ text += `${called}>`
135
+ }
136
+ text += module.tail
107
137
  // What each name read by name holds when it is read: what calling it hands back, and through a
108
138
  // props accessor, what calling what that hands back does.
109
139
  let values = ''
@@ -122,22 +152,6 @@ export function checkedShadow(
122
152
  }
123
153
  }
124
154
 
125
- /**
126
- * The props type a caller is held to, spelled off the file's `props` call.
127
- *
128
- * 33.4 — WHAT THE CHILD DECLARES IS WHAT A CALLER IS HELD TO: the type `props<X>()` hands back, read
129
- * off the call as the author wrote it, which stands at the top of the module where this is
130
- * declared. 33.3 — `props()` with no argument hands back the untyped record, and takes anything.
131
- */
132
- function propsOf(lowering: Lowering): string {
133
- const read = lowering.ownPropsRead()
134
- return read === undefined
135
- ? NO_PROPS
136
- : `ReturnType<typeof ${lowering.projection.text
137
- .slice(read.from, read.to)
138
- .replace(/\(\s*\)$/, '')}>`
139
- }
140
-
141
155
  /**
142
156
  * 27.13 — WHAT EACH `onConfig` DEFAULTS IS DECLARED INTO `Config`, so `config().RETRIES` is the
143
157
  * `number` its defaults return rather than the index's `unknown`. Each call is bound by the lowering
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Every clause a finding's message cites, in the order it cites them: each number after a
3
+ * `RULEBOOK`, a list (`RULEBOOK 32.14, 32.32.`) included. What `abide-ignore` answers to (14.19)
4
+ * and what the editor links (38.118) — one reading, so the two cannot name different clauses.
5
+ */
6
+ export function citedClauses(message: string): string[] {
7
+ const clauses: string[] = []
8
+ for (const citation of message.matchAll(
9
+ /\bRULEBOOK (\d+\.\d+(?:,\s*\d+\.\d+)*)/g,
10
+ ))
11
+ for (const clause of (citation[1] as string).split(/,\s*/))
12
+ clauses.push(clause)
13
+ return clauses
14
+ }
@@ -103,6 +103,11 @@ export type Read = {
103
103
  keys: string[]
104
104
  /** 23.9 — the read is abide's `route.params`, which a page or a layout types by its route. */
105
105
  routeParams: boolean
106
+ /**
107
+ * 32.67 — a read of a `{#for}` row's item: where that row's item binding starts in the
108
+ * projection, or -1 for every other read.
109
+ */
110
+ item: number
106
111
  }
107
112
 
108
113
  /**
@@ -134,6 +139,7 @@ export function newRead(
134
139
  local: false,
135
140
  keys: [],
136
141
  routeParams: false,
142
+ item: -1,
137
143
  }
138
144
  }
139
145
 
@@ -84,7 +84,8 @@ export type ClassifiedAttribute = {
84
84
  | { kind: 'spread'; explicit: string[] }
85
85
  // 32.5, 32.7, 32.8 — the property and the value, kept in step both ways. `reflects` is the
86
86
  // attribute the property's value stands at in the tag, and `undefined` where it is not an
87
- // attribute: a `<textarea>`'s or `<select>`'s value is the client's to set.
87
+ // attribute: a `<textarea>`'s value is its text and a `<select>`'s is its options' `selected`,
88
+ // which the markup writes inside the tag (32.61).
88
89
  | {
89
90
  kind: 'bound'
90
91
  bind: ValueBind
@@ -463,13 +464,15 @@ export function isParserOwned(lowerTag: string): boolean {
463
464
  /**
464
465
  * The key a component's attribute passes a prop under, or `undefined` where it is no prop.
465
466
  *
466
- * 32.6, 32.10 — a `bind:` handing over a `Reactive` keys the name after it, and any other `:` name
467
- * — `class:`, `style:`, a `bind:` with nothing bound — is an element's own. The build writes the
467
+ * 32.6, 32.10, 32.69 — a `bind:` handing over a `Reactive` keys the name after it, and any other
468
+ * `:` name — `class:`, `style:`, `bind:element`, a `bind:` with nothing bound — is an element's own.
469
+ * Read as a prop, `bind:element` was one named `element` the child never declared, and the
470
+ * callback never ran. The build writes the
468
471
  * props record by this and the checker projects it by this, so a tag is held to what it sends.
469
472
  */
470
473
  export function propKeyOf(attribute: Attribute): string | undefined {
471
474
  if (attribute.lowerName.startsWith('bind:'))
472
- return attribute.expression
475
+ return attribute.expression && !isElementBind(attribute.lowerName)
473
476
  ? attribute.name.slice('bind:'.length)
474
477
  : undefined
475
478
  return attribute.name.includes(':') ? undefined : attribute.name
@@ -206,7 +206,7 @@ export async function compiled(
206
206
  // ONE LAYOUT for the sources, the handler files and the public files, and what the tree refuses
207
207
  // is met before the checker is asked anything.
208
208
  const layout = await (kept?.layout() ?? appLayout(root))
209
- refusals.adopt(layout.refused)
209
+ refusals.adopt([...layout.unparsed, ...layout.refused])
210
210
  const app: CompiledApp = {
211
211
  files: {},
212
212
  tables: {},
@@ -534,7 +534,7 @@ ${tableOf(routes)}
534
534
  ]
535
535
 
536
536
  // The router this document gets, the page it arrived on, and the two places a reader navigates
537
- // from (30.7). Hydrating here rather than leaving it to the app is what makes the page the server
537
+ // from (RULEBOOK 30.7). Hydrating here rather than leaving it to the app is what makes the page the server
538
538
  // painted live without anybody wiring it.
539
539
  export function start(host: Element) {
540
540
  const router = startRouter(routes, host, ${transitions})
@@ -35,8 +35,14 @@ import {
35
35
  import type { Method } from '#shared/rpc.ts'
36
36
  import { aliasedSymbol } from './aliasedSymbol.ts'
37
37
  import { defaultedMembers, parameterPattern } from './argsDefaultsIn.ts'
38
- import { argsSchemaOf, DEEPEST, type Walk } from './argsSchemaOf.ts'
38
+ import {
39
+ argsSchemaOf,
40
+ DEEPEST,
41
+ resultSchemaOf,
42
+ type Walk,
43
+ } from './argsSchemaOf.ts'
39
44
  import { diagnosisAt } from './lineOf.ts'
45
+ import type { Refusals } from './Refusals.ts'
40
46
  import { isReactiveCall, METHODS, optionNamed } from './reactiveOptions.ts'
41
47
  import {
42
48
  abideIn,
@@ -44,7 +50,6 @@ import {
44
50
  isReactiveMember,
45
51
  valueTypeOf,
46
52
  } from './reactiveTypes.ts'
47
- import type { Refusals } from './Refusals.ts'
48
53
  import type { Diagnosis } from './TypeSession.ts'
49
54
 
50
55
  /**
@@ -83,7 +88,7 @@ export async function declaredExports(
83
88
  refusals.at(
84
89
  file,
85
90
  new Error(
86
- `abide: ${path} is not inside the project ${projectName}, so what it exports cannot be read (16.73). Give the app a tsconfig whose include reaches it.`,
91
+ `abide: ${path} is not inside the project ${projectName}, so what it exports cannot be read (RULEBOOK 16.73). Give the app a tsconfig whose include reaches it.`,
87
92
  ),
88
93
  )
89
94
  return undefined
@@ -123,17 +128,21 @@ export async function declaredExports(
123
128
  declared === undefined
124
129
  ? undefined
125
130
  : await checker.getTypeOfSymbol(target)
131
+ const value =
132
+ handlerType === undefined
133
+ ? undefined
134
+ : await valueOfRpc(checker, handlerType)
126
135
  if (handlerType !== undefined && abide.reactive) {
127
136
  const at = await nestedReactiveIn(
128
137
  checker,
129
138
  abide.reactive,
130
- await valueOfRpc(checker, handlerType),
139
+ value,
131
140
  '',
132
141
  { seen: new Set(), depth: 0 },
133
142
  )
134
143
  if (at !== undefined)
135
144
  throw new Error(
136
- `abide: ${path} — \`${exported.name}\` returns a \`Reactive\` at \`${at}\`, which the wire would carry as the function it is rather than the value it holds. Read it there, or return the \`Reactive\` itself to have it adopted (11.12). RULEBOOK 16.39.`,
145
+ `abide: ${path} — \`${exported.name}\` returns a \`Reactive\` at \`${at}\`, which the wire would carry as the function it is rather than the value it holds. Read it there, or return the \`Reactive\` itself to have it adopted (RULEBOOK 11.12). RULEBOOK 16.39.`,
137
146
  )
138
147
  }
139
148
  return {
@@ -153,6 +162,10 @@ export async function declaredExports(
153
162
  handlerType,
154
163
  declared?.defaulted ?? [],
155
164
  ),
165
+ // 19.21 — a stream's chunks are framed one by one, never one JSON value.
166
+ result: declared?.copy?.stream
167
+ ? undefined
168
+ : await resultSchemaOf(checker, value),
156
169
  unread,
157
170
  }
158
171
  } catch (thrown) {
@@ -179,7 +192,15 @@ const METHODS_SOURCE = Bun.fileURLToPath(
179
192
  type Declared = Mount['method']
180
193
  type DeclaredExport = Pick<
181
194
  Mount,
182
- 'name' | 'ui' | 'cli' | 'mcp' | 'copy' | 'defaults' | 'args' | 'description'
195
+ | 'name'
196
+ | 'ui'
197
+ | 'cli'
198
+ | 'mcp'
199
+ | 'copy'
200
+ | 'defaults'
201
+ | 'args'
202
+ | 'result'
203
+ | 'description'
183
204
  > & {
184
205
  method: Declared | undefined
185
206
  /** 18.15 — what its copy's options declare and the build could not read. */