@abide/abide 0.123.2 → 0.124.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 (150) hide show
  1. package/ABIDE.md +8 -5
  2. package/CHANGELOG.md +58 -0
  3. package/bin/abide.ts +36 -65
  4. package/docs/REGISTRY.md +45 -12
  5. package/docs/RULEBOOK.md +24 -14
  6. package/docs/abide.md +999 -459
  7. package/package.json +1 -1
  8. package/src/compiler/ARRAY_MUTATORS.ts +20 -0
  9. package/src/compiler/Diagnostics.ts +198 -0
  10. package/src/compiler/KeptChecker.ts +97 -0
  11. package/src/compiler/LoweredFile.ts +609 -0
  12. package/src/compiler/Lowering.ts +18 -1
  13. package/src/compiler/PositionedRefusal.ts +1 -1
  14. package/src/compiler/SHARED_NAMES.ts +2 -0
  15. package/src/compiler/ShadowHost.ts +670 -0
  16. package/src/compiler/ShadowMap.ts +1 -1
  17. package/src/compiler/TypeSession.ts +76 -1743
  18. package/src/compiler/appLayout.ts +28 -20
  19. package/src/compiler/appLints.ts +109 -0
  20. package/src/compiler/argsDefaultsIn.ts +1 -1
  21. package/src/compiler/checkApp.ts +64 -68
  22. package/src/compiler/checkedShadow.ts +2 -2
  23. package/src/compiler/classifyRead.ts +16 -1
  24. package/src/compiler/compileApp.ts +102 -71
  25. package/src/compiler/declaredExports.ts +46 -21
  26. package/src/compiler/diagnoseSources.ts +232 -0
  27. package/src/compiler/emitClient.ts +1 -1
  28. package/src/compiler/formatAbide.ts +7 -8
  29. package/src/compiler/handlersUnder.ts +32 -63
  30. package/src/compiler/lineOf.ts +0 -19
  31. package/src/compiler/matchersOf.ts +3 -3
  32. package/src/compiler/mountsOf.ts +3 -3
  33. package/src/compiler/reactiveReads.ts +235 -0
  34. package/src/compiler/reactiveTypes.ts +13 -0
  35. package/src/compiler/readSources.ts +16 -18
  36. package/src/compiler/readsIn.ts +181 -82
  37. package/src/compiler/routesDeclaration.ts +46 -0
  38. package/src/compiler/scaffold.ts +7 -2
  39. package/src/compiler/serveLanguage.ts +46 -74
  40. package/src/compiler/sharedModuleWrites.ts +1 -1
  41. package/src/compiler/splitImports.ts +2 -2
  42. package/src/compiler/templateWarnings.ts +1 -2
  43. package/src/compiler/trackingWarnings.ts +1 -1
  44. package/src/server/CLIENT_BUILD.ts +4 -32
  45. package/src/server/DesktopSession.ts +213 -0
  46. package/src/server/Followers.ts +399 -0
  47. package/src/server/MODULE_QUERIES.ts +6 -2
  48. package/src/server/PageRender.ts +201 -0
  49. package/src/server/RESUME_MILLISECONDS.ts +2 -0
  50. package/src/server/abideModules.ts +7 -7
  51. package/src/server/answerFailure.ts +3 -2
  52. package/src/server/answerRequest.ts +16 -369
  53. package/src/server/answerRpc.ts +48 -302
  54. package/src/server/answerSocket.ts +41 -490
  55. package/src/server/appBuild.ts +377 -0
  56. package/src/server/appKit.ts +1 -1
  57. package/src/server/appPlugins.ts +15 -17
  58. package/src/server/appTree.ts +6 -5
  59. package/src/server/argsContract.ts +209 -0
  60. package/src/server/bridgeRelay.ts +153 -65
  61. package/src/server/claudeCode.ts +30 -127
  62. package/src/server/config.ts +6 -7
  63. package/src/server/createApp.ts +38 -66
  64. package/src/server/csp.ts +1 -1
  65. package/src/server/deferredBuilds.ts +3 -16
  66. package/src/server/devWorker.ts +11 -15
  67. package/src/server/faceOf.ts +1 -1
  68. package/src/server/generatedSurfaces.ts +2 -1
  69. package/src/server/handlerFileMount.ts +40 -0
  70. package/src/server/index.ts +3 -3
  71. package/src/server/installServer.ts +5 -9
  72. package/src/server/invokeHandler.ts +230 -0
  73. package/src/server/mountHandlers.ts +184 -0
  74. package/src/server/openDesktopWindow.ts +308 -0
  75. package/src/server/packageDirectory.ts +14 -0
  76. package/src/server/preload.ts +21 -2
  77. package/src/server/principal.ts +5 -4
  78. package/src/server/remote.ts +2 -2
  79. package/src/server/render.ts +31 -59
  80. package/src/server/requestScope.ts +5 -36
  81. package/src/server/runCompiled.ts +1 -1
  82. package/src/server/runDesktop.ts +20 -448
  83. package/src/server/seedBuffer.ts +12 -12
  84. package/src/server/serveDev.ts +24 -58
  85. package/src/server/servedApp.ts +390 -0
  86. package/src/server/shell.ts +1 -1
  87. package/src/server/signInChanged.ts +20 -0
  88. package/src/server/surfaceOf.ts +3 -2
  89. package/src/server/versionOf.ts +7 -16
  90. package/src/shared/RPC_SERVER.ts +27 -31
  91. package/src/shared/Schema.ts +1 -2
  92. package/src/shared/applyPatch.ts +1 -1
  93. package/src/shared/index.ts +1 -1
  94. package/src/shared/{note.ts → issueReport.ts} +22 -0
  95. package/src/shared/loadCall.ts +66 -0
  96. package/src/shared/muxFrames.ts +1 -1
  97. package/src/shared/reactive/Keyed.ts +24 -16
  98. package/src/shared/reactive/Reactive.ts +37 -991
  99. package/src/shared/reactive/argsContract.ts +131 -4
  100. package/src/shared/reactive/byOf.ts +2 -5
  101. package/src/shared/reactive/callLoad.ts +8 -14
  102. package/src/shared/reactive/cellShape.ts +734 -0
  103. package/src/shared/reactive/index.ts +12 -23
  104. package/src/shared/reactive/pairReactive.ts +5 -3
  105. package/src/shared/reactive/patchLog.ts +79 -0
  106. package/src/shared/reactive/patchMarks.ts +34 -0
  107. package/src/shared/reactive/producer.ts +121 -0
  108. package/src/shared/reactive/recordWrites.ts +11 -14
  109. package/src/shared/reactive/scoped.ts +3 -2
  110. package/src/shared/reactive/selections.ts +209 -0
  111. package/src/shared/reactive/share.ts +4 -3
  112. package/src/shared/reactive/watch.ts +3 -4
  113. package/src/shared/rpc.ts +20 -24
  114. package/src/shared/validateJson.ts +1 -2
  115. package/src/shared/writePath.ts +29 -0
  116. package/src/ui/ArmRegion.ts +10 -4
  117. package/src/ui/Level.ts +11 -20
  118. package/src/ui/LevelChain.ts +272 -0
  119. package/src/ui/Router.ts +106 -313
  120. package/src/ui/Scope.ts +17 -3
  121. package/src/ui/TryRegion.ts +22 -23
  122. package/src/ui/bindSpread.ts +2 -4
  123. package/src/ui/listenForRoutes.ts +3 -10
  124. package/src/ui/outboxQueue.ts +23 -11
  125. package/src/ui/outcomeOf.ts +120 -0
  126. package/src/ui/reconcile.ts +4 -6
  127. package/src/ui/routing.ts +2 -1
  128. package/src/ui/rows.ts +2 -2
  129. package/src/ui/setAttribute.ts +12 -3
  130. package/src/ui/socketWire.ts +28 -13
  131. package/src/ui/wire.ts +9 -10
  132. package/src/compiler/Finding.ts +0 -2
  133. package/src/compiler/Refusals.ts +0 -65
  134. package/src/compiler/formatDiagnosis.ts +0 -20
  135. package/src/server/appDirectories.ts +0 -9
  136. package/src/server/argsFor.ts +0 -39
  137. package/src/server/buildDirectory.ts +0 -10
  138. package/src/server/coerceArgs.ts +0 -109
  139. package/src/server/derivedArgs.ts +0 -26
  140. package/src/server/runBuild.ts +0 -24
  141. package/src/server/serveBuild.ts +0 -144
  142. package/src/server/testBuild.ts +0 -220
  143. package/src/shared/LOAD_OFFLINE.ts +0 -15
  144. package/src/shared/LOAD_SIGNAL.ts +0 -10
  145. package/src/shared/issuesOf.ts +0 -18
  146. package/src/shared/reactive/ENTRY_LIVES.ts +0 -7
  147. package/src/shared/reactive/FOLLOWED.ts +0 -8
  148. package/src/shared/reactive/PATCHES.ts +0 -11
  149. package/src/shared/reactive/PATCH_SINKS.ts +0 -26
  150. package/src/shared/reactive/canonicalKey.ts +0 -95
@@ -9,8 +9,9 @@
9
9
  // each module exports and which call declares each (16.73) — which is also what makes an export
10
10
  // that is not a handler a refusal here rather than an address that silently never answers.
11
11
 
12
- import type { Mount } from '#server/answerRpc.ts'
13
12
  import { frameworkRouteRefusal } from '#server/appTree.ts'
13
+ import { handlerFileMount } from '#server/handlerFileMount.ts'
14
+ import type { Mount } from '#server/mountHandlers.ts'
14
15
  import { PRIVATE_EXPORT } from '#server/PRIVATE_EXPORT.ts'
15
16
  import { surfaceName } from '#server/surfaceOf.ts'
16
17
  import { PRIVATE_HANDLERS_ADDRESS } from '#shared/ADDRESSES.ts'
@@ -18,8 +19,8 @@ import { parseRoutePattern } from '#shared/routePattern.ts'
18
19
 
19
20
  import type { PlacedHandler, PlacedSource } from './appLayout.ts'
20
21
  import { declaredExports } from './declaredExports.ts'
21
- import type { Refusals } from './Refusals.ts'
22
- import type { Diagnosis, TypeSession } from './TypeSession.ts'
22
+ import type { Diagnostics } from './Diagnostics.ts'
23
+ import type { TypeSession } from './TypeSession.ts'
23
24
 
24
25
  /** A handler file, by its path and its path below the app's root. */
25
26
  export type HandlerFile = {
@@ -36,7 +37,7 @@ type Source = Pick<
36
37
  >
37
38
 
38
39
  /**
39
- * The app's handler mounts, with the warnings the build raised reading them.
40
+ * The app's handler mounts, the warnings the build raised reading them kept in `diagnostics`.
40
41
  *
41
42
  * Off one read of the checker. 18.15 — with what the build could not read of a copy's options.
42
43
  * 19.1's one owner per route and method is `compileApp`'s, which holds the pages as well.
@@ -48,46 +49,27 @@ export async function handlersUnder(
48
49
  PlacedHandler,
49
50
  'path' | 'relative' | 'route' | 'scope' | 'matchers'
50
51
  >[],
51
- refusals: Refusals,
52
- ): Promise<{ mounts: Mount[]; warnings: Diagnosis[] }> {
52
+ diagnostics: Diagnostics,
53
+ ): Promise<Mount[]> {
53
54
  const serving: Source[] = []
54
55
  for (const source of sources)
55
56
  if (source.file.servers.length > 0) serving.push(source)
56
- const paths: string[] = []
57
- const named: string[] = []
58
- for (const file of files) {
59
- paths.push(file.path)
60
- named.push(file.relative)
61
- }
62
- // The project is asked of below, so the session keeps its snapshot until then.
63
- using _held = session.hold()
64
- const project = await session.projectWith(paths)
65
- for (const source of serving) {
66
- paths.push(session.projectionOf(source.path))
67
- named.push(source.path)
68
- }
57
+ const host = session.host
58
+ const modules: { path: string; file: string; onDisk: boolean }[] = []
59
+ for (const file of files)
60
+ modules.push({ path: file.path, file: file.relative, onDisk: true })
61
+ for (const source of serving)
62
+ modules.push({
63
+ path: host.projectionOf(source.path),
64
+ file: source.path,
65
+ onDisk: false,
66
+ })
69
67
  // A module whose read was refused is `undefined`, refused at its file already.
70
- const declared =
71
- project === undefined || paths.length === 0
72
- ? paths.map(() => [])
73
- : await declaredExports(
74
- project,
75
- session.tsconfig,
76
- paths,
77
- named,
78
- refusals,
79
- )
68
+ const declared = await declaredExports(host, modules, diagnostics)
80
69
  const mounts: Mount[] = []
81
- const warnings: Diagnosis[] = []
82
70
  const names = new Map<string, string>()
83
- for (let at = 0; at < files.length; at += 1) {
84
- const {
85
- relative,
86
- route: address,
87
- scope,
88
- matchers,
89
- } = files[at] as (typeof files)[number]
90
- const exported = declared[at]
71
+ for (const { path, relative, route: address, scope, matchers } of files) {
72
+ const exported = declared.get(path)
91
73
  if (exported === undefined) continue
92
74
  try {
93
75
  const handler = exported.find((each) => each.name === 'default')
@@ -114,35 +96,22 @@ export async function handlersUnder(
114
96
  `abide: ${named} and ${relative} are both named ${surface} on the generated surfaces. RULEBOOK 20.13.`,
115
97
  )
116
98
  names.set(surface, relative)
117
- mounts.push({
118
- address,
119
- method: handler.method,
120
- source: relative,
121
- name: address.slice(address.lastIndexOf('/') + 1),
122
- export: 'default',
123
- scope,
124
- matchers,
125
- private: false,
126
- ui: handler.ui,
127
- cli: handler.cli,
128
- mcp: handler.mcp,
129
- description: handler.description,
130
- copy: handler.copy,
131
- defaults: handler.defaults,
132
- args: handler.args,
133
- result: handler.result,
134
- })
135
- warnings.push(...handler.unread)
99
+ mounts.push(
100
+ handlerFileMount(
101
+ { address, source: relative, scope, matchers },
102
+ { ...handler, method: handler.method },
103
+ ),
104
+ )
105
+ diagnostics.warn(handler.unread)
136
106
  } catch (thrown) {
137
- refusals.at(relative, thrown)
107
+ diagnostics.at(relative, thrown)
138
108
  }
139
109
  }
140
- for (let at = 0; at < serving.length; at += 1) {
141
- const { relative, position, place, scope } = serving[at] as Source
110
+ for (const { path, relative, position, place, scope } of serving) {
142
111
  // 34.24 — a dependency's file answers where a mount places it, and its handlers below the
143
112
  // path it has there.
144
113
  const below = place === 'dependency' ? relative : position
145
- for (const each of declared[files.length + at] ?? []) {
114
+ for (const each of declared.get(host.projectionOf(path)) ?? []) {
146
115
  if (each.method === undefined) continue
147
116
  if (!each.name.startsWith(PRIVATE_EXPORT)) continue
148
117
  const name = each.name.slice(PRIVATE_EXPORT.length)
@@ -164,10 +133,10 @@ export async function handlersUnder(
164
133
  args: each.args,
165
134
  result: each.result,
166
135
  })
167
- warnings.push(...each.unread)
136
+ diagnostics.warn(each.unread)
168
137
  }
169
138
  }
170
- return { mounts, warnings }
139
+ return mounts
171
140
  }
172
141
 
173
142
  /**
@@ -1,5 +1,3 @@
1
- import type { Diagnosis } from './TypeSession.ts'
2
-
3
1
  /**
4
2
  * An offset into a file's text as the line and column an editor shows, both from 1.
5
3
  *
@@ -36,20 +34,3 @@ export function offsetOf(
36
34
  }
37
35
  return at + position.character
38
36
  }
39
-
40
- /** abide's own finding at `offset` into `text`, or at line 0 where the text was not handed in. */
41
- export function diagnosisAt(
42
- path: string,
43
- text: string | undefined,
44
- offset: number,
45
- category: Diagnosis['category'],
46
- message: string,
47
- ): Diagnosis {
48
- return {
49
- path,
50
- ...(text === undefined ? { line: 0, column: 0 } : lineOf(text, offset)),
51
- code: 0,
52
- category,
53
- message,
54
- }
55
- }
@@ -10,8 +10,8 @@
10
10
 
11
11
  import { parseRoutePattern } from '#shared/routePattern.ts'
12
12
 
13
+ import type { Diagnostics } from './Diagnostics.ts'
13
14
  import { matcherOf } from './paramFile.ts'
14
- import type { Refusals } from './Refusals.ts'
15
15
 
16
16
  /** A directory a file mounts (23.19), where it mounts, and the param files under it, by path. */
17
17
  export type MountedTree = { directory: string; at: string; params: string[] }
@@ -28,7 +28,7 @@ type Routed = {
28
28
  * Each routed file's matchers, by its path: every name its route's segments give, to the anchored
29
29
  * source of the param file it resolves to — empty where its route names none.
30
30
  *
31
- * `params` is every param file of the app's own, by path. Refusals land at the file the fault is in:
31
+ * `params` is every param file of the app's own, by path. Diagnostics land at the file the fault is in:
32
32
  * a param file that declares no matcher at itself, a name that resolves to none at the segment's file.
33
33
  */
34
34
  export async function matchersOf(
@@ -36,7 +36,7 @@ export async function matchersOf(
36
36
  routed: readonly Routed[],
37
37
  params: readonly string[],
38
38
  trees: readonly MountedTree[],
39
- refusals: Refusals,
39
+ refusals: Diagnostics,
40
40
  ): Promise<Map<string, Record<string, string>>> {
41
41
  // Every param file's matcher, read once whichever routes name it; `undefined` where 23.24 or
42
42
  // 23.25 refused the file, so a segment naming it refuses nothing more.
@@ -14,9 +14,9 @@ import { dirname, relative, resolve } from 'node:path' // bun exposes no path he
14
14
  import { appFiles } from '#server/appTree.ts'
15
15
  import { placeOf } from '#server/placement.ts'
16
16
 
17
+ import { Diagnostics } from './Diagnostics.ts'
17
18
  import { nearest } from './nearest.ts'
18
19
  import { type AbideFile, scriptBodies } from './parseAbide.ts'
19
- import { Refusals } from './Refusals.ts'
20
20
  import { attributedImports } from './splitImports.ts'
21
21
 
22
22
  /** A `.abide` on disk, and the position it takes in the app. */
@@ -49,7 +49,7 @@ export function mountsOf(
49
49
  path: string,
50
50
  position: string,
51
51
  file: AbideFile,
52
- refusals: Refusals,
52
+ refusals: Diagnostics,
53
53
  ): DirectoryMount[] {
54
54
  const mounts: DirectoryMount[] = []
55
55
  const at = position.includes('/')
@@ -78,7 +78,7 @@ export function mountsOf(
78
78
  return []
79
79
  }
80
80
  // The package's tree is the package's to refuse; a mount places what it finds.
81
- const found = appFiles(directory, new Refusals(directory, true))
81
+ const found = appFiles(directory, new Diagnostics(directory, true))
82
82
  const placed = (below: string): Placed => ({
83
83
  path: `${directory}/${below}`,
84
84
  position: `${at}${below}`,
@@ -0,0 +1,235 @@
1
+ import { getTouchingPropertyName } from 'typescript/unstable/ast'
2
+ import type { Project } from 'typescript/unstable/async'
3
+
4
+ import { checkedShadow } from './checkedShadow.ts'
5
+ import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
6
+ import { byPlace, type Diagnosis, placedFindings } from './Diagnostics.ts'
7
+ import { type Projection, projectShadow } from './projectShadow.ts'
8
+ import { type Abide, abideIn, isReactiveSymbol } from './reactiveTypes.ts'
9
+ import { readsIn } from './readsIn.ts'
10
+ import type { ParsedSource } from './readSources.ts'
11
+ import type { ShadowEdit, ShadowHost } from './ShadowHost.ts'
12
+ import { mapForward, ShadowMap } from './ShadowMap.ts'
13
+ import { templateWarnings } from './templateWarnings.ts'
14
+
15
+ /** Whether a script can bind a name at all — a file that cannot is never handed to the checker. */
16
+ const DECLARES = /\b(?:const|let|var|import)\b/
17
+
18
+ /**
19
+ * Reads and compiler warnings for every file, in one checker snapshot.
20
+ *
21
+ * 31.3 — THE READS IN EACH FILE: every occurrence of a name bound to a `Reactive`, classified, in
22
+ * its projection (`projectShadow`). Everything else is left exactly as written. What each file's
23
+ * shadow is asked is `readsIn`'s; the rounds a binding typed from a read takes are this module's.
24
+ *
25
+ * EVERY FILE IN ONE SNAPSHOT, which is the whole of the compiler's build time. Measured at
26
+ * 19.7 ms a file taking a snapshot each against 0.8 ms a file sharing one — 25x, because
27
+ * adding a file to the project's root set is what costs, not asking about it.
28
+ *
29
+ * WHAT IT HAS TO SAY THAT IS NOT A TYPE ERROR comes back beside the reads: the compiler's own
30
+ * warnings (14.14 to 14.17, 32.14), each on the `.abide` line where the source's text
31
+ * was handed in.
32
+ */
33
+ export async function reactiveReads(
34
+ host: ShadowHost,
35
+ sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
36
+ text?: string
37
+ })[],
38
+ ): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
39
+ // Per source, its shadow and what is asked of it, or `undefined` where nothing is declared.
40
+ const queries: ({ map: ShadowMap; projection: Projection } | undefined)[] =
41
+ []
42
+ const edit: ShadowEdit = { created: [], changed: [] }
43
+
44
+ // EVERY SOURCE PROJECTS BEFORE ANY SHADOW IS REGISTERED. A projection refuses (a top-level
45
+ // `await`), and a shadow registered ahead of the refusal was in the host with the edit that
46
+ // would have reported it dropped — so the retry found nothing moved, and the checker answered
47
+ // from the old shadow at the new one's offsets, or never had the file at all.
48
+ const projected = sources.map((source) => ({
49
+ source,
50
+ projection: projectShadow(source.file),
51
+ }))
52
+ for (const { source, projection } of projected) {
53
+ // 34.2 — A `<script module>` DECLARES NAMES TOO, and the `<script>` under it is inside
54
+ // that scope on both sides. The projection holds both from offset 0.
55
+ const scripts = projection.text.slice(0, projection.scripts.end)
56
+ // A TEMPLATE POSITION IS A QUESTION TOO (14.2): a file that declares nothing but
57
+ // writes `{format(total)}` has a position only the checker can prove static. And a
58
+ // `<script server>` always is (16.70): what it binds is read off this shadow, re-exported
59
+ // under `PRIVATE_EXPORT`.
60
+ if (
61
+ !DECLARES.test(scripts) &&
62
+ projection.slots.size === 0 &&
63
+ source.file.servers.length === 0
64
+ ) {
65
+ queries.push(undefined)
66
+ continue
67
+ }
68
+ // CANONICAL AT THE KEY, not at the comparison. A doubled separator or a relative
69
+ // path is the same file and a raw string is not the same string — registered as it
70
+ // arrived, the shadow appeared in nobody's directory listing AND named a file the
71
+ // checker could not open, and the refusal blamed the app's tsconfig for it.
72
+ const map = new ShadowMap(
73
+ host.canonical(source.path),
74
+ projection.origins,
75
+ )
76
+ // AN UNCHANGED SHADOW IS NOT REPORTED, which is what a kept session saves on a save:
77
+ // the one page that moved is re-read rather than all of them.
78
+ host.write(map.shadowPath, projection.text, edit)
79
+ host.openOutside(source.path, map.shadowPath, edit)
80
+ queries.push({ map, projection })
81
+ }
82
+
83
+ const answered = queries.some((query) => query !== undefined)
84
+ ? await host.ask(
85
+ (project) => fixedPoint(host, project, sources, queries),
86
+ { edit },
87
+ )
88
+ : undefined
89
+ const answers = answered?.answers ?? sources.map(() => UNCHECKED)
90
+ const warnings = answered?.warnings ?? []
91
+ for (let at = 0; at < sources.length; at += 1) {
92
+ const source = sources[at] as (typeof sources)[number]
93
+ if (source.text !== undefined)
94
+ warnings.push(
95
+ ...templateWarnings(
96
+ source.path,
97
+ source.text,
98
+ source.file,
99
+ answers[at] ?? UNCHECKED,
100
+ ),
101
+ )
102
+ }
103
+ warnings.sort(byPlace)
104
+ return { answers, warnings }
105
+ }
106
+
107
+ /**
108
+ * Each query's answer, round after round, and the warnings each file's last answer earned — a
109
+ * source with no query answered `UNCHECKED`.
110
+ *
111
+ * 31.1 — A BINDING THE PROJECTION TYPED FROM A `Reactive` IS ASKED OF THE PROGRAM IT RUNS AS: each
112
+ * file holding one is lowered with what it has answered so far, and a binding that is a `Reactive`
113
+ * there is one in the projection too. A binding typed from one that just turned out to be a
114
+ * `Reactive` is asked again, until a round turns none. Nothing is asked, and no second snapshot
115
+ * taken, where no file holds one (`readsIn`'s `suspects`).
116
+ */
117
+ async function fixedPoint(
118
+ host: ShadowHost,
119
+ first: Project,
120
+ sources: readonly (Pick<ParsedSource, 'path' | 'file'> & {
121
+ text?: string
122
+ })[],
123
+ queries: readonly (
124
+ { map: ShadowMap; projection: Projection } | undefined
125
+ )[],
126
+ ): Promise<{ answers: CheckerAnswer[]; warnings: Diagnosis[] }> {
127
+ const answers = sources.map(() => UNCHECKED)
128
+ // Per source, its compiler warnings, the bindings the projection could not type, and those the
129
+ // lowered program proved a `Reactive`.
130
+ const warned: Diagnosis[][] = sources.map(() => [])
131
+ const suspects: number[][] = sources.map(() => [])
132
+ const held: Set<number>[] = sources.map(() => new Set())
133
+ const answer = async (
134
+ asked: Project,
135
+ abide: Abide,
136
+ at: number,
137
+ ): Promise<void> => {
138
+ const query = queries[at]
139
+ if (query === undefined) return
140
+ const source = sources[at] as (typeof sources)[number]
141
+ const sourceFile = await host.sourceFile(
142
+ asked,
143
+ query.map.shadowPath,
144
+ source.path,
145
+ 'no name in it can be typed (RULEBOOK 31.3)',
146
+ )
147
+ const read = await readsIn(
148
+ asked.checker,
149
+ abide,
150
+ sourceFile,
151
+ query.map.shadowPath,
152
+ query.projection,
153
+ source.file,
154
+ held[at] as Set<number>,
155
+ )
156
+ answers[at] = read.answer
157
+ suspects[at] = read.suspects
158
+ warned[at] = placedFindings(
159
+ source.path,
160
+ source.text,
161
+ read.warnings,
162
+ (offset) => query.map.toSource(query.map.shadowPath, offset),
163
+ 'warning',
164
+ true,
165
+ )
166
+ }
167
+ const every = queries.map((_, at) => at)
168
+ const firstAbide = await abideIn(first)
169
+ await Promise.all(every.map((at) => answer(first, firstAbide, at)))
170
+ for (;;) {
171
+ const asking = every.filter(
172
+ (at) => (suspects[at] as number[]).length > 0,
173
+ )
174
+ if (asking.length === 0) break
175
+ const edit: ShadowEdit = { created: [], changed: [] }
176
+ const lowered = asking.map((at) => {
177
+ const source = sources[at] as (typeof sources)[number]
178
+ const query = queries[at] as { map: ShadowMap }
179
+ const shadow = checkedShadow(
180
+ source.file,
181
+ answers[at] as CheckerAnswer,
182
+ )
183
+ host.write(query.map.checkedPath, shadow.text, edit)
184
+ host.openOutside(source.path, query.map.checkedPath, edit)
185
+ return { at, path: query.map.checkedPath, origins: shadow.origins }
186
+ })
187
+ const turned = await host.ask(
188
+ async (asked) => {
189
+ const abide = await abideIn(asked)
190
+ const turning = new Set<number>()
191
+ await Promise.all(
192
+ lowered.map(async ({ at, path, origins }) => {
193
+ const sourceFile =
194
+ await asked.program.getSourceFile(path)
195
+ if (sourceFile === undefined) return
196
+ for (const start of suspects[at] as number[]) {
197
+ const position = mapForward(origins, start)
198
+ if (position === undefined) continue
199
+ const symbol =
200
+ await asked.checker.getSymbolAtLocation(
201
+ getTouchingPropertyName(
202
+ sourceFile,
203
+ position,
204
+ ),
205
+ )
206
+ if (
207
+ symbol !== undefined &&
208
+ (await isReactiveSymbol(
209
+ asked.checker,
210
+ abide,
211
+ symbol,
212
+ ))
213
+ ) {
214
+ ;(held[at] as Set<number>).add(start)
215
+ turning.add(at)
216
+ }
217
+ }
218
+ }),
219
+ )
220
+ if (turning.size > 0)
221
+ await Promise.all(
222
+ [...turning].map((at) => answer(asked, abide, at)),
223
+ )
224
+ return turning
225
+ },
226
+ { edit },
227
+ )
228
+ // A snapshot the checker opened no project in answers nothing further, and no warning of
229
+ // an earlier round is reported.
230
+ if (turned === undefined) return { answers, warnings: [] }
231
+ if (turned.size === 0) break
232
+ for (const at of asking) if (!turned.has(at)) suspects[at] = []
233
+ }
234
+ return { answers, warnings: warned.flat() }
235
+ }
@@ -182,6 +182,19 @@ export async function isReactiveType(
182
182
  )
183
183
  }
184
184
 
185
+ /**
186
+ * Whether the value `symbol` names is a `Reactive` (31.3) — the one question a binding is asked,
187
+ * whether of its projection or of the program it lowers to.
188
+ */
189
+ export async function isReactiveSymbol(
190
+ checker: Checker,
191
+ abide: Abide,
192
+ symbol: CheckedSymbol,
193
+ ): Promise<boolean> {
194
+ const type = await checker.getTypeOfSymbol(symbol)
195
+ return type !== undefined && isReactiveType(checker, abide, type, false)
196
+ }
197
+
185
198
  /**
186
199
  * Whether a type is abide's `Selection`, or a type parameter constrained by one.
187
200
  *
@@ -1,11 +1,11 @@
1
1
  import { type CheckerAnswer, UNCHECKED } from './CheckerAnswer.ts'
2
+ import type { Diagnostics } from './Diagnostics.ts'
2
3
  import { type AbideFile, parseAbide } from './parseAbide.ts'
3
- import type { Refusals } from './Refusals.ts'
4
- import type { Diagnosis, TypeSession } from './TypeSession.ts'
4
+ import type { TypeSession } from './TypeSession.ts'
5
5
 
6
6
  // EVERY COMPILE OF A `.abide` STARTS THE SAME WAY: the text parsed, then every file read by the
7
7
  // checker in one snapshot. `abide build`, `abide check` and an editor's one file differ only in
8
- // what a refusal does (`Refusals`) — the build stops at it, and a check reports it and goes on with
8
+ // what a refusal does (`Diagnostics`) — the build stops at it, and a check reports it and goes on with
9
9
  // the rest.
10
10
 
11
11
  /** One `.abide`, parsed. `quiet` is read for its props alone and reported on by nobody (33.4). */
@@ -28,7 +28,7 @@ export function parseSources(
28
28
  text: string
29
29
  quiet?: boolean
30
30
  }[],
31
- refusals: Refusals,
31
+ refusals: Diagnostics,
32
32
  ): ParsedSource[] {
33
33
  const parsed: ParsedSource[] = []
34
34
  for (const source of sources) {
@@ -51,37 +51,35 @@ export function parseSources(
51
51
  }
52
52
 
53
53
  /**
54
- * Each parsed source with the checker's answer, and the compiler's warnings — in one snapshot.
54
+ * Each parsed source with the checker's answer — in one snapshot — the compiler's warnings kept in
55
+ * `diagnostics`.
55
56
  *
56
57
  * ONE SNAPSHOT FOR THE LOT, which is the whole of the compiler's build time (`readsFor`). Where
57
- * `refusals` collects, a batch that refuses is asked again a file at a time, so which file it was is
58
- * known and the others still answer; where it throws, so does the batch.
58
+ * `diagnostics` collects, a batch that refuses is asked again a file at a time, so which file it was
59
+ * is known and the others still answer; where it throws, so does the batch.
59
60
  */
60
61
  export async function readSources<Source extends ParsedSource>(
61
62
  session: TypeSession,
62
63
  parsed: readonly Source[],
63
- refusals: Refusals,
64
- ): Promise<{
65
- read: ReadSource<Source>[]
66
- warnings: Diagnosis[]
67
- }> {
64
+ diagnostics: Diagnostics,
65
+ ): Promise<ReadSource<Source>[]> {
68
66
  try {
69
67
  const { answers, warnings } = await session.readsFor(parsed)
70
- return { read: answered(parsed, answers), warnings }
68
+ diagnostics.warn(warnings)
69
+ return answered(parsed, answers)
71
70
  } catch (thrown) {
72
- if (!refusals.collecting) throw thrown
71
+ if (!diagnostics.collecting) throw thrown
73
72
  }
74
73
  const read: ReadSource<Source>[] = []
75
- const warnings: Diagnosis[] = []
76
74
  for (const source of parsed)
77
75
  try {
78
76
  const alone = await session.readsFor([source])
79
77
  read.push(...answered([source], alone.answers))
80
- warnings.push(...alone.warnings)
78
+ diagnostics.warn(alone.warnings)
81
79
  } catch (thrown) {
82
- if (!source.quiet) refusals.at(source.path, thrown)
80
+ if (!source.quiet) diagnostics.at(source.path, thrown)
83
81
  }
84
- return { read, warnings }
82
+ return read
85
83
  }
86
84
 
87
85
  function answered<Source extends ParsedSource>(