@prestack/thread 0.1.3 → 0.1.5

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 (270) hide show
  1. package/CHANGELOG.md +563 -0
  2. package/dist/core/actor.d.ts +1 -1
  3. package/dist/core/actor.js +5 -4
  4. package/dist/core/actor.ts +5 -4
  5. package/dist/core/attach.d.ts +0 -7
  6. package/dist/core/attach.js +38 -67
  7. package/dist/core/attach.ts +34 -80
  8. package/dist/core/chain.js +13 -17
  9. package/dist/core/chain.ts +11 -15
  10. package/dist/core/condition.d.ts +4 -4
  11. package/dist/core/condition.js +2 -2
  12. package/dist/core/descend.d.ts +4 -2
  13. package/dist/core/descend.js +91 -23
  14. package/dist/core/descend.ts +90 -21
  15. package/dist/core/errors/catalogue.d.ts +52 -0
  16. package/dist/{operations/errors/errors.js → core/errors/catalogue.js} +1 -1
  17. package/dist/{operations/errors/errors.ts → core/errors/catalogue.ts} +1 -1
  18. package/dist/{operations/errors/catalog.d.ts → core/errors/error.d.ts} +2 -3
  19. package/dist/{operations/errors/catalog.js → core/errors/error.js} +1 -16
  20. package/dist/{operations/errors/catalog.ts → core/errors/error.ts} +3 -19
  21. package/dist/core/errors/namespaces.d.ts +3 -0
  22. package/dist/core/errors/namespaces.js +15 -0
  23. package/dist/core/errors/namespaces.ts +19 -0
  24. package/dist/core/finding.js +2 -2
  25. package/dist/core/finding.ts +1 -2
  26. package/dist/core/init.d.ts +1 -1
  27. package/dist/core/init.js +51 -5
  28. package/dist/core/init.ts +58 -5
  29. package/dist/core/invoked.d.ts +1 -1
  30. package/dist/core/invoked.js +3 -1
  31. package/dist/core/invoked.ts +5 -2
  32. package/dist/core/round.js +36 -22
  33. package/dist/core/round.ts +26 -9
  34. package/dist/core/rows.d.ts +4 -0
  35. package/dist/core/rows.js +31 -2
  36. package/dist/core/rows.ts +33 -1
  37. package/dist/core/scope.d.ts +1 -0
  38. package/dist/core/scope.js +1 -0
  39. package/dist/core/scope.ts +2 -0
  40. package/dist/core/thread.d.ts +3 -3
  41. package/dist/core/tree.d.ts +4 -1
  42. package/dist/core/tree.js +26 -6
  43. package/dist/core/tree.ts +29 -6
  44. package/dist/core/types.d.ts +3 -3
  45. package/dist/core/types.js +3 -1
  46. package/dist/core/types.ts +2 -1
  47. package/dist/derivation/attach.js +3 -2
  48. package/dist/derivation/attach.ts +2 -1
  49. package/dist/derivation/generators/build.js +6 -6
  50. package/dist/derivation/generators/build.ts +1 -1
  51. package/dist/derivation/generators/check.js +1 -1
  52. package/dist/derivation/generators/determinism.js +1 -1
  53. package/dist/derivation/generators/release.js +60 -47
  54. package/dist/derivation/generators/release.ts +66 -43
  55. package/dist/derivation/release.d.ts +7 -3
  56. package/dist/derivation/release.js +14 -1
  57. package/dist/derivation/release.ts +22 -2
  58. package/dist/integrations/attach.d.ts +4 -0
  59. package/dist/integrations/attach.js +37 -56
  60. package/dist/integrations/attach.ts +54 -60
  61. package/dist/integrations/cli.d.ts +37 -0
  62. package/dist/integrations/cli.js +440 -0
  63. package/dist/integrations/cli.ts +543 -0
  64. package/dist/{core/files.d.ts → integrations/disk.d.ts} +16 -24
  65. package/dist/integrations/disk.js +435 -0
  66. package/dist/integrations/disk.ts +465 -0
  67. package/dist/integrations/generators/read.js +6 -6
  68. package/dist/integrations/generators/read.ts +4 -4
  69. package/dist/integrations/generators/vault.js +10 -11
  70. package/dist/integrations/generators/vault.ts +4 -5
  71. package/dist/{operations/http.d.ts → integrations/https.d.ts} +10 -6
  72. package/dist/integrations/https.js +170 -0
  73. package/dist/integrations/https.ts +216 -0
  74. package/dist/integrations/pglite.d.ts +26 -0
  75. package/dist/integrations/pglite.js +143 -0
  76. package/dist/integrations/pglite.ts +206 -0
  77. package/dist/integrations/secret.d.ts +8 -2
  78. package/dist/integrations/secret.js +3 -5
  79. package/dist/integrations/secret.ts +10 -6
  80. package/dist/integrations/target.d.ts +155 -14
  81. package/dist/integrations/target.js +171 -50
  82. package/dist/integrations/target.ts +377 -61
  83. package/dist/integrations/vault.d.ts +1 -0
  84. package/dist/integrations/vault.js +20 -5
  85. package/dist/integrations/vault.ts +17 -5
  86. package/dist/integrity/attach.js +8 -77
  87. package/dist/integrity/attach.ts +7 -76
  88. package/dist/integrity/check-threads.js +5 -5
  89. package/dist/integrity/check-threads.ts +2 -2
  90. package/dist/integrity/constraint.d.ts +0 -1
  91. package/dist/integrity/constraint.js +267 -366
  92. package/dist/integrity/constraint.ts +267 -375
  93. package/dist/integrity/generators/compiler.js +14 -17
  94. package/dist/integrity/generators/compiler.ts +10 -15
  95. package/dist/integrity/generators/gate.js +15 -15
  96. package/dist/integrity/generators/gate.ts +10 -10
  97. package/dist/integrity/generators/issues.js +7 -7
  98. package/dist/integrity/generators/publish.js +39 -57
  99. package/dist/integrity/generators/publish.ts +44 -55
  100. package/dist/integrity/generators/trial.d.ts +3 -3
  101. package/dist/integrity/generators/trial.js +133 -165
  102. package/dist/integrity/generators/trial.ts +145 -169
  103. package/dist/integrity/lint.js +9 -13
  104. package/dist/integrity/lint.ts +7 -10
  105. package/dist/integrity/lsp/bin.js +5 -4
  106. package/dist/integrity/lsp/bin.ts +3 -1
  107. package/dist/integrity/lsp/diagnostics.js +3 -3
  108. package/dist/integrity/lsp/diagnostics.ts +1 -1
  109. package/dist/integrity/lsp/lint.js +19 -30
  110. package/dist/integrity/lsp/lint.ts +16 -32
  111. package/dist/integrity/lsp/server.js +3 -3
  112. package/dist/integrity/slopnator.d.ts +3 -13
  113. package/dist/integrity/slopnator.js +59 -38
  114. package/dist/integrity/slopnator.ts +54 -38
  115. package/dist/integrity/slopnators/sl_broken_lineage.js +2 -2
  116. package/dist/integrity/slopnators/sl_declared_elsewhere.js +1 -1
  117. package/dist/integrity/slopnators/sl_derived_drift.js +4 -4
  118. package/dist/integrity/slopnators/sl_derived_drift.ts +1 -1
  119. package/dist/integrity/slopnators/sl_door_in_a_node.js +220 -0
  120. package/dist/integrity/slopnators/sl_door_in_a_node.ts +250 -0
  121. package/dist/integrity/slopnators/sl_inert_island.js +10 -8
  122. package/dist/integrity/slopnators/sl_inert_island.ts +8 -6
  123. package/dist/integrity/slopnators/sl_missing_row.js +6 -51
  124. package/dist/integrity/slopnators/sl_missing_row.ts +4 -50
  125. package/dist/integrity/slopnators/sl_no_barrel.js +1 -1
  126. package/dist/integrity/slopnators/sl_no_child_process.js +36 -18
  127. package/dist/integrity/slopnators/sl_no_child_process.ts +44 -18
  128. package/dist/integrity/slopnators/sl_no_dispatch_bypass.js +80 -149
  129. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +95 -147
  130. package/dist/integrity/slopnators/sl_no_import_cycle.js +15 -4
  131. package/dist/integrity/slopnators/sl_no_import_cycle.ts +20 -4
  132. package/dist/integrity/slopnators/sl_no_such_command.js +2 -2
  133. package/dist/integrity/slopnators/sl_no_such_command.ts +1 -1
  134. package/dist/integrity/slopnators/sl_outside_an_operation.js +20 -0
  135. package/dist/integrity/slopnators/sl_outside_an_operation.ts +25 -0
  136. package/dist/integrity/slopnators/sl_screen_holds_only_declared.d.ts +2 -0
  137. package/dist/integrity/slopnators/sl_screen_holds_only_declared.js +61 -0
  138. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +70 -0
  139. package/dist/integrity/slopnators/sl_secret_literal.js +1 -33
  140. package/dist/integrity/slopnators/sl_secret_literal.ts +1 -33
  141. package/dist/integrity/slopnators/sl_stale_path.js +5 -22
  142. package/dist/integrity/slopnators/sl_stale_path.ts +4 -20
  143. package/dist/integrity/slopnators/sl_twice_meant.js +1 -1
  144. package/dist/integrity/slopnators/sl_twice_parented.js +29 -5
  145. package/dist/integrity/slopnators/sl_twice_parented.ts +28 -4
  146. package/dist/integrity/slopnators/sl_undeclared_crossing.d.ts +4 -1
  147. package/dist/integrity/slopnators/sl_undeclared_crossing.js +426 -62
  148. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +454 -70
  149. package/dist/integrity/slopnators/sl_undeclared_destination.js +59 -25
  150. package/dist/integrity/slopnators/sl_undeclared_destination.ts +69 -21
  151. package/dist/integrity/slopnators/sl_undeclared_list.d.ts +2 -0
  152. package/dist/integrity/slopnators/sl_undeclared_list.js +363 -0
  153. package/dist/integrity/slopnators/sl_undeclared_list.ts +358 -0
  154. package/dist/integrity/slopnators/sl_undeclared_node.js +1 -11
  155. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -10
  156. package/dist/integrity/slopnators/sl_undeclared_operation.js +111 -29
  157. package/dist/integrity/slopnators/sl_undeclared_operation.ts +134 -26
  158. package/dist/integrity/slopnators/sl_undeclared_variant.js +2 -2
  159. package/dist/integrity/slopnators/sl_undeclared_webhook.d.ts +2 -0
  160. package/dist/integrity/slopnators/sl_undeclared_webhook.js +90 -0
  161. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +108 -0
  162. package/dist/integrity/slopnators/sl_underived_route.d.ts +2 -0
  163. package/dist/integrity/slopnators/sl_underived_route.js +61 -0
  164. package/dist/integrity/slopnators/sl_underived_route.ts +72 -0
  165. package/dist/integrity/slopnators/sl_ungoverned_island.js +3 -3
  166. package/dist/integrity/slopnators/sl_ungoverned_island.ts +1 -1
  167. package/dist/integrity/slopnators/sl_unlisted_program.d.ts +2 -0
  168. package/dist/integrity/slopnators/sl_unlisted_program.js +39 -0
  169. package/dist/integrity/slopnators/sl_unlisted_program.ts +40 -0
  170. package/dist/integrity/slopnators/sl_unnamed_island.js +1 -1
  171. package/dist/integrity/slopnators/sl_unpaired_member.js +3 -3
  172. package/dist/integrity/slopnators/sl_unpaired_member.ts +1 -1
  173. package/dist/integrity/slopnators/sl_unpaired_slopnator.js +4 -4
  174. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +1 -1
  175. package/dist/integrity/slopnators/sl_unprefixed_member.js +5 -31
  176. package/dist/integrity/slopnators/sl_unprefixed_member.ts +2 -28
  177. package/dist/integrity/slopnators/sl_unthreaded.js +11 -7
  178. package/dist/integrity/slopnators/sl_unthreaded.ts +6 -7
  179. package/dist/integrity/slopnators/sl_untranslated_key.js +2 -25
  180. package/dist/integrity/slopnators/sl_untranslated_key.ts +1 -25
  181. package/dist/integrity/slopnators/sl_unvaulted_secret.d.ts +2 -0
  182. package/dist/integrity/slopnators/sl_unvaulted_secret.js +107 -0
  183. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +132 -0
  184. package/dist/integrity/slopnators/sl_unversioned.js +6 -4
  185. package/dist/integrity/slopnators/sl_unversioned.ts +5 -3
  186. package/dist/integrity/sweep.d.ts +1 -0
  187. package/dist/integrity/sweep.js +105 -42
  188. package/dist/integrity/sweep.ts +115 -42
  189. package/dist/knowledge/attach.js +4 -3
  190. package/dist/knowledge/attach.ts +3 -2
  191. package/dist/knowledge/generators/docsgen.js +8 -6
  192. package/dist/knowledge/generators/docsgen.ts +4 -2
  193. package/dist/knowledge/generators/glossary.js +4 -4
  194. package/dist/knowledge/generators/glossary.ts +1 -1
  195. package/dist/operations/access.d.ts +23 -0
  196. package/dist/operations/access.js +28 -1
  197. package/dist/operations/access.ts +43 -0
  198. package/dist/operations/attach.js +33 -113
  199. package/dist/operations/attach.ts +51 -117
  200. package/dist/operations/capabilities.d.ts +14 -5
  201. package/dist/operations/capabilities.js +13 -4
  202. package/dist/operations/capabilities.ts +27 -8
  203. package/dist/operations/dispatch.d.ts +8 -2
  204. package/dist/operations/dispatch.js +128 -185
  205. package/dist/operations/dispatch.ts +138 -191
  206. package/dist/operations/error-results.d.ts +1 -1
  207. package/dist/operations/error-results.js +2 -2
  208. package/dist/operations/error-results.ts +2 -2
  209. package/dist/operations/generators/capability.d.ts +11 -1
  210. package/dist/operations/generators/capability.js +481 -224
  211. package/dist/operations/generators/capability.ts +562 -233
  212. package/dist/operations/generators/compile.js +66 -39
  213. package/dist/operations/generators/compile.ts +78 -39
  214. package/dist/operations/generators/permits.js +11 -12
  215. package/dist/operations/generators/permits.ts +3 -4
  216. package/dist/operations/generators/seed.d.ts +1 -0
  217. package/dist/operations/generators/seed.js +19 -17
  218. package/dist/operations/generators/seed.ts +22 -14
  219. package/dist/operations/generators/trial.d.ts +6 -8
  220. package/dist/operations/generators/trial.js +200 -70
  221. package/dist/operations/generators/trial.ts +230 -93
  222. package/dist/operations/nodes.d.ts +15 -16
  223. package/dist/operations/nodes.ts +16 -17
  224. package/dist/operations/operations.d.ts +40 -4
  225. package/dist/operations/operations.js +1 -0
  226. package/dist/operations/operations.ts +61 -4
  227. package/dist/operations/result.d.ts +2 -2
  228. package/dist/operations/result.ts +2 -2
  229. package/package.json +1 -4
  230. package/dist/core/answers.d.ts +0 -171
  231. package/dist/core/answers.js +0 -789
  232. package/dist/core/answers.ts +0 -999
  233. package/dist/core/client.d.ts +0 -63
  234. package/dist/core/client.js +0 -249
  235. package/dist/core/client.ts +0 -289
  236. package/dist/core/files.js +0 -171
  237. package/dist/core/files.ts +0 -222
  238. package/dist/integrations/module.d.ts +0 -6
  239. package/dist/integrations/module.js +0 -153
  240. package/dist/integrations/module.ts +0 -191
  241. package/dist/integrity/slopnators/sl_unkept_rows.js +0 -104
  242. package/dist/integrity/slopnators/sl_unkept_rows.ts +0 -111
  243. package/dist/integrity/slopnators/sl_unseamed_reach.js +0 -73
  244. package/dist/integrity/slopnators/sl_unseamed_reach.ts +0 -79
  245. package/dist/operations/cost.d.ts +0 -14
  246. package/dist/operations/cost.js +0 -37
  247. package/dist/operations/cost.ts +0 -58
  248. package/dist/operations/errors/errors.d.ts +0 -52
  249. package/dist/operations/http.js +0 -124
  250. package/dist/operations/http.ts +0 -169
  251. package/dist/operations/memory.d.ts +0 -104
  252. package/dist/operations/memory.js +0 -365
  253. package/dist/operations/memory.ts +0 -470
  254. package/dist/operations/metering.d.ts +0 -55
  255. package/dist/operations/metering.js +0 -247
  256. package/dist/operations/metering.ts +0 -328
  257. package/dist/operations/queue-policy.d.ts +0 -3
  258. package/dist/operations/queue-policy.js +0 -8
  259. package/dist/operations/queue-policy.ts +0 -10
  260. package/dist/operations/seams.d.ts +0 -239
  261. package/dist/operations/seams.js +0 -45
  262. package/dist/operations/seams.ts +0 -364
  263. package/dist/operations/store.d.ts +0 -14
  264. package/dist/operations/store.js +0 -1
  265. package/dist/operations/store.ts +0 -29
  266. /package/dist/{operations → core}/errors/classes.d.ts +0 -0
  267. /package/dist/{operations → core}/errors/classes.js +0 -0
  268. /package/dist/{operations → core}/errors/classes.ts +0 -0
  269. /package/dist/integrity/slopnators/{sl_unkept_rows.d.ts → sl_door_in_a_node.d.ts} +0 -0
  270. /package/dist/integrity/slopnators/{sl_unseamed_reach.d.ts → sl_outside_an_operation.d.ts} +0 -0
@@ -1,87 +1,471 @@
1
- import { descend, under } from '../../core/descend.ts'
2
-
3
- import type { Slopnator } from '../slopnator.ts'
4
- import { here } from './sl_unthreaded.ts'
5
-
6
- interface Crossing {
7
- readonly what: string
8
- readonly finds: RegExp
9
- }
10
-
11
- const CROSSINGS: readonly Crossing[] = [
12
- { what: 'the network', finds: /(?<![.\w])fetch\s*\(/ },
13
- { what: 'the network', finds: /(?<![.\w])XMLHttpRequest\b/ },
14
- { what: 'the clock', finds: /\bDate\s*\.\s*now\s*\(/ },
15
- { what: 'the clock', finds: /\bperformance\s*\.\s*now\s*\(/ },
16
- { what: 'the clock', finds: /\bsetInterval\s*\(/ },
17
- { what: 'the clock', finds: /\bsetTimeout\s*\(/ },
18
- { what: 'the clock', finds: /\brequestAnimationFrame\s*\(/ },
19
- { what: 'randomness', finds: /\bMath\s*\.\s*random\s*\(/ },
20
- { what: 'randomness', finds: /\bcrypto\s*\.\s*randomUUID\s*\(/ },
21
- { what: 'the queue', finds: /\bqueue\s*\.\s*enqueue\s*\(/ },
22
- { what: 'a seam', finds: /\b(?:files|llm|http)\s*\.\s*(?:call|run|complete|put|signedUrl|remove|get)\s*\(/ },
23
- ]
24
-
25
- const OPERATIONS_ID = 'operations'
26
-
27
- const NODES_ID = 'nodes'
28
-
29
- let bodies: ReadonlySet<string> | undefined
30
-
31
- /*
32
- * The files this law is about, read off the tree rather than off a path.
33
- *
34
- * A crossing belongs in a declared node and nowhere else, so what the law has to know is which
35
- * files ARE declared nodes — every row in an operation's `seeds` that requires a node type. It
36
- * cannot be a path rule: an operation seeds its node bodies and its input file into the same
37
- * directory, so excepting the directory would except the input file too and leave a hole exactly
38
- * one file wide per operation. Measured 2026-08-27: with the directory excepted instead, this law
39
- * fired on `operations/wf_asks/asks_it.ts` — a node body calling the model its operation had
40
- * declared, which is the one place the law's own `instead` sends you.
41
- */
42
- function declared(): ReadonlySet<string> {
43
- if (bodies !== undefined) return bodies
1
+ import { resolve } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ import ts from 'typescript'
5
+
6
+ import { ROOT, THREAD_AT, configured, descend, hosting, sources, under, type Pass } from '../../core/descend.ts'
7
+ import { packageOf } from '../../core/attach.ts'
8
+ import { PREFIXES } from '../../core/thread.ts'
9
+ import { HOLDS, OPENS, type Reach } from '../../integrations/target.ts'
10
+
11
+ import type { Slopnator, SlopnatorContext } from '../slopnator.ts'
12
+
13
+ const FROM_SOURCE = import.meta.url.endsWith('.ts')
14
+
15
+ const OPENED_AT = fileURLToPath(new URL(FROM_SOURCE ? '../../operations/operations.ts' : '../../operations/operations.d.ts', import.meta.url))
16
+
17
+
18
+ const HANDED = 'Opened'
19
+
20
+ const EVERY = ''
21
+
22
+ const IMPORTED = 'import '
23
+
24
+ const MADE = 'new '
25
+
26
+ const ANYWHERE = 'globalThis'
27
+
28
+ const REQUIRED = 'require'
29
+
30
+ const RUNTIME = 'node:'
31
+
32
+ const SOURCE = /[.](?:ts|tsx|mts|cts)$/
33
+
34
+ const ROOTED = ROOT + '/'
35
+
36
+ const here = (filename: string): string => (filename.startsWith(ROOTED) ? filename.slice(ROOTED.length) : filename)
37
+
38
+ const plain = (specifier: string): string => (specifier.startsWith(RUNTIME) ? specifier.slice(RUNTIME.length) : specifier)
39
+
40
+ interface Door {
41
+ readonly hand: string
42
+ readonly spelling: string
43
+ }
44
+
45
+ const HANDS: Readonly<Record<string, string>> = Object.fromEntries(
46
+ (Object.keys(HOLDS) as Reach[]).map((one) => [
47
+ one,
48
+ HOLDS[one].takes === 'entities' ? 'store' : HOLDS[one].takes === 'calls' ? 'target' : one,
49
+ ]),
50
+ )
51
+
52
+ function opening(): Readonly<Record<string, readonly string[]>> {
53
+ const found: Record<string, string[]> = {}
54
+ const add = (hand: string, spellings: readonly string[]): void => {
55
+ found[hand] = [...(found[hand] ?? []), ...spellings]
56
+ }
57
+ for (const [reach, spellings] of Object.entries(OPENS)) add(HANDS[reach] ?? reach, spellings)
58
+ return found
59
+ }
60
+
61
+ interface Doors {
62
+ readonly modules: ReadonlyMap<string, Door>
63
+ readonly members: ReadonlyMap<string, ReadonlyMap<string, Door>>
64
+ readonly globals: ReadonlyMap<string, Door>
65
+ readonly properties: ReadonlyMap<string, Door>
66
+ readonly made: ReadonlyMap<string, Door>
67
+ }
68
+
69
+ function doors(): Doors {
70
+ const modules = new Map<string, Door>()
71
+ const members = new Map<string, Map<string, Door>>()
72
+ const globals = new Map<string, Door>()
73
+ const properties = new Map<string, Door>()
74
+ const made = new Map<string, Door>()
75
+ for (const [hand, spellings] of Object.entries(opening())) {
76
+ for (const spelling of spellings) {
77
+ const door = { hand, spelling }
78
+ if (spelling.startsWith(IMPORTED)) {
79
+ const rest = spelling.slice(IMPORTED.length)
80
+ const dot = rest.lastIndexOf('.')
81
+ if (dot === -1 || rest.slice(dot).includes('/')) {
82
+ modules.set(plain(rest), door)
83
+ continue
84
+ }
85
+ const specifier = plain(rest.slice(0, dot))
86
+ const held = members.get(specifier) ?? new Map<string, Door>()
87
+ held.set(rest.slice(dot + 1), door)
88
+ members.set(specifier, held)
89
+ continue
90
+ }
91
+ if (spelling.startsWith(MADE)) {
92
+ made.set(spelling.slice(MADE.length), door)
93
+ continue
94
+ }
95
+ if (spelling.includes('.')) properties.set(spelling, door)
96
+ else globals.set(spelling, door)
97
+ }
98
+ }
99
+ return { modules, members, globals, properties, made }
100
+ }
101
+
102
+ const DOORS = doors()
103
+
104
+ const INTEGRATION = 'Integration'
105
+
106
+ const NEEDS = 'needs'
107
+
108
+ function needed(pass: Pass): ReadonlyMap<string, Door> {
109
+ const found = new Map<string, Door>()
110
+ const rows = [...pass.stated.values(), ...[...pass.threaded.values()].flat().map((one) => one.row)]
111
+ for (const row of rows) {
112
+ if (row.type !== INTEGRATION) continue
113
+ const held = row.fields.get(NEEDS)
114
+ if (held === undefined || !ts.isObjectLiteralExpression(held)) continue
115
+ for (const one of held.properties) {
116
+ if (!ts.isPropertyAssignment(one)) continue
117
+ const name = ts.isStringLiteral(one.name) || ts.isIdentifier(one.name) ? one.name.text : undefined
118
+ if (name !== undefined) found.set(name, { hand: HANDS['sdk'] ?? 'sdk', spelling: IMPORTED + name })
119
+ }
120
+ }
121
+ return found
122
+ }
123
+
124
+ const moduleOf = (door: Door): string => door.spelling.slice(IMPORTED.length, door.spelling.lastIndexOf('.'))
125
+
126
+ const stands = (door: Door): string => ', which the ' + door.hand + ' this app is handed stands in for'
127
+
128
+ interface Built {
129
+ readonly of: Pass
130
+ readonly program: ts.Program
131
+ readonly checker: ts.TypeChecker
132
+ readonly hands: ReadonlyMap<ts.Symbol, string>
133
+ readonly packages: ReadonlyMap<string, Door>
134
+ }
135
+
136
+ let built: Built | undefined
137
+
138
+ function argumentsOf(type: ts.Type, checker: ts.TypeChecker): readonly ts.Type[] {
139
+ const found: ts.Type[] = [...(type.aliasTypeArguments ?? [])]
140
+ if ((type.flags & ts.TypeFlags.Object) !== 0 && ((type as ts.ObjectType).objectFlags & ts.ObjectFlags.Reference) !== 0) {
141
+ found.push(...checker.getTypeArguments(type as ts.TypeReference))
142
+ }
143
+ return found
144
+ }
145
+
146
+ function handed(type: ts.Type, checker: ts.TypeChecker, hand: string, into: Map<ts.Symbol, string>, seen: Set<ts.Type>, own: (symbol: ts.Symbol) => boolean): void {
147
+ if (seen.has(type)) return
148
+ seen.add(type)
149
+ if (type.isUnionOrIntersection()) {
150
+ for (const one of type.types) handed(one, checker, hand, into, seen, own)
151
+ return
152
+ }
153
+ for (const one of argumentsOf(type, checker)) handed(one, checker, hand, into, seen, own)
154
+ const index = checker.getIndexTypeOfType(type, ts.IndexKind.String)
155
+ if (index !== undefined) handed(index, checker, hand, into, seen, own)
156
+ const symbol = type.getSymbol()
157
+ if (symbol === undefined || !own(symbol)) return
158
+ if ((symbol.flags & (ts.SymbolFlags.Interface | ts.SymbolFlags.TypeLiteral | ts.SymbolFlags.Class)) === 0) return
159
+ if (!into.has(symbol)) into.set(symbol, hand)
160
+ }
161
+ function given(node: ts.TypeNode, checker: ts.TypeChecker, into: Map<ts.Symbol, string>, own: (symbol: ts.Symbol) => boolean): void {
162
+ const take = (one: ts.Node): void => {
163
+ if (ts.isTypeReferenceNode(one)) {
164
+ const named = ts.isIdentifier(one.typeName) ? one.typeName : one.typeName.right
165
+ const found = checker.getSymbolAtLocation(named)
166
+ const real = found !== undefined && (found.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(found) : found
167
+ const shapes = ts.SymbolFlags.Interface | ts.SymbolFlags.Class | ts.SymbolFlags.TypeAlias
168
+ if (real !== undefined && own(real) && (real.flags & shapes) !== 0 && !into.has(real)) into.set(real, EVERY)
169
+ }
170
+ ts.forEachChild(one, take)
171
+ }
172
+ const inside = (one: ts.Node): void => {
173
+ if (ts.isMappedTypeNode(one)) {
174
+ if (one.type !== undefined) take(one.type)
175
+ return
176
+ }
177
+ ts.forEachChild(one, inside)
178
+ }
179
+ inside(node)
180
+ }
181
+
182
+ function compiled(): Built {
44
183
  const pass = descend()
45
- const found = new Set<string>()
184
+ if (built !== undefined && built.of === pass) return built
185
+ const options = configured().options
186
+ const roots = [THREAD_AT, ...sources()].filter((one) => SOURCE.test(one)).map((one) => resolve(ROOT, one))
187
+ const program = ts.createProgram([...new Set([OPENED_AT, ...roots])], options, hosting(options))
188
+ const checker = program.getTypeChecker()
189
+ const hands = new Map<ts.Symbol, string>()
190
+ const own = (symbol: ts.Symbol): boolean =>
191
+ (symbol.getDeclarations() ?? []).some((one) => {
192
+ const from = one.getSourceFile()
193
+ return !program.isSourceFileDefaultLibrary(from) && !program.isSourceFileFromExternalLibrary(from)
194
+ })
195
+ const shape = program.getSourceFile(OPENED_AT)
196
+ for (const statement of shape?.statements ?? []) {
197
+ if (!ts.isTypeAliasDeclaration(statement) || statement.name.text !== HANDED) continue
198
+ const alias = checker.getSymbolAtLocation(statement.name)
199
+ if (alias !== undefined) hands.set(alias, EVERY)
200
+ handed(checker.getTypeFromTypeNode(statement.type), checker, EVERY, hands, new Set(), own)
201
+ given(statement.type, checker, hands, own)
202
+ }
203
+ built = { of: pass, program, checker, hands, packages: needed(pass) }
204
+ return built
205
+ }
206
+
207
+ const INTEGRATIONS_ID = 'integrations'
208
+
209
+ function integral(pass: Pass, path: string): boolean {
210
+ for (const [at, threadings] of pass.threaded) {
211
+ if (at !== path) continue
212
+ for (const one of threadings) {
213
+ if (one.by === INTEGRATIONS_ID || under(pass, one.by, INTEGRATIONS_ID)) return true
214
+ }
215
+ }
216
+ return false
217
+ }
218
+
219
+ function operational(pass: Pass, path: string): boolean {
220
+ const prefixes = Object.values(PREFIXES)
221
+ const owned = (one: string): boolean => prefixes.some((prefix) => one.startsWith(prefix))
222
+ if (path.split('/').some((part) => owned(part))) return true
46
223
  for (const [at, threadings] of pass.threaded) {
224
+ if (at !== path) continue
47
225
  for (const one of threadings) {
48
- if (one.list !== 'seeds') continue
49
- if (!under(pass, one.by, OPERATIONS_ID) || under(pass, one.by, NODES_ID)) continue
50
- if ((one.row.requires ?? []).length === 0) continue
51
- found.add(at)
226
+ if (owned(one.by) || owned(String(one.row.id ?? ''))) return true
227
+ }
228
+ }
229
+ return false
230
+ }
231
+
232
+ function handOf(type: ts.Type, checker: ts.TypeChecker, hands: ReadonlyMap<ts.Symbol, string>, seen: Set<ts.Type>): string | undefined {
233
+ if (seen.has(type)) return undefined
234
+ seen.add(type)
235
+ const named = type.aliasSymbol === undefined ? undefined : hands.get(type.aliasSymbol)
236
+ if (named !== undefined) return named
237
+ if (type.isUnionOrIntersection()) {
238
+ for (const one of type.types) {
239
+ const found = handOf(one, checker, hands, seen)
240
+ if (found !== undefined) return found
241
+ }
242
+ return undefined
243
+ }
244
+ const symbol = type.getSymbol() ?? type.aliasSymbol
245
+ const own = symbol === undefined ? undefined : hands.get(symbol)
246
+ if (own !== undefined) return own
247
+ for (const one of argumentsOf(type, checker)) {
248
+ const found = handOf(one, checker, hands, seen)
249
+ if (found !== undefined) return found
250
+ }
251
+ return undefined
252
+ }
253
+
254
+ function implemented(file: ts.SourceFile, built: Built): ReadonlySet<string> {
255
+ const found = new Set<string>()
256
+ const { checker, hands } = built
257
+ const held = checker.getSymbolAtLocation(file)
258
+ if (held === undefined) return found
259
+ const carries = (type: ts.Type, depth: number, seen: Set<ts.Type>): void => {
260
+ if (seen.has(type)) return
261
+ seen.add(type)
262
+ const hand = handOf(type, checker, hands, new Set())
263
+ if (hand !== undefined) found.add(hand)
264
+ if (depth === 0) return
265
+ for (const one of type.isUnionOrIntersection() ? type.types : [type]) {
266
+ for (const signature of one.getCallSignatures()) carries(signature.getReturnType(), depth - 1, seen)
267
+ for (const property of one.getProperties()) carries(checker.getTypeOfSymbolAtLocation(property, file), depth - 1, seen)
52
268
  }
53
269
  }
54
- bodies = found
270
+ for (const exported of checker.getExportsOfModule(held)) {
271
+ const real = (exported.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(exported) : exported
272
+ if ((real.flags & ts.SymbolFlags.Value) === 0) continue
273
+ carries(checker.getTypeOfSymbolAtLocation(real, file), 2, new Set())
274
+ }
55
275
  return found
56
276
  }
57
277
 
58
- export const slopnator: Slopnator = {
59
- holds: (_node, at) => {
60
- if (declared().has(here(at.filename))) return false
61
- for (const crossing of CROSSINGS) {
62
- if (!crossing.finds.test(at.text)) continue
63
- return (
64
- `${crossing.what} reached outside a declared node — nothing recorded this crossing, so it ` +
65
- 'cannot be replayed, retried, priced or unwound, and the page renders exactly the same either way'
66
- )
278
+ export const DOORWAY = 'door'
279
+
280
+ export const HANDED_TO = 'hand'
281
+
282
+ interface Noted {
283
+ readonly line: number
284
+ readonly said: string
285
+ readonly kind: string
286
+ }
287
+
288
+ function judged(path: string, built: Built, operates: boolean, wanted: string): readonly string[] {
289
+ const file = built.program.getSourceFile(resolve(ROOT, path))
290
+ if (file === undefined) return []
291
+ const { checker, hands } = built
292
+ let door: boolean | undefined
293
+ let allowed: ReadonlySet<string> | undefined
294
+ const may = (hand: string): boolean => {
295
+ if (door === undefined) door = integral(built.of, path)
296
+ if (allowed === undefined) allowed = implemented(file, built)
297
+ return allowed.has(EVERY) || allowed.has(hand) || door || (operates && hand === EVERY)
298
+ }
299
+ const found: Noted[] = []
300
+ const line = (node: ts.Node): number => file.getLineAndCharacterOfPosition(node.getStart(file)).line + 1
301
+ const note = (node: ts.Node, said: string, kind: string): void => {
302
+ found.push({ line: line(node), said: 'line ' + String(line(node)) + ': ' + said, kind })
303
+ }
304
+ const locals = new Map<string, Door>()
305
+ const spaces = new Map<string, ReadonlyMap<string, Door>>()
306
+
307
+ const global = (id: ts.Identifier): boolean => {
308
+ const symbol = checker.getSymbolAtLocation(id)
309
+ if (symbol === undefined) return true
310
+ const declared = symbol.getDeclarations() ?? []
311
+ return declared.every((one) => one.getSourceFile().isDeclarationFile)
312
+ }
313
+
314
+ const named = (node: ts.Node): boolean => {
315
+ const parent = node.parent as { readonly name?: ts.Node } | undefined
316
+ return parent !== undefined && parent.name === node
317
+ }
318
+
319
+ const inType = (node: ts.Node): boolean => {
320
+ let up: ts.Node | undefined = node
321
+ while (up !== undefined) {
322
+ if (ts.isTypeQueryNode(up)) return true
323
+ up = up.parent
67
324
  }
68
325
  return false
69
- },
326
+ }
327
+
328
+ const reaching = (specifier: string, node: ts.Node): void => {
329
+ const door = DOORS.modules.get(plain(specifier)) ?? built.packages.get(packageOf(specifier))
330
+ if (door !== undefined && !may(door.hand)) note(node, 'imports ' + specifier + stands(door), DOORWAY)
331
+ }
332
+
333
+ const visit = (node: ts.Node): void => {
334
+ if (ts.isImportDeclaration(node) && node.importClause?.isTypeOnly !== true && ts.isStringLiteral(node.moduleSpecifier)) {
335
+ reaching(node.moduleSpecifier.text, node)
336
+ const held = DOORS.members.get(plain(node.moduleSpecifier.text))
337
+ const bindings = node.importClause?.namedBindings
338
+ if (held !== undefined && bindings !== undefined) {
339
+ if (ts.isNamespaceImport(bindings)) spaces.set(bindings.name.text, held)
340
+ else {
341
+ for (const element of bindings.elements) {
342
+ const door = held.get((element.propertyName ?? element.name).text)
343
+ if (door !== undefined) locals.set(element.name.text, door)
344
+ }
345
+ }
346
+ }
347
+ }
348
+ if (ts.isExportDeclaration(node) && !node.isTypeOnly && node.moduleSpecifier !== undefined && ts.isStringLiteral(node.moduleSpecifier)) {
349
+ reaching(node.moduleSpecifier.text, node)
350
+ }
351
+ if (ts.isCallExpression(node)) {
352
+ const first = node.arguments[0]
353
+ const loads = node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === REQUIRED)
354
+ if (loads && first !== undefined && ts.isStringLiteral(first)) reaching(first.text, node)
355
+ const callee = node.expression
356
+ if (ts.isPropertyAccessExpression(callee) || ts.isElementAccessExpression(callee)) {
357
+ const taken = callee.expression
358
+ const hand =
359
+ handOf(checker.getTypeAtLocation(taken), checker, hands, new Set()) ??
360
+ (ts.isPropertyAccessExpression(taken) || ts.isElementAccessExpression(taken)
361
+ ? handOf(checker.getTypeAtLocation(taken.expression), checker, hands, new Set())
362
+ : undefined)
363
+ if (hand !== undefined && !may(hand)) {
364
+ const what = ts.isPropertyAccessExpression(callee) ? callee.name.text : 'an order'
365
+ note(node, 'calls ' + what + ' on ' + (hand === EVERY ? 'what this app hands an operation' : 'the ' + hand + ' it is handed'), HANDED_TO)
366
+ }
367
+ }
368
+ }
369
+ if (ts.isNewExpression(node) && ts.isIdentifier(node.expression) && (node.arguments ?? []).length === 0) {
370
+ const door = DOORS.made.get(node.expression.text)
371
+ if (door !== undefined && !may(door.hand) && global(node.expression)) note(node, 'reaches ' + door.spelling + '()' + stands(door), DOORWAY)
372
+ }
373
+ if (ts.isPropertyAccessExpression(node) && !ts.isPartOfTypeNode(node)) {
374
+ const inner = ts.isPropertyAccessExpression(node.expression) && ts.isIdentifier(node.expression.expression) && node.expression.expression.text === ANYWHERE ? node.expression.name : undefined
375
+ const object = inner ?? node.expression
376
+ if (ts.isIdentifier(object)) {
377
+ const spaced = spaces.get(object.text)?.get(node.name.text)
378
+ if (spaced !== undefined && !may(spaced.hand)) note(node, 'reaches ' + node.name.text + ' from ' + moduleOf(spaced) + stands(spaced), DOORWAY)
379
+ const door = DOORS.properties.get(object.text + '.' + node.name.text)
380
+ if (door !== undefined && !may(door.hand) && (inner !== undefined || global(object))) note(node, 'reaches ' + door.spelling + stands(door), DOORWAY)
381
+ if (object.text === ANYWHERE) {
382
+ const bare = DOORS.globals.get(node.name.text)
383
+ if (bare !== undefined && !may(bare.hand)) note(node, 'reaches ' + bare.spelling + stands(bare), DOORWAY)
384
+ }
385
+ }
386
+ }
387
+ if (ts.isIdentifier(node) && !named(node) && !ts.isPartOfTypeNode(node) && !inType(node)) {
388
+ const local = locals.get(node.text)
389
+ if (local !== undefined && !may(local.hand)) note(node, 'reaches ' + node.text + ' from ' + moduleOf(local) + stands(local), DOORWAY)
390
+ const door = DOORS.globals.get(node.text)
391
+ if (door !== undefined && !may(door.hand) && global(node)) note(node, 'reaches ' + door.spelling + stands(door), DOORWAY)
392
+ }
393
+ ts.forEachChild(node, visit)
394
+ }
395
+ visit(file)
396
+ return found.filter((one) => one.kind === wanted).sort((one, two) => one.line - two.line).map((one) => one.said)
397
+ }
398
+
399
+ export function judging(at: SlopnatorContext, wanted: string, closing: string): string | false {
400
+ const path = here(at.filename)
401
+ if (!SOURCE.test(path)) return false
402
+ const held = compiled()
403
+ if (held.hands.size === 0 && resolve(ROOT, path) === OPENED_AT) {
404
+ return 'no type called ' + HANDED + ' can be read here, so nothing can tell what an operation was opened with from any other value, and a call on it outside an operation goes unmeasured'
405
+ }
406
+ const found = judged(path, held, operational(held.of, path), wanted)
407
+ if (found.length === 0) return false
408
+ return found.join('; ') + '. ' + closing
409
+ }
410
+
411
+ export const slopnator: Slopnator = {
412
+ holds: (_node, at) =>
413
+ judging(at, DOORWAY, 'Each of these reaches past this process without going through a declared integration, and a reach this app never declared is one nothing can price, replay or unwind.'),
70
414
  proves: {
71
415
  wrong: [
72
- "const res = await fetch('https://api.stripe.com/v1/charges')",
73
- 'const at = Date.now()',
74
- 'const id = crypto.randomUUID()',
75
- "await queue.enqueue('job', payload)",
76
- "await files.put('receipt.pdf', bytes)",
77
- 'const t = setInterval(() => refetch(), 5000)',
78
- 'const t = setTimeout(function again() { refetch(); setTimeout(again, 5000) }, 5000)',
79
- 'window.requestAnimationFrame(step)',
416
+ "import { spawnSync } from 'node:child_process'\n\nexport const ran = spawnSync('git', ['status'])",
417
+ "import * as processes from 'node:child_process'\n\nexport const ran = processes.spawnSync('git', ['status'])",
418
+ {
419
+ files: [
420
+ { at: 'threads/thread.ts', source: "export const THREAD: Threaded = {\n declares: [\n { id: 'wf_thing', at: 'operations/wf_thing.ts' },\n ],\n}\n" },
421
+ { at: 'operations/wf_thing.ts', source: "export const asked = await fetch('https://example.com')" },
422
+ ],
423
+ at: 'operations/wf_thing.ts',
424
+ },
425
+ "import { PGlite } from '@electric-sql/pglite'\n\nexport const db = new PGlite()",
426
+ {
427
+ files: [
428
+ { at: 'thread/core/thread.ts', source: 'export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n}\n' },
429
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport interface Integration extends Threaded {\n readonly needs?: Readonly<Record<string, string>>\n}\n\nexport const THREAD: Threaded = {\n declares: [\n { id: 'wf_pad', at: 'operations/wf_pad.ts' },\n ],\n}\n\nexport const INTEGRATIONS: readonly Integration[] = [\n { id: 'in_pad', describes: 'Pads text.', needs: { 'left-pad': '^1.3.0' } },\n]\n" },
430
+ { at: 'operations/wf_pad.ts', source: "import pad from 'left-pad'\n\nexport const padded = pad('a', 3)\n" },
431
+ ],
432
+ at: 'operations/wf_pad.ts',
433
+ },
80
434
  ],
81
435
  right: [
82
- "const node = { id: 'st_charge', kind: 'action', action: 'payment.charge', declares: { seams: ['http'] } }",
83
- 'const rows = useRead(recentOrders)',
84
- "const { data } = useRead(recentOrders, { refresh: 'every-5s' })",
436
+ 'export const began = Date.now()',
437
+ 'export const later = setTimeout(() => again(), 5000)',
438
+ "import { randomBytes } from 'node:crypto'\n\nexport const nonce = randomBytes(6).toString('hex')",
439
+ "import { join } from 'node:path'\n\nexport const at = join('a', 'b')",
440
+ "import { createHash } from 'node:crypto'\n\nexport const sum = createHash('sha256').update('a').digest('hex')",
441
+ 'export const when = new Date(1767225600000)',
442
+ {
443
+ files: [
444
+ { at: 'thread/operations/operations.ts', source: 'export interface Http {\n call(address: string): Promise<void>\n}\n\nexport type Opened<R extends string = never> = { readonly [K in R]: Http }\n' },
445
+ { at: 'threads/_probe_door.ts', source: "import type { Http } from '../thread/operations/operations.ts'\n\nexport const made: Http = {\n call: async (address) => {\n await fetch(address)\n },\n}\n" },
446
+ ],
447
+ at: 'threads/_probe_door.ts',
448
+ },
449
+ {
450
+ files: [
451
+ { at: 'thread/operations/operations.ts', source: 'export interface Store {\n insert(row: unknown): Promise<void>\n}\n\nexport type Opened<R extends string = never> = { readonly [K in R]: Store }\n' },
452
+ { at: 'threads/_probe_handed.ts', source: "import type { Opened } from '../thread/operations/operations.ts'\n\nexport const carried = (opened: Opened<'pg_main'>, run: (given: Opened<'pg_main'>) => Promise<void>): Promise<void> => run(opened)\n" },
453
+ ],
454
+ at: 'threads/_probe_handed.ts',
455
+ },
456
+ {
457
+ files: [
458
+ { at: 'thread/core/thread.ts', source: 'export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n}\n' },
459
+ { at: 'thread/operations/operations.ts', source: 'export type Calling = { readonly [call: string]: (given?: unknown) => Promise<unknown> }\n\nexport type Opened<R extends string = never> = { readonly [K in R]: Calling }\n' },
460
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport interface Integration extends Threaded {\n readonly needs?: Readonly<Record<string, string>>\n}\n\nexport const THREAD: Threaded = {\n declares: [\n { id: 'sdk_pad_calls', at: 'src/sdk/sdk_pad.ts' },\n ],\n}\n\nexport const INTEGRATIONS: readonly Integration[] = [\n { id: 'in_pad', describes: 'Pads text.', needs: { 'left-pad': '^1.3.0' } },\n]\n" },
461
+ { at: 'src/sdk/sdk_pad.ts', source: "import type { Calling } from '../../thread/operations/operations.ts'\nimport pad from 'left-pad'\n\nexport const calls: Calling = {\n padded: async (given) => pad(String(given), 3),\n}\n" },
462
+ ],
463
+ at: 'src/sdk/sdk_pad.ts',
464
+ },
85
465
  ],
86
466
  },
87
467
  }
468
+
469
+
470
+
471
+
@@ -1,7 +1,6 @@
1
+ import { descend, read, reached } from '../../core/descend.js';
2
+ import { rowsIn } from './sl_unthreaded.js';
1
3
  const HREF = 'href';
2
- const GLOBALS = new Set(['window', 'globalThis', 'self']);
3
- const HISTORIES = new Set(['router', 'navigation', 'history']);
4
- const MOVES = new Set(['push', 'replace', 'prefetch']);
5
4
  const named = (held) => {
6
5
  if (held?.type === 'Identifier' || held?.type === 'JSXIdentifier')
7
6
  return typeof held.name === 'string' ? held.name : undefined;
@@ -16,41 +15,76 @@ const spoken = (held) => {
16
15
  return held.computed === true ? undefined : named(held.key);
17
16
  return undefined;
18
17
  };
19
- function moving(held) {
20
- if (held.type !== 'MemberExpression' || held.computed === true)
21
- return undefined;
22
- const on = named(held.object);
23
- const reached = named(held.property);
24
- if (on === undefined || reached === undefined)
25
- return undefined;
26
- if (GLOBALS.has(on) && reached === 'location')
27
- return `${on}.location`;
28
- if (HISTORIES.has(on) && MOVES.has(reached))
29
- return `${on}.${reached}()`;
30
- return undefined;
18
+ const SCREEN = 'screen';
19
+ const PASCAL = (id) => id
20
+ .split(/[-_]/)
21
+ .map((part) => `${part.charAt(0).toUpperCase()}${part.slice(1)}`)
22
+ .join('');
23
+ const nameOf = (at) => PASCAL((at.split('\\').join('/').split('/').at(-1) ?? at).replace(/^scr_/, '').replace(/\.[^.]+$/, ''));
24
+ const routeFor = (id) => `/${id}`;
25
+ function addressed() {
26
+ const found = new Map();
27
+ descend();
28
+ for (const at of reached()) {
29
+ const text = read(at);
30
+ if (text === undefined)
31
+ continue;
32
+ let waiting = [];
33
+ for (const one of rowsIn(text)) {
34
+ if (one.id === undefined)
35
+ continue;
36
+ if (one.list === 'declares') {
37
+ if (one.at !== undefined)
38
+ waiting.push({ id: one.id, at: one.at });
39
+ continue;
40
+ }
41
+ if (one.list !== undefined)
42
+ continue;
43
+ if (one.id === SCREEN)
44
+ for (const row of waiting)
45
+ found.set(routeFor(row.id), nameOf(row.at));
46
+ waiting = [];
47
+ }
48
+ }
49
+ return found;
31
50
  }
32
51
  export const slopnator = {
33
- on: ['JSXAttribute', 'Property', 'MemberExpression'],
52
+ on: ['JSXAttribute', 'Property', 'Literal'],
34
53
  holds: (node) => {
35
54
  const held = node;
36
- const written = spoken(held) === HREF ? '`href`' : moving(held);
37
- if (written === undefined)
55
+ if (spoken(held) === HREF) {
56
+ return ('`href` moves somebody without naming a screen, so the map holds no link for it — ' +
57
+ 'the destination reads as unreachable to the orphan pass, the app reads as having no path to it, and it navigates correctly');
58
+ }
59
+ if (held.type !== 'Literal' || typeof held.value !== 'string')
60
+ return false;
61
+ const screen = addressed().get(held.value);
62
+ if (screen === undefined)
38
63
  return false;
39
- return (`${written} moves somebody without naming a screen, so the map holds no link for it — ` +
40
- 'the destination reads as unreachable to the orphan pass, the app reads as having no path to it, and it navigates correctly');
64
+ return ('this writes the address of ' + screen + ' rather than naming it, so the map holds no link for it — ' +
65
+ 'the destination reads as unreachable to the orphan pass and the app navigates there anyway. Name the screen: navigateTo="' + screen + '"');
41
66
  },
42
67
  proves: {
43
68
  wrong: [
44
69
  "const link = { href: '/orders/42' }",
45
- "window.location = '/orders/42'",
46
- "router.push('/orders/42')",
47
- 'history.replace(next)',
48
- 'navigation.prefetch(next)',
70
+ '<a href="/orders/42">{label}</a>',
71
+ {
72
+ files: [
73
+ { at: 'thread/core/thread.ts', source: "export interface Threaded {\n readonly id?: string\n readonly at?: string\n readonly describes?: string\n readonly declares?: readonly Threaded[]\n readonly derives?: readonly Threaded[]\n readonly seeds?: readonly Threaded[]\n}\n" },
74
+ { at: 'threads/thread.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const THREAD: Threaded = {\n declares: [\n {\n at: 'threads/_probe_screens.ts',\n },\n ],\n}\n" },
75
+ { at: 'threads/_probe_screens.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const SCREENS: Threaded = {\n id: 'screen',\n declares: [\n {\n id: 'orders',\n at: 'screens/scr_orders.tsx',\n },\n ],\n}\n" },
76
+ { at: 'app/_probe_address.tsx', source: "export const HELD = router.push('/orders')\n" },
77
+ ],
78
+ at: 'app/_probe_address.tsx',
79
+ },
49
80
  ],
50
81
  right: [
51
- '<Link.label navigateTo="Home" labelKey="home.title" />',
82
+ 'const path = router.pathname',
83
+ '<Link.linkInline navigateTo="Home" labelKey="home.title" />',
52
84
  "const to = { navigateTo: 'Order', labelKey: 'order.title' }",
53
85
  'const rows = orders.push(one)',
86
+ "const parts = one.split('/')",
87
+ "const at = '/api/webhook'",
54
88
  ],
55
89
  },
56
90
  };