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