@prestack/thread 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -0,0 +1,328 @@
1
+ import { dirname, join, normalize } from 'node:path'
2
+
3
+ import { parser } from 'typescript-eslint'
4
+
5
+ import { descend } from '../../core/descend.ts'
6
+
7
+ import type { Slopnator } from '../slopnator.ts'
8
+ import { here, index, parses, unreadable } from './sl_unthreaded.ts'
9
+
10
+ const RELATIVE = /^\.\.?\//
11
+
12
+ const UNDER = 'operations/'
13
+
14
+ const NODES = 'nodes'
15
+
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
+ const INVOKER = 'dispatch'
24
+
25
+ const slash = (one: string): string => one.split('\\').join('/')
26
+
27
+ interface Held {
28
+ readonly type?: string
29
+ readonly name?: string
30
+ readonly value?: unknown
31
+ readonly callee?: Held
32
+ readonly source?: Held
33
+ readonly arguments?: readonly Held[]
34
+ readonly quasis?: readonly { readonly value?: { readonly cooked?: string; readonly raw?: string } }[]
35
+ }
36
+
37
+ /* Where a name is bound, rather than used. An import is how the operation arrives. */
38
+ const BINDS = new Set(['ImportSpecifier', 'ImportDefaultSpecifier', 'ImportNamespaceSpecifier'])
39
+
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'])
49
+
50
+ let bodies: ReadonlySet<string> | undefined
51
+
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
+ function operations(): ReadonlySet<string> {
64
+ 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
+
74
+ const found = new Set<string>()
75
+ for (const [at, threadings] of pass.threaded) {
76
+ for (const one of threadings) {
77
+ if (one.list !== 'derives') continue
78
+ if (!under(one.by, 'operations', new Set())) continue
79
+ if (under(one.by, NODES, new Set())) continue
80
+ found.add(at)
81
+ }
82
+ }
83
+ bodies = found
84
+ return found
85
+ }
86
+
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
+ function isBody(path: string): boolean {
97
+ const known = index().reached
98
+ if (known.has(path)) return operations().has(path)
99
+ if (!path.startsWith(UNDER)) return false
100
+ return !path.slice(UNDER.length).includes('/')
101
+ }
102
+
103
+ const TRIED = ['', '.ts', '.tsx']
104
+
105
+ function reaches(said: string, from: string): boolean {
106
+ if (said === '') return false
107
+ const at = slash(normalize(RELATIVE.test(said) ? join(from, said) : said))
108
+ return TRIED.some((one) => isBody(`${at}${one}`))
109
+ }
110
+
111
+ /* Whatever a run-time reach spells out, whole or in pieces. */
112
+ function spelled(node: Held | undefined, said: string[]): void {
113
+ if (node === undefined || node === null || typeof node !== 'object') return
114
+ if (node.type === 'Literal' && typeof node.value === 'string') said.push(node.value)
115
+ for (const one of node.quasis ?? []) {
116
+ const held = one.value?.cooked ?? one.value?.raw
117
+ if (typeof held === 'string') said.push(held)
118
+ }
119
+ for (const value of Object.values(node as Record<string, unknown>)) {
120
+ if (Array.isArray(value)) for (const one of value) spelled(one as Held, said)
121
+ else if (value !== null && typeof value === 'object') spelled(value as Held, said)
122
+ }
123
+ }
124
+
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)
163
+ }
164
+ }
165
+ return found
166
+ }
167
+
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
+ function walked(
179
+ node: unknown,
180
+ up: Held | undefined,
181
+ seen: (one: Held, parent: Held | undefined) => string | undefined,
182
+ ): string | undefined {
183
+ if (node === null || typeof node !== 'object') return undefined
184
+ if (Array.isArray(node)) {
185
+ for (const one of node) {
186
+ const said = walked(one, up, seen)
187
+ if (said !== undefined) return said
188
+ }
189
+ return undefined
190
+ }
191
+ const held = node as Held
192
+ const real = typeof held.type === 'string'
193
+ if (real) {
194
+ const said = seen(held, up)
195
+ if (said !== undefined) return said
196
+ }
197
+ for (const value of Object.values(node as Record<string, unknown>)) {
198
+ const said = walked(value, real ? held : up, seen)
199
+ if (said !== undefined) return said
200
+ }
201
+ return undefined
202
+ }
203
+
204
+ export const slopnator: Slopnator = {
205
+ holds: (_node, at) => {
206
+ const path = here(at.filename)
207
+ if (unreadable(path, at.text) !== undefined) return false
208
+ if (!parses(path)) return false
209
+
210
+ const text = String(at.text)
211
+ if (!text.includes(UNDER)) return false
212
+
213
+ const from = dirname(path)
214
+ const mine = bound(text, from)
215
+
216
+ let ast: unknown
217
+ try {
218
+ ast = (parser.parseForESLint as (t: string, o: unknown) => { ast: unknown })(text, {
219
+ range: true,
220
+ ecmaVersion: 'latest',
221
+ sourceType: 'module',
222
+ ecmaFeatures: { jsx: true },
223
+ }).ast
224
+ } catch {
225
+ return false
226
+ }
227
+
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
+ */
238
+ 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
+ */
250
+ const dynamic =
251
+ held.type === 'ImportExpression'
252
+ ? held.source
253
+ : held.type === 'CallExpression' && held.callee?.type === 'Identifier' && held.callee.name === 'require'
254
+ ? held.arguments?.[0]
255
+ : undefined
256
+
257
+ if (dynamic !== undefined) {
258
+ const said: string[] = []
259
+ spelled(dynamic, said)
260
+ const reaching = said.find((one) => one.includes(UNDER) || reaches(one, from))
261
+ if (reaching !== undefined) {
262
+ return (
263
+ `this reaches \`${reaching}\` at run time, which points at an operation. An operation is handed ` +
264
+ `to \`${INVOKER}\`, and a file that goes looking for one is looking for a second way in`
265
+ )
266
+ }
267
+ }
268
+
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
272
+ if (
273
+ parent?.type === 'CallExpression' &&
274
+ parent.callee?.type === 'Identifier' &&
275
+ parent.callee.name === INVOKER &&
276
+ (parent.arguments ?? []).includes(held)
277
+ ) {
278
+ return undefined
279
+ }
280
+
281
+ return (
282
+ `\`${held.name}\` is an operation, and this does something with it other than hand it to ` +
283
+ `\`${INVOKER}\`. Every gate that decides whether it may run is in \`${INVOKER}\` — naming it, ` +
284
+ 'aliasing it, taking it apart or passing it on all reach past every one of them'
285
+ )
286
+ }) ?? false
287
+ )
288
+ },
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
+ proves: {
303
+ wrong: [
304
+ "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)",
306
+ "import { operation } from './operations/wf_send_invoice.ts'\n\nawait operation(input)",
307
+ "import { operation } from './operations/wf_send_invoice.ts'\n\nawait somewhere(operation, input)",
308
+ "import { operation } from './operations/wf_send_invoice.ts'\n\nconst one = operation\n\nawait one.run(input)",
309
+ "import { operation } from './operations/wf_send_invoice.ts'\n\nconst { requires } = operation",
310
+ "import { operation } from './operations/wf_send_invoice.ts'\n\nlet one\none = operation\nawait one.run(input)",
311
+ "import { operation } from './operations/wf_send_invoice.ts'\n\nconst [one] = [operation]\n\nawait one.run(input)",
312
+ "import { operation } from './operations/wf_send_invoice.ts'\n\nawait (0, operation).run(input)",
313
+ "import { operation } from './operations/wf_send_invoice.ts'\n\nexport { operation }",
314
+ "const held = await import('./operations/wf_send_invoice.ts')",
315
+ "const held = require('../operations/wf_send_invoice.ts')",
316
+ "const held = await import('../operations/' + name + '.ts')",
317
+ 'const held = await import(`../operations/${name}.ts`)',
318
+ ],
319
+ right: [
320
+ "import { dispatch } from './operations/_dispatch.ts'\n\nimport { operation } from './operations/wf_send_invoice.ts'\n\nawait dispatch(operation, input, actor, seams)",
321
+ "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)",
324
+ "import * as steps from './operations/wf_send_invoice/steps.ts'\n\nexport const held = steps.reads_the_invoice",
325
+ "import * as steps from './steps/wf_send_invoice.ts'\n\nexport const held = steps.reads_the_invoice",
326
+ ],
327
+ },
328
+ }
@@ -0,0 +1,50 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, resolve } from 'node:path'
3
+
4
+ import type { Slopnator } from '../slopnator.ts'
5
+ import { findTree } from '../../core/tree.ts'
6
+
7
+ const ROOT = findTree().at
8
+
9
+ const IMPORTS = /(?:^|\n)\s*import\s[^'"]*['"]([^'"]+)['"]/g
10
+
11
+ type Node = { source?: { value?: unknown } }
12
+
13
+ const target = (from: string, spec: string): string | undefined =>
14
+ spec.startsWith('.') ? resolve(dirname(from), spec) : undefined
15
+
16
+ export const slopnator: Slopnator = {
17
+ on: ['ImportDeclaration'],
18
+ holds: (node, at) => {
19
+ const spec = (node as Node).source?.value
20
+ if (typeof spec !== 'string') return false
21
+
22
+ const here = resolve(ROOT, at.filename)
23
+ const there = target(here, spec)
24
+ if (there === undefined || !existsSync(there)) return false
25
+
26
+ for (const found of readFileSync(there, 'utf8').matchAll(IMPORTS)) {
27
+ const back = found[1] ?? ''
28
+ if (target(there, back) === here) return `${spec} imports \`${back}\` straight back, so the two initialise each other`
29
+ }
30
+ return false
31
+ },
32
+ proves: {
33
+ wrong: [
34
+ {
35
+ files: [
36
+ { at: 'threads/_probe_one.ts', source: "import { two } from './_probe_two.ts'\nexport const one = two\n" },
37
+ { at: 'threads/_probe_two.ts', source: "import { one } from './_probe_one.ts'\nexport const two = one\n" },
38
+ ],
39
+ at: 'threads/_probe_one.ts',
40
+ },
41
+ ],
42
+ right: [
43
+ "import type { Law } from '../../core/generators/meta.ts'",
44
+ "import { SLUG, type Shaped } from '../../core/types.ts'",
45
+ "import { existsSync } from 'node:fs'",
46
+ "import type { Rule } from 'eslint'",
47
+ "import { CONSTRAINTS } from '../constraint.ts'",
48
+ ],
49
+ },
50
+ }
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { resolve } from 'node:path'
3
+
4
+ import type { Slopnator } from '../slopnator.ts'
5
+ import { findTree } from '../../core/tree.ts'
6
+
7
+ const ROOT = findTree().at
8
+
9
+ type Node = {
10
+ type?: string
11
+ key?: { type?: string; name?: string; value?: unknown }
12
+ value?: { type?: string; value?: unknown }
13
+ computed?: boolean
14
+ }
15
+
16
+ const keyOf = (property: Node): string | undefined => {
17
+ if (property.computed === true) return undefined
18
+ const key = property.key
19
+ if (key?.type === 'Identifier') return key.name
20
+ if (key?.type === 'Literal' && typeof key.value === 'string') return key.value
21
+ return undefined
22
+ }
23
+
24
+ function scripts(): ReadonlySet<string> {
25
+ try {
26
+ const held = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as { scripts?: Record<string, string> }
27
+ return new Set(Object.keys(held.scripts ?? {}))
28
+ } catch {
29
+ return new Set()
30
+ }
31
+ }
32
+
33
+ export const slopnator: Slopnator = {
34
+ on: ['Property'],
35
+ holds: (node) => {
36
+ const property = node as Node
37
+ const key = keyOf(property)
38
+ if (key !== 'runs' && key !== 'by') return false
39
+
40
+ const held = property.value
41
+ if (held?.type !== 'Literal' || typeof held.value !== 'string') return false
42
+
43
+ const spoken = held.value.trim()
44
+ if (!spoken.startsWith('pnpm ')) return false
45
+
46
+ const named = spoken.replace(/^pnpm (run )?/, '').split(/\s+/)[0] ?? ''
47
+ const known = scripts()
48
+ if (known.size === 0 || known.has(named)) return false
49
+
50
+ return `\`${key}: ${spoken}\` names the script \`${named}\`, and package.json has no such script`
51
+ },
52
+ proves: {
53
+ wrong: [
54
+ "const M = {\n runs: 'pnpm nope:check',\n}",
55
+ "const M = {\n by: 'pnpm also-not-a-script',\n}",
56
+ ],
57
+ right: [
58
+ "const M = {\n runs: 'pnpm typecheck',\n}",
59
+ "const M = {\n by: 'pnpm gate:generate',\n}",
60
+ "const M = {\n runs: 'node scripts/_generated/gate.mjs',\n}",
61
+ "const M = {\n describes: 'A sentence mentioning pnpm nope:check without naming it as a command.',\n}",
62
+ ],
63
+ },
64
+ }
@@ -0,0 +1,63 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+
3
+ interface Node {
4
+ readonly type?: string
5
+ readonly callee?: { readonly type?: string; readonly name?: string }
6
+ readonly arguments?: readonly { readonly type?: string; readonly elements?: readonly { readonly type?: string }[] }[]
7
+ }
8
+
9
+ export const slopnator: Slopnator = {
10
+ on: ['CallExpression'],
11
+ holds: (node) => {
12
+ const held = node as Node
13
+ if (held.callee?.type !== 'Identifier' || held.callee.name !== 'only') return false
14
+ if (held.arguments?.length !== 3) return false
15
+ const keys = held.arguments[1]
16
+ if (keys?.type !== 'ArrayExpression') return false
17
+ const typed = (keys.elements ?? []).filter((one) => one?.type !== 'SpreadElement')
18
+ if (typed.length === 0) return false
19
+ return `it hands \`only\` ${String(typed.length)} key(s) written here, and the receipt prints its questions from somewhere else`
20
+ },
21
+ proves: {
22
+ wrong: [
23
+ {
24
+ files: [
25
+ {
26
+ at: 'packages/core/src/integrations/generators/probe.ts',
27
+ source: "only(piped, ['name', 'describes'], 'a thing')\n",
28
+ },
29
+ ],
30
+ at: 'packages/core/src/integrations/generators/probe.ts',
31
+ },
32
+ {
33
+ files: [
34
+ {
35
+ at: 'packages/core/src/integrations/generators/probe.ts',
36
+ source: "only(one, ['stop', 'sets'], 'a stop')\n",
37
+ },
38
+ ],
39
+ at: 'packages/core/src/integrations/generators/probe.ts',
40
+ },
41
+ ],
42
+ right: [
43
+ {
44
+ files: [
45
+ {
46
+ at: 'packages/core/src/integrations/generators/probe.ts',
47
+ source: "const TAKES = ASKS.map((one) => one.key)\nonly(piped, TAKES, 'a thing')\n",
48
+ },
49
+ ],
50
+ at: 'packages/core/src/integrations/generators/probe.ts',
51
+ },
52
+ {
53
+ files: [
54
+ {
55
+ at: 'packages/core/src/integrations/generators/probe.ts',
56
+ source: "only(all, [...allowed, ...worded], 'a thing')\n",
57
+ },
58
+ ],
59
+ at: 'packages/core/src/integrations/generators/probe.ts',
60
+ },
61
+ ],
62
+ },
63
+ }
@@ -0,0 +1,100 @@
1
+ import type { Slopnator } from '../slopnator.ts'
2
+
3
+ /*
4
+ * Credentials that announce themselves.
5
+ *
6
+ * A vendor prefix is the one thing here that is certain rather than probable: nothing else in a
7
+ * source file begins `sk_live_` or `AKIA` followed by twenty characters. These are matched on their
8
+ * own, with no need to look at what they were assigned to.
9
+ */
10
+ const ANNOUNCED: readonly { readonly what: string; readonly pattern: RegExp }[] = [
11
+ { what: 'a Stripe secret key', pattern: /\bsk_(?:live|test)_[A-Za-z0-9]{16,}/ },
12
+ { what: 'a Stripe restricted key', pattern: /\brk_(?:live|test)_[A-Za-z0-9]{16,}/ },
13
+ { what: 'a GitHub token', pattern: /\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{30,}/ },
14
+ { what: 'a GitHub personal access token', pattern: /\bgithub_pat_[A-Za-z0-9_]{30,}/ },
15
+ { what: 'an AWS access key id', pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/ },
16
+ { what: 'a Slack token', pattern: /\bxox[abposr]-[A-Za-z0-9-]{10,}/ },
17
+ { what: 'an OpenAI key', pattern: /\bsk-[A-Za-z0-9_-]{32,}/ },
18
+ { what: 'an Anthropic key', pattern: /\bsk-ant-[A-Za-z0-9_-]{20,}/ },
19
+ { what: 'a Google API key', pattern: /\bAIza[A-Za-z0-9_-]{35}\b/ },
20
+ { what: 'a JSON web token', pattern: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/ },
21
+ { what: 'a private key', pattern: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/ },
22
+ { what: 'a password inside a connection string', pattern: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@]+:[^\s:/@]{6,}@/ },
23
+ ]
24
+
25
+ /*
26
+ * A value that does not announce itself, read off what it was called.
27
+ *
28
+ * Needs both halves — a name that means credential AND something long enough and mixed enough to be
29
+ * one. Either alone is noise: `const key = 'id'` is a map key, and a long opaque string is as likely
30
+ * to be a hash, a path or a fixture. `process.env.X`, an import, a template hole and a reference to
31
+ * a declared row are all values rather than literals and are not matched at all.
32
+ */
33
+ const NAMED = /\b(?:secret|password|passwd|api_?key|apikey|access_?token|auth_?token|private_?key|client_?secret|credential)s?\b\s*[:=]\s*(['"`])([^'"`\n]{16,})\1/gi
34
+
35
+ const MIXED = /^(?=.*[a-z])(?=.*[A-Z0-9])[A-Za-z0-9_+/=.-]{16,}$/
36
+
37
+ /*
38
+ * What is deliberately NOT a finding.
39
+ *
40
+ * A secret this app declares is a NAME, and naming one is the whole remedy this law recommends — so
41
+ * `sec_stripe_key` sitting in a row, a `requires`, or a call to the vault must never be the thing
42
+ * that trips it. The same for an environment variable, which is a value read at run time rather than
43
+ * a value written down, and for the obvious placeholders people write while wiring something up.
44
+ */
45
+ const NOT = [
46
+ /^(?:sec|pub)_[a-z0-9_]+$/,
47
+ /^\$\{/,
48
+ /^(?:x{3,}|X{3,}|\.{3,}|<[^>]+>|change[-_]?me|your[-_]|example|placeholder|redacted|todo|test|dummy|sample|fake|dGhpcyBpcyBub3QgYSBzZWNyZXQ)/i,
49
+ ]
50
+
51
+ const CODE = /\.(?:ts|tsx|mts|cts|js|jsx|mjs|cjs|json|env|ya?ml|toml|sql|md|sh)$/
52
+
53
+ export const slopnator: Slopnator = {
54
+ holds: (_node, at) => {
55
+ if (!CODE.test(at.filename)) return false
56
+
57
+ /*
58
+ * The sealed store is ciphertext and is meant to be here.
59
+ *
60
+ * It is base64 by construction, so the entropy half below would find every value in it — and
61
+ * the fix it would recommend is the thing the file already is. A law whose refusal has no honest
62
+ * remedy is one people learn to ignore.
63
+ */
64
+ if (/(?:^|\/)\.thread\/vault\.sealed\.json$/.test(at.filename)) return false
65
+
66
+ const text = String(at.text)
67
+
68
+ for (const one of ANNOUNCED) {
69
+ if (!one.pattern.test(text)) continue
70
+ return (
71
+ `this holds ${one.what}, written into the file. It is in every copy of this repository, in its history for ` +
72
+ 'ever, and in whatever bundle reaches it'
73
+ )
74
+ }
75
+
76
+ for (const found of text.matchAll(NAMED)) {
77
+ const value = found[2] ?? ''
78
+ if (NOT.some((each) => each.test(value))) continue
79
+ if (!MIXED.test(value)) continue
80
+ return (
81
+ `this writes a ${(found[0].split(/[:=]/)[0] ?? '').trim()} down as a literal ${String(value.length)} characters ` +
82
+ 'long. A credential in a file is in every copy of this repository and in its history for ever'
83
+ )
84
+ }
85
+
86
+ return false
87
+ },
88
+ proves: {
89
+ wrong: [
90
+ "export const stripe = { apiKey: 'sk_live_51H8xQvL2eZvKYlo2C0Nx9K4mQwErTyUiOp' }\n",
91
+ "const config = { password: 'Hunter2Hunter2Hunter2' }\n",
92
+ "export const db = 'postgres://app:s3cr3tpassword@db.example.com:5432/main'\n",
93
+ ],
94
+ right: [
95
+ "export const stripe = { apiKey: 'sec_stripe_key' }\n",
96
+ "const config = { password: process.env['DB_PASSWORD'] }\n",
97
+ "export const node = nd_action(async (hands) => await hands.ht_stripe.charge({ json: { amount: 100 } }))\n",
98
+ ],
99
+ },
100
+ }