@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
@@ -3,6 +3,7 @@ import { dirname, join, normalize } from 'node:path'
3
3
  import { parser } from 'typescript-eslint'
4
4
 
5
5
  import { descend } from '../../core/descend.ts'
6
+ import { PREFIXES } from '../../core/thread.ts'
6
7
 
7
8
  import type { Slopnator } from '../slopnator.ts'
8
9
  import { here, index, parses, unreadable } from './sl_unthreaded.ts'
@@ -11,15 +12,8 @@ const RELATIVE = /^\.\.?\//
11
12
 
12
13
  const UNDER = 'operations/'
13
14
 
14
- const NODES = 'nodes'
15
+ const COMPILES = 'thread compile'
15
16
 
16
- /*
17
- * The one thing an operation may be handed to.
18
- *
19
- * It is still `dispatch` and it is still the only one. What moved 2026-08-28 is where it lives:
20
- * it was imported from this framework, and it is now the app's own `operations/_dispatch.ts`,
21
- * written by `thread compile`. The name a caller writes did not change, so neither did this.
22
- */
23
17
  const INVOKER = 'dispatch'
24
18
 
25
19
  const slash = (one: string): string => one.split('\\').join('/')
@@ -34,49 +28,24 @@ interface Held {
34
28
  readonly quasis?: readonly { readonly value?: { readonly cooked?: string; readonly raw?: string } }[]
35
29
  }
36
30
 
37
- /* Where a name is bound, rather than used. An import is how the operation arrives. */
38
- const BINDS = new Set(['ImportSpecifier', 'ImportDefaultSpecifier', 'ImportNamespaceSpecifier'])
31
+ const IMPORTED = 'ImportBinding'
39
32
 
40
- /*
41
- * Where a name is a shape, not a thing.
42
- *
43
- * `const one: typeof operation` runs nothing and reaches nothing — it borrows the type and is erased
44
- * before anything executes. Refusing it was a false positive found by reading, not by running: the
45
- * probe written to test the reassignment form used exactly this line to type its own variable, and
46
- * the law would have refused the probe for its type annotation rather than for the bypass under it.
47
- */
48
- const TYPED = new Set(['TSTypeQuery', 'TSTypeReference', 'TSQualifiedName', 'TSImportType'])
33
+ const TYPE_ONLY = 'type'
34
+
35
+ const QUERIED = 'TSTypeQuery'
49
36
 
50
37
  let bodies: ReadonlySet<string> | undefined
51
38
 
52
- /*
53
- * Which files are operation bodies, asked of the tree rather than guessed from a directory.
54
- *
55
- * An operation body is a file a row beneath `operations` DERIVES and that is not a node's body, and
56
- * the descent already knows every one of them. Reading the directory instead was close enough while
57
- * an operation was the only thing living there — and stopped being close enough the moment an
58
- * operation was folded into a body plus the steps it is woven from: the woven body imports its own
59
- * steps, the steps sat under `operations/`, and this law refused the one file it exists to protect
60
- * for importing the one file it is made of. A rule that refuses the sanctioned form is deciding
61
- * nothing, which is the same failure this law was rewritten for on 2026-08-25.
62
- */
63
39
  function operations(): ReadonlySet<string> {
64
40
  if (bodies !== undefined) return bodies
65
- const pass = descend()
66
-
67
- const under = (id: string, root: string, seen: Set<string>): boolean => {
68
- if (id === root) return true
69
- if (seen.has(id)) return false
70
- seen.add(id)
71
- return (pass.coined.get(id)?.from ?? []).some((one) => under(one, root, seen))
72
- }
73
41
 
42
+ const prefixed = Object.values(PREFIXES)
74
43
  const found = new Set<string>()
75
- for (const [at, threadings] of pass.threaded) {
44
+ for (const [at, threadings] of descend().threaded) {
76
45
  for (const one of threadings) {
77
46
  if (one.list !== 'derives') continue
78
- if (!under(one.by, 'operations', new Set())) continue
79
- if (under(one.by, NODES, new Set())) continue
47
+ if (one.row.by !== COMPILES) continue
48
+ if (!prefixed.some((each) => one.by.startsWith(each))) continue
80
49
  found.add(at)
81
50
  }
82
51
  }
@@ -84,31 +53,35 @@ function operations(): ReadonlySet<string> {
84
53
  return found
85
54
  }
86
55
 
87
- /*
88
- * Whether a path is an operation body.
89
- *
90
- * The tree decides for every file the tree reaches. For a file it does not reach — a probe, a
91
- * scratch file, anything not yet declared — the shape decides: an operation is seeded as one file
92
- * directly inside `operations/`, so a single segment is a body and a deeper path is something an
93
- * operation is built from. Nothing the tree does not reach can be part of an app anyway; that
94
- * fallback is what makes this law's own proofs mean the same thing in every repository.
95
- */
96
56
  function isBody(path: string): boolean {
97
57
  const known = index().reached
98
58
  if (known.has(path)) return operations().has(path)
99
59
  if (!path.startsWith(UNDER)) return false
100
- return !path.slice(UNDER.length).includes('/')
60
+ const file = path.slice(UNDER.length)
61
+ return !file.includes('/') && Object.values(PREFIXES).some((one) => file.startsWith(one))
101
62
  }
102
63
 
103
- const TRIED = ['', '.ts', '.tsx']
64
+ let endings: ReadonlyMap<string, string> | undefined
65
+
66
+ function spellings(): ReadonlyMap<string, string> {
67
+ if (endings !== undefined) return endings
68
+ const found = new Map<string, string>()
69
+ for (const one of index().reached) {
70
+ const dot = one.lastIndexOf('.')
71
+ if (dot > one.lastIndexOf('/')) found.set(one.slice(0, dot), one)
72
+ }
73
+ endings = found
74
+ return found
75
+ }
104
76
 
105
77
  function reaches(said: string, from: string): boolean {
106
78
  if (said === '') return false
107
79
  const at = slash(normalize(RELATIVE.test(said) ? join(from, said) : said))
108
- return TRIED.some((one) => isBody(`${at}${one}`))
80
+ if (isBody(at)) return true
81
+ const whole = spellings().get(at)
82
+ return whole !== undefined && isBody(whole)
109
83
  }
110
84
 
111
- /* Whatever a run-time reach spells out, whole or in pieces. */
112
85
  function spelled(node: Held | undefined, said: string[]): void {
113
86
  if (node === undefined || node === null || typeof node !== 'object') return
114
87
  if (node.type === 'Literal' && typeof node.value === 'string') said.push(node.value)
@@ -122,59 +95,43 @@ function spelled(node: Held | undefined, said: string[]): void {
122
95
  }
123
96
  }
124
97
 
125
- const IMPORTS = /import\s+((?:type\s+)?[^'"]*?)\s+from\s+['"]([^'"]+)['"]/g
126
-
127
- const LOCAL = /(?:^|[,{]|\s)(?:[A-Za-z_$][\w$]*\s+as\s+)?([A-Za-z_$][\w$]*)/g
128
-
129
- /*
130
- * `dispatch` IS NOT AN OPERATION, AND IT LIVES WHERE THE OPERATIONS LIVE.
131
- *
132
- * `bound` below calls every name imported from under the operations directory an operation, which is
133
- * right for every file there but one: an app's own dispatch is written to `operations/_dispatch.ts`,
134
- * so a file importing `dispatch` from beside its operations had `dispatch` itself added to the set —
135
- * and then this law refused that file for doing something with an operation other than handing it to
136
- * dispatch. The thing it was accused of mishandling was the handler.
137
- *
138
- * It had never surfaced because nothing imported dispatch from inside that directory. Measured
139
- * 2026-08-30, when the generated client gained a binding that calls the app's own dispatch for an
140
- * operation needing no server, and the app refused a file its own generator had just written.
141
- *
142
- * The other three below are not names at all — they are the words of an import clause that the
143
- * regex cannot tell from an identifier. This one is a real binding that is really imported and
144
- * really is not an operation, which is the same answer for a different reason.
145
- */
146
- const NOT_A_NAME = new Set(['type', 'as', 'from', 'import', 'dispatch'])
147
-
148
- /*
149
- * The names this file bound by importing an operation body.
150
- *
151
- * A type-only import binds nothing that can be run and is skipped: `import type { Operation }` names
152
- * a shape, and a shape cannot be dispatched or reached past.
153
- */
154
- function bound(text: string, from: string): ReadonlySet<string> {
155
- const found = new Set<string>()
156
- for (const one of text.matchAll(IMPORTS)) {
157
- const clause = one[1] ?? ''
158
- if (/^type\s/.test(clause)) continue
159
- if (!reaches(one[2] ?? '', from)) continue
160
- for (const name of clause.matchAll(LOCAL)) {
161
- const held = name[1]
162
- if (held !== undefined && !NOT_A_NAME.has(held)) found.add(held)
98
+ interface Bound {
99
+ readonly type?: string
100
+ readonly node?: { readonly importKind?: string }
101
+ readonly parent?: { readonly importKind?: string; readonly source?: { readonly value?: unknown } }
102
+ }
103
+
104
+ interface Named {
105
+ readonly defs?: readonly Bound[]
106
+ readonly references?: readonly { readonly identifier?: unknown; readonly isValueReference?: boolean }[]
107
+ }
108
+
109
+ interface Scoped {
110
+ readonly scopes?: readonly { readonly variables?: readonly Named[] }[]
111
+ }
112
+
113
+ interface Parsed {
114
+ readonly ast: unknown
115
+ readonly scopeManager?: Scoped
116
+ }
117
+
118
+ function handled(scopes: Scoped | undefined, from: string): ReadonlySet<unknown> {
119
+ const found = new Set<unknown>()
120
+ for (const scope of scopes?.scopes ?? []) {
121
+ for (const one of scope.variables ?? []) {
122
+ const made = (one.defs ?? [])[0]
123
+ if (made?.type !== IMPORTED) continue
124
+ if (made.node?.importKind === TYPE_ONLY || made.parent?.importKind === TYPE_ONLY) continue
125
+ const said = made.parent?.source?.value
126
+ if (typeof said !== 'string' || !reaches(said, from)) continue
127
+ for (const seen of one.references ?? []) {
128
+ if (seen.isValueReference !== false && seen.identifier !== undefined) found.add(seen.identifier)
129
+ }
163
130
  }
164
131
  }
165
132
  return found
166
133
  }
167
134
 
168
- /*
169
- * Parsed here rather than taken from the visitor.
170
- *
171
- * `holds` is handed a node when a law runs as an eslint rule and nothing at all when the sweep runs
172
- * it over a file — measured 2026-08-25, the sweep calls it once per file with `node` undefined, so a
173
- * reading that only inspected what it was given threw on the first file it saw. What a file DOES
174
- * with a binding is a question about the whole file anyway, not about one node in it.
175
- *
176
- * The parent comes down with the node because the whole rule is about where a name appears.
177
- */
178
135
  function walked(
179
136
  node: unknown,
180
137
  up: Held | undefined,
@@ -211,42 +168,23 @@ export const slopnator: Slopnator = {
211
168
  if (!text.includes(UNDER)) return false
212
169
 
213
170
  const from = dirname(path)
214
- const mine = bound(text, from)
215
171
 
216
- let ast: unknown
172
+ let parsed: Parsed
217
173
  try {
218
- ast = (parser.parseForESLint as (t: string, o: unknown) => { ast: unknown })(text, {
174
+ parsed = (parser.parseForESLint as (t: string, o: unknown) => Parsed)(text, {
219
175
  range: true,
220
176
  ecmaVersion: 'latest',
221
177
  sourceType: 'module',
222
178
  ecmaFeatures: { jsx: true },
223
- }).ast
179
+ })
224
180
  } catch {
225
181
  return false
226
182
  }
227
183
 
228
- /*
229
- * One rule, stated once.
230
- *
231
- * The first reading of this enumerated the SHAPES of a bypass — a member access, a call, an
232
- * argument — and every shape it did not think of walked through: an alias, a destructure, an
233
- * array holding the name, a comma operator. Enumerating forms is how a refusal ends up with a
234
- * list of everything somebody happened to try. The rule is not a list. An operation goes to
235
- * `dispatch` and nowhere else, so the name may appear where it is bound and as an argument to
236
- * `dispatch`, and every other appearance is the finding — whatever it happens to look like.
237
- */
184
+ const mine = handled(parsed.scopeManager, from)
185
+
238
186
  return (
239
- walked(ast, undefined, (held, parent) => {
240
- /*
241
- * A body reached at run time never binds a name at all, so the rule above cannot see it.
242
- *
243
- * What is read is everything the reach spells out, whole or in pieces: a plain literal, a
244
- * concatenation, a template. Measured 2026-08-26 against the reading this replaces, which
245
- * only ever inspected a plain `Literal` — `await import('../operations/' + name + '.ts')`
246
- * passed it without a word, which is the exact form its own comment said it was written to
247
- * catch. Nothing legitimate needs any of it: dispatch is handed the operation, and the one
248
- * file that hands it over imports it statically.
249
- */
187
+ walked(parsed.ast, undefined, (held, parent) => {
250
188
  const dynamic =
251
189
  held.type === 'ImportExpression'
252
190
  ? held.source
@@ -266,9 +204,8 @@ export const slopnator: Slopnator = {
266
204
  }
267
205
  }
268
206
 
269
- if (held.type !== 'Identifier' || typeof held.name !== 'string' || !mine.has(held.name)) return undefined
270
- const up = parent?.type ?? ''
271
- if (BINDS.has(up) || TYPED.has(up)) return undefined
207
+ if (held.type !== 'Identifier' || typeof held.name !== 'string' || !mine.has(held)) return undefined
208
+ if (parent?.type === QUERIED) return undefined
272
209
  if (
273
210
  parent?.type === 'CallExpression' &&
274
211
  parent.callee?.type === 'Identifier' &&
@@ -286,23 +223,10 @@ export const slopnator: Slopnator = {
286
223
  }) ?? false
287
224
  )
288
225
  },
289
- /*
290
- * These were driven at `holds` directly, because the trial harness cannot run them.
291
- *
292
- * A shipped law is meant to be proved by `thread trials`, and today nothing proves one anywhere:
293
- * the harness cannot run in this repository at all — the framework has no tree, and the command
294
- * that once tried was removed in 6e98a6d after it was measured exiting 1 — and in an app
295
- * `thread trials` answers "0 trials from the 0 rule(s) this tree declares",
296
- * because a shipped constraint is proved in the package that ships it. So every proof below was
297
- * written to a real probe file and handed to `slopnator.holds` by hand, 2026-08-26: fourteen
298
- * refused, six accepted, no failures. That is a stand-in for the harness and it is written down
299
- * here rather than only in a report, because a proof nobody can run is indistinguishable from a
300
- * proof that passes, and the next person to read this list deserves to know which one it is.
301
- */
302
226
  proves: {
303
227
  wrong: [
304
228
  "import { operation } from './operations/wf_send_invoice.ts'\n\nawait operation.run(input)",
305
- "import { operation } from './operations/wf_send_invoice.ts'\n\nconst { run } = operation\n\nawait run(hands, held)",
229
+ "import { operation } from './operations/wf_send_invoice.ts'\n\nconst { run } = operation\n\nawait run(opened, held)",
306
230
  "import { operation } from './operations/wf_send_invoice.ts'\n\nawait operation(input)",
307
231
  "import { operation } from './operations/wf_send_invoice.ts'\n\nawait somewhere(operation, input)",
308
232
  "import { operation } from './operations/wf_send_invoice.ts'\n\nconst one = operation\n\nawait one.run(input)",
@@ -317,10 +241,34 @@ export const slopnator: Slopnator = {
317
241
  'const held = await import(`../operations/${name}.ts`)',
318
242
  ],
319
243
  right: [
320
- "import { dispatch } from './operations/_dispatch.ts'\n\nimport { operation } from './operations/wf_send_invoice.ts'\n\nawait dispatch(operation, input, actor, seams)",
244
+ {
245
+ files: [
246
+ { 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" },
247
+ { 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_operations.ts',\n },\n ],\n}\n" },
248
+ { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_send_invoice',\n describes: 'sends an invoice',\n derives: [\n {\n id: 'wf_send_invoice_body',\n at: 'operations/wf_send_invoice.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
249
+ { at: 'probe.tsx', source: "import { dispatch } from './operations/_dispatch.ts'\n\nimport { operation } from './operations/wf_send_invoice.ts'\n\nawait dispatch(operation, input, actor, opened)\n" },
250
+ ],
251
+ at: 'probe.tsx',
252
+ },
321
253
  "import type { Operation } from '../../operations/operations.ts'\n\nexport const held: Operation = one",
322
- "import { dispatch } from './operations/_dispatch.ts'\n\nawait dispatch(one, input, actor, seams)",
323
- "import { dispatch } from './operations/_dispatch.ts'\n\nimport { operation } from './operations/wf_send_invoice.ts'\n\nlet one: typeof operation\n\nawait dispatch(operation, input, actor, seams)",
254
+ {
255
+ files: [
256
+ { 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" },
257
+ { 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_operations.ts',\n },\n ],\n}\n" },
258
+ { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_send_invoice',\n describes: 'sends an invoice',\n derives: [\n {\n id: 'wf_send_invoice_body',\n at: 'operations/wf_send_invoice.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
259
+ { at: 'probe.tsx', source: "import { dispatch } from './operations/_dispatch.ts'\n\nawait dispatch(wf_send_invoice_body, input, actor, opened)\n" },
260
+ ],
261
+ at: 'probe.tsx',
262
+ },
263
+ {
264
+ files: [
265
+ { 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" },
266
+ { 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_operations.ts',\n },\n ],\n}\n" },
267
+ { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_send_invoice',\n describes: 'sends an invoice',\n derives: [\n {\n id: 'wf_send_invoice_body',\n at: 'operations/wf_send_invoice.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
268
+ { at: 'probe.tsx', source: "import { dispatch } from './operations/_dispatch.ts'\n\nimport { operation } from './operations/wf_send_invoice.ts'\n\nlet one: typeof operation\n\nawait dispatch(operation, input, actor, opened)\n" },
269
+ ],
270
+ at: 'probe.tsx',
271
+ },
324
272
  "import * as steps from './operations/wf_send_invoice/steps.ts'\n\nexport const held = steps.reads_the_invoice",
325
273
  "import * as steps from './steps/wf_send_invoice.ts'\n\nexport const held = steps.reads_the_invoice",
326
274
  ],
@@ -1,12 +1,16 @@
1
- import { existsSync, readFileSync } from "../../core/files.js";
1
+ import { existsSync, readFileSync } from '../../integrations/disk.js';
2
2
  import { dirname, resolve } from 'node:path';
3
- import { findTree } from "../../core/tree.js";
3
+ import { findTree } from '../../core/tree.js';
4
4
  const ROOT = findTree().at;
5
- const IMPORTS = /(?:^|\n)\s*import\s[^'"]*['"]([^'"]+)['"]/g;
5
+ const IMPORTS = /(?:^|\n)\s*import\s+(?!type\s)[^'"]*['"]([^'"]+)['"]/g;
6
6
  const target = (from, spec) => spec.startsWith('.') ? resolve(dirname(from), spec) : undefined;
7
+ const erased = (node) => node.importKind === 'type' ||
8
+ ((node.specifiers ?? []).length > 0 && (node.specifiers ?? []).every((one) => one.importKind === 'type'));
7
9
  export const slopnator = {
8
10
  on: ['ImportDeclaration'],
9
11
  holds: (node, at) => {
12
+ if (erased(node))
13
+ return false;
10
14
  const spec = node.source?.value;
11
15
  if (typeof spec !== 'string')
12
16
  return false;
@@ -34,9 +38,16 @@ export const slopnator = {
34
38
  right: [
35
39
  "import type { Law } from '../../core/generators/meta.ts'",
36
40
  "import { SLUG, type Shaped } from '../../core/types.ts'",
37
- "import { existsSync } from 'node:fs'",
41
+ "import { join } from 'node:path'",
38
42
  "import type { Rule } from 'eslint'",
39
43
  "import { CONSTRAINTS } from '../constraint.ts'",
44
+ {
45
+ files: [
46
+ { at: 'threads/_probe_three.ts', source: "import type { Four } from './_probe_four.ts'\nexport const three: Four = 1\n" },
47
+ { at: 'threads/_probe_four.ts', source: "import { three } from './_probe_three.ts'\nexport type Four = number\nexport const four = three\n" },
48
+ ],
49
+ at: 'threads/_probe_three.ts',
50
+ },
40
51
  ],
41
52
  },
42
53
  };
@@ -1,4 +1,4 @@
1
- import { existsSync, readFileSync } from '../../core/files.ts'
1
+ import { existsSync, readFileSync } from '../../integrations/disk.ts'
2
2
  import { dirname, resolve } from 'node:path'
3
3
 
4
4
  import type { Slopnator } from '../slopnator.ts'
@@ -6,16 +6,25 @@ import { findTree } from '../../core/tree.ts'
6
6
 
7
7
  const ROOT = findTree().at
8
8
 
9
- const IMPORTS = /(?:^|\n)\s*import\s[^'"]*['"]([^'"]+)['"]/g
9
+ const IMPORTS = /(?:^|\n)\s*import\s+(?!type\s)[^'"]*['"]([^'"]+)['"]/g
10
10
 
11
- type Node = { source?: { value?: unknown } }
11
+ type Node = {
12
+ source?: { value?: unknown }
13
+ importKind?: string
14
+ specifiers?: readonly { importKind?: string }[]
15
+ }
12
16
 
13
17
  const target = (from: string, spec: string): string | undefined =>
14
18
  spec.startsWith('.') ? resolve(dirname(from), spec) : undefined
15
19
 
20
+ const erased = (node: Node): boolean =>
21
+ node.importKind === 'type' ||
22
+ ((node.specifiers ?? []).length > 0 && (node.specifiers ?? []).every((one) => one.importKind === 'type'))
23
+
16
24
  export const slopnator: Slopnator = {
17
25
  on: ['ImportDeclaration'],
18
26
  holds: (node, at) => {
27
+ if (erased(node as Node)) return false
19
28
  const spec = (node as Node).source?.value
20
29
  if (typeof spec !== 'string') return false
21
30
 
@@ -42,9 +51,16 @@ export const slopnator: Slopnator = {
42
51
  right: [
43
52
  "import type { Law } from '../../core/generators/meta.ts'",
44
53
  "import { SLUG, type Shaped } from '../../core/types.ts'",
45
- "import { existsSync } from 'node:fs'",
54
+ "import { join } from 'node:path'",
46
55
  "import type { Rule } from 'eslint'",
47
56
  "import { CONSTRAINTS } from '../constraint.ts'",
57
+ {
58
+ files: [
59
+ { at: 'threads/_probe_three.ts', source: "import type { Four } from './_probe_four.ts'\nexport const three: Four = 1\n" },
60
+ { at: 'threads/_probe_four.ts', source: "import { three } from './_probe_three.ts'\nexport type Four = number\nexport const four = three\n" },
61
+ ],
62
+ at: 'threads/_probe_three.ts',
63
+ },
48
64
  ],
49
65
  },
50
66
  }
@@ -1,6 +1,6 @@
1
- import { readFileSync } from "../../core/files.js";
1
+ import { readFileSync } from '../../integrations/disk.js';
2
2
  import { resolve } from 'node:path';
3
- import { findTree } from "../../core/tree.js";
3
+ import { findTree } from '../../core/tree.js';
4
4
  const ROOT = findTree().at;
5
5
  const keyOf = (property) => {
6
6
  if (property.computed === true)
@@ -1,4 +1,4 @@
1
- import { readFileSync } from '../../core/files.ts'
1
+ import { readFileSync } from '../../integrations/disk.ts'
2
2
  import { resolve } from 'node:path'
3
3
 
4
4
  import type { Sourced } from '../constraint.ts'
@@ -0,0 +1,20 @@
1
+ import { HANDED_TO, judging } from './sl_undeclared_crossing.js';
2
+ const SAYS = 'Each of these uses what this app hands an operation, and an operation is the only thing handed one.';
3
+ export const slopnator = {
4
+ holds: (_node, at) => judging(at, HANDED_TO, SAYS),
5
+ proves: {
6
+ wrong: [
7
+ {
8
+ files: [
9
+ { 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' },
10
+ { at: 'threads/_probe_reach.ts', source: "import type { Store } from '../thread/operations/operations.ts'\n\nexport const seeded = async (kept: Store): Promise<void> => {\n await kept['insert']({})\n}\n" },
11
+ ],
12
+ at: 'threads/_probe_reach.ts',
13
+ },
14
+ ],
15
+ right: [
16
+ 'export const began = Date.now()',
17
+ "import { join } from 'node:path'\n\nexport const at = join('a', 'b')",
18
+ ],
19
+ },
20
+ };
@@ -0,0 +1,25 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+
3
+ import { HANDED_TO, judging } from './sl_undeclared_crossing.ts'
4
+
5
+ const SAYS =
6
+ 'Each of these uses what this app hands an operation, and an operation is the only thing handed one.'
7
+
8
+ export const slopnator: Slopnator = {
9
+ holds: (_node, at) => judging(at, HANDED_TO, SAYS),
10
+ proves: {
11
+ wrong: [
12
+ {
13
+ files: [
14
+ { 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' },
15
+ { at: 'threads/_probe_reach.ts', source: "import type { Store } from '../thread/operations/operations.ts'\n\nexport const seeded = async (kept: Store): Promise<void> => {\n await kept['insert']({})\n}\n" },
16
+ ],
17
+ at: 'threads/_probe_reach.ts',
18
+ },
19
+ ],
20
+ right: [
21
+ 'export const began = Date.now()',
22
+ "import { join } from 'node:path'\n\nexport const at = join('a', 'b')",
23
+ ],
24
+ },
25
+ }
@@ -0,0 +1,2 @@
1
+ import type { Slopnator } from '../slopnator.ts';
2
+ export declare const slopnator: Slopnator;
@@ -0,0 +1,61 @@
1
+ import { descend, read, reached } from '../../core/descend.js';
2
+ import { rowsIn } from './sl_unthreaded.js';
3
+ const WRAPS = 'screen';
4
+ const HANDS = 'dispatch';
5
+ const ELEMENT = 'element';
6
+ const COMPOSITION = 'composition';
7
+ const nameOf = (id) => id.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('');
8
+ function declared() {
9
+ const found = new Set();
10
+ descend();
11
+ for (const at of reached()) {
12
+ const text = read(at);
13
+ if (text === undefined)
14
+ continue;
15
+ let waiting = [];
16
+ for (const one of rowsIn(text)) {
17
+ if (one.id === undefined)
18
+ continue;
19
+ if (one.list === 'declares') {
20
+ waiting.push(one.id);
21
+ continue;
22
+ }
23
+ if (one.list !== undefined)
24
+ continue;
25
+ if (one.id === ELEMENT || one.id === COMPOSITION)
26
+ for (const id of waiting)
27
+ found.add(nameOf(id));
28
+ waiting = [];
29
+ }
30
+ }
31
+ return found;
32
+ }
33
+ export const slopnator = {
34
+ on: ['CallExpression'],
35
+ holds: (node) => {
36
+ const held = node;
37
+ const called = held.callee?.type === 'Identifier' ? held.callee.name : undefined;
38
+ if (called === undefined)
39
+ return false;
40
+ if (called === WRAPS || called === HANDS)
41
+ return false;
42
+ if (declared().has(called))
43
+ return false;
44
+ return (called + ' is called here, and a screen is made of declared elements and compositions and may hand an operation to ' +
45
+ HANDS + '. Nothing else has a place in one, so this is work that belongs in an operation');
46
+ },
47
+ proves: {
48
+ wrong: ["export const Orders = screen('orders', {}, () => loadOrders())"],
49
+ right: [
50
+ {
51
+ files: [
52
+ { 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" },
53
+ { 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_operations.ts',\n },\n ],\n}\n" },
54
+ { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_list_orders',\n describes: 'lists the orders',\n derives: [\n {\n id: 'wf_list_orders_body',\n at: 'operations/wf_list_orders.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
55
+ { at: 'screens/probe.tsx', source: "import { dispatch } from '../operations/_dispatch.ts'\n\nimport { operation } from '../operations/wf_list_orders.ts'\n\nexport const Orders = screen('orders', {}, () => dispatch(operation, input, actor, opened))\n" },
56
+ ],
57
+ at: 'screens/probe.tsx',
58
+ },
59
+ ],
60
+ },
61
+ };
@@ -0,0 +1,70 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+
3
+ import { descend, read, reached } from '../../core/descend.ts'
4
+ import { rowsIn } from './sl_unthreaded.ts'
5
+
6
+ const WRAPS = 'screen'
7
+
8
+ const HANDS = 'dispatch'
9
+
10
+ const ELEMENT = 'element'
11
+
12
+ const COMPOSITION = 'composition'
13
+
14
+ const nameOf = (id: string): string =>
15
+ id.split('-').map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join('')
16
+
17
+ function declared(): ReadonlySet<string> {
18
+ const found = new Set<string>()
19
+
20
+ descend()
21
+
22
+ for (const at of reached()) {
23
+ const text = read(at)
24
+ if (text === undefined) continue
25
+
26
+ let waiting: string[] = []
27
+ for (const one of rowsIn(text)) {
28
+ if (one.id === undefined) continue
29
+ if (one.list === 'declares') {
30
+ waiting.push(one.id)
31
+ continue
32
+ }
33
+ if (one.list !== undefined) continue
34
+ if (one.id === ELEMENT || one.id === COMPOSITION) for (const id of waiting) found.add(nameOf(id))
35
+ waiting = []
36
+ }
37
+ }
38
+
39
+ return found
40
+ }
41
+
42
+ export const slopnator: Slopnator = {
43
+ on: ['CallExpression'],
44
+ holds: (node) => {
45
+ const held = node as { readonly callee?: { readonly type?: string; readonly name?: string } }
46
+ const called = held.callee?.type === 'Identifier' ? held.callee.name : undefined
47
+ if (called === undefined) return false
48
+ if (called === WRAPS || called === HANDS) return false
49
+ if (declared().has(called)) return false
50
+
51
+ return (
52
+ called + ' is called here, and a screen is made of declared elements and compositions and may hand an operation to ' +
53
+ HANDS + '. Nothing else has a place in one, so this is work that belongs in an operation'
54
+ )
55
+ },
56
+ proves: {
57
+ wrong: ["export const Orders = screen('orders', {}, () => loadOrders())"],
58
+ right: [
59
+ {
60
+ files: [
61
+ { 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" },
62
+ { 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_operations.ts',\n },\n ],\n}\n" },
63
+ { at: 'threads/_probe_operations.ts', source: "import type { Threaded } from '../thread/core/thread.ts'\n\nexport const OPERATIONS: Threaded = {\n id: 'operations',\n declares: [\n {\n id: 'wf_list_orders',\n describes: 'lists the orders',\n derives: [\n {\n id: 'wf_list_orders_body',\n at: 'operations/wf_list_orders.ts',\n by: 'thread compile',\n },\n ],\n },\n ],\n}\n" },
64
+ { at: 'screens/probe.tsx', source: "import { dispatch } from '../operations/_dispatch.ts'\n\nimport { operation } from '../operations/wf_list_orders.ts'\n\nexport const Orders = screen('orders', {}, () => dispatch(operation, input, actor, opened))\n" },
65
+ ],
66
+ at: 'screens/probe.tsx',
67
+ },
68
+ ],
69
+ },
70
+ }