@prestack/thread 0.1.21 → 0.1.23

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 (138) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.js +6 -3
  4. package/dist/core/generators/commit.js +55 -3
  5. package/dist/core/generators/dev.d.ts +1 -0
  6. package/dist/core/generators/dev.js +69 -0
  7. package/dist/derivation/generators/build.d.ts +3 -1
  8. package/dist/derivation/generators/build.js +104 -22
  9. package/dist/derivation/generators/release.js +74 -25
  10. package/dist/derivation/release.js +3 -3
  11. package/dist/integrations/cli.d.ts +0 -1
  12. package/dist/integrations/cli.js +69 -11
  13. package/dist/integrations/declared.d.ts +2 -0
  14. package/dist/integrations/declared.js +6 -1
  15. package/dist/integrations/pglite.js +13 -3
  16. package/dist/integrations/target.d.ts +3 -0
  17. package/dist/integrity/attach.js +1 -5
  18. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  19. package/dist/operations/access.js +3 -2
  20. package/dist/operations/capabilities.d.ts +2 -0
  21. package/dist/operations/capabilities.js +2 -0
  22. package/dist/operations/generators/capability.js +237 -24
  23. package/dist/operations/generators/compile.js +150 -50
  24. package/dist/operations/generators/seed.d.ts +3 -0
  25. package/dist/operations/generators/seed.js +25 -3
  26. package/dist/operations/nodes.d.ts +2 -2
  27. package/package.json +1 -1
  28. package/dist/core/actor.ts +0 -21
  29. package/dist/core/attach.ts +0 -613
  30. package/dist/core/chain.ts +0 -145
  31. package/dist/core/condition.ts +0 -227
  32. package/dist/core/descend.ts +0 -1185
  33. package/dist/core/errors/catalogue.ts +0 -314
  34. package/dist/core/errors/classes.ts +0 -51
  35. package/dist/core/errors/error.ts +0 -152
  36. package/dist/core/errors/namespaces.ts +0 -19
  37. package/dist/core/finding.ts +0 -37
  38. package/dist/core/generators/commit.ts +0 -157
  39. package/dist/core/init.ts +0 -222
  40. package/dist/core/invoked.ts +0 -52
  41. package/dist/core/round.ts +0 -74
  42. package/dist/core/rows.ts +0 -506
  43. package/dist/core/scope.ts +0 -20
  44. package/dist/core/thread.ts +0 -29
  45. package/dist/core/tree.ts +0 -90
  46. package/dist/core/types.ts +0 -351
  47. package/dist/derivation/attach.ts +0 -57
  48. package/dist/derivation/generators/build.ts +0 -371
  49. package/dist/derivation/generators/check.ts +0 -118
  50. package/dist/derivation/generators/determinism.ts +0 -145
  51. package/dist/derivation/generators/release.ts +0 -236
  52. package/dist/derivation/release.ts +0 -43
  53. package/dist/integrations/attach.ts +0 -187
  54. package/dist/integrations/cli.ts +0 -606
  55. package/dist/integrations/declared.ts +0 -34
  56. package/dist/integrations/disk.ts +0 -466
  57. package/dist/integrations/generators/read.ts +0 -127
  58. package/dist/integrations/generators/vault.ts +0 -210
  59. package/dist/integrations/https.ts +0 -216
  60. package/dist/integrations/pglite.ts +0 -206
  61. package/dist/integrations/secret.ts +0 -54
  62. package/dist/integrations/target.ts +0 -672
  63. package/dist/integrations/vault.ts +0 -148
  64. package/dist/integrity/attach.ts +0 -150
  65. package/dist/integrity/check-threads.ts +0 -166
  66. package/dist/integrity/constraint.ts +0 -1246
  67. package/dist/integrity/generators/compiler.ts +0 -138
  68. package/dist/integrity/generators/gate.ts +0 -227
  69. package/dist/integrity/generators/issues.ts +0 -160
  70. package/dist/integrity/generators/publish.ts +0 -233
  71. package/dist/integrity/generators/trial.ts +0 -359
  72. package/dist/integrity/lint.ts +0 -214
  73. package/dist/integrity/lsp/bin.ts +0 -44
  74. package/dist/integrity/lsp/diagnostics.ts +0 -92
  75. package/dist/integrity/lsp/findings.ts +0 -63
  76. package/dist/integrity/lsp/lint.ts +0 -124
  77. package/dist/integrity/lsp/protocol.ts +0 -78
  78. package/dist/integrity/lsp/server.ts +0 -224
  79. package/dist/integrity/slopnator.ts +0 -324
  80. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  81. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  82. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  83. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  84. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  85. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  86. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  87. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  88. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  89. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  90. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  91. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  92. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  93. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  94. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  95. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  96. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  97. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  98. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  99. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  100. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  101. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  102. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  103. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  104. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  105. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  106. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  107. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  108. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  109. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  110. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  111. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  112. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  113. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  114. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  115. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  116. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  117. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  118. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  119. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  120. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  121. package/dist/integrity/sweep.ts +0 -395
  122. package/dist/knowledge/attach.ts +0 -327
  123. package/dist/knowledge/generators/docsgen.ts +0 -186
  124. package/dist/knowledge/generators/glossary.ts +0 -73
  125. package/dist/knowledge/glossary.ts +0 -6
  126. package/dist/operations/access.ts +0 -131
  127. package/dist/operations/attach.ts +0 -408
  128. package/dist/operations/capabilities.ts +0 -88
  129. package/dist/operations/dispatch.ts +0 -294
  130. package/dist/operations/error-results.ts +0 -31
  131. package/dist/operations/generators/capability.ts +0 -1054
  132. package/dist/operations/generators/compile.ts +0 -701
  133. package/dist/operations/generators/permits.ts +0 -229
  134. package/dist/operations/generators/seed.ts +0 -359
  135. package/dist/operations/generators/trial.ts +0 -666
  136. package/dist/operations/nodes.ts +0 -53
  137. package/dist/operations/operations.ts +0 -91
  138. package/dist/operations/result.ts +0 -28
@@ -1,701 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from '../../integrations/disk.ts'
2
- import { resolve } from 'node:path'
3
- import { argv, stdout } from 'node:process'
4
-
5
- import ts from 'typescript'
6
-
7
- import { ROOT, descend } from '../../core/descend.ts'
8
- import { runChecker, type Finding } from '../../core/finding.ts'
9
- import { exit, invoked } from '../../core/invoked.ts'
10
- import { inCode, valuesIn } from '../../core/rows.ts'
11
-
12
- import { credentialsOf, holdsOf, reachOf, type Call, type Target } from '../../integrations/target.ts'
13
- import type { Secret } from '../../integrations/secret.ts'
14
-
15
- import { DISPATCH_AT, dispatched } from '../dispatch.ts'
16
- import { beside, blockIn, builtFrom, operations, problem, quoted, runtime, types } from './seed.ts'
17
-
18
- async function reaching(): Promise<Readonly<Record<string, ReturnType<typeof reached>[string]>>> {
19
- const held = async <T>(file: string, list: string): Promise<readonly T[]> => {
20
- const path = resolve(ROOT, `_threads/${file}`)
21
- if (!existsSync(path)) return []
22
- try {
23
- const taken = valuesIn(readFileSync(path, 'utf8')) as Record<string, readonly T[] | undefined>
24
- return taken[list] ?? []
25
- } catch {
26
- return []
27
- }
28
- }
29
- const targets = await held<Target>('targets.ts', 'TARGETS')
30
- const calls = await held<Call>('targets.ts', 'CALLS')
31
- const secrets = await held<Secret>('secrets.ts', 'SECRETS')
32
- return reached(targets, calls, secrets)
33
- }
34
-
35
- export function reached(
36
- targets: readonly Target[],
37
- calls: readonly Call[],
38
- secrets: readonly Secret[],
39
- ): Readonly<
40
- Record<
41
- string,
42
- {
43
- readonly reach: string
44
- readonly address?: string
45
- readonly headers: Readonly<Record<string, string>>
46
- readonly sends: readonly { readonly secret: string; readonly header: string; readonly prefix: string }[]
47
- readonly signs: readonly { readonly secret: string; readonly header: string; readonly prefix: string }[]
48
- readonly calls: Readonly<Record<string, { readonly method: string | null; readonly path: string }>>
49
- }
50
- >
51
- > {
52
- const out: Record<string, ReturnType<typeof reached>[string]> = {}
53
- for (const one of targets) {
54
- const mine = calls.filter((each) => each.of === one.id && one.takes.includes(each.id))
55
- out[one.id] = {
56
- reach: reachOf(one.id) ?? 'nothing known',
57
- address: one.address,
58
- headers: one.headers ?? {},
59
- signs: credentialsOf(one, secrets)
60
- .filter((each) => each.signs !== undefined)
61
- .map((each) => ({
62
- secret: each.id,
63
- header: each.signs?.header ?? '',
64
- prefix: each.signs?.prefix ?? '',
65
- })),
66
- sends: credentialsOf(one, secrets).filter((each) => each.sends !== undefined).map((each) => ({
67
- secret: each.id,
68
- header: each.sends?.header ?? '',
69
- prefix: each.sends?.prefix ?? '',
70
- })),
71
-
72
- calls: Object.fromEntries(
73
- mine.map((each) => [each.id.replace(/^cl_/, ''), { method: each.method ?? null, path: each.path }]),
74
- ),
75
- }
76
- }
77
- return out
78
- }
79
-
80
- const OWN = '$'
81
-
82
- const OPENED = 'opened'
83
-
84
- const ENTERED = `${OWN}at`
85
-
86
- const PLAIN = /^(?:[A-Za-z_][A-Za-z0-9_]*|'(?:[^'\\]|\\.)*')$/
87
-
88
- export function parts(text: string): readonly string[] {
89
- const code = inCode(text)
90
- const found: string[] = []
91
- let depth = 0
92
- let from = 0
93
- for (let at = 0; at < text.length; at += 1) {
94
- if (code[at] !== true) continue
95
- const one = text[at]
96
- if (one === '(' || one === '[' || one === '{') depth += 1
97
- else if (one === ')' || one === ']' || one === '}') depth -= 1
98
- else if (one === ',' && depth === 0) {
99
- found.push(text.slice(from, at))
100
- from = at + 1
101
- }
102
- }
103
- found.push(text.slice(from))
104
- return found.map((one) => one.trim()).filter((one) => one !== '')
105
- }
106
-
107
- const inside = (text: string, open: string, shut: string): string | undefined => {
108
- const said = text.trim()
109
- return said.startsWith(open) && said.endsWith(shut) ? said.slice(1, -1) : undefined
110
- }
111
-
112
- function keyed(text: string): { readonly key: string; readonly held: string } | undefined {
113
- const code = inCode(text)
114
- let depth = 0
115
- for (let at = 0; at < text.length; at += 1) {
116
- if (code[at] !== true) continue
117
- const one = text[at]
118
- if (one === '(' || one === '[' || one === '{') depth += 1
119
- else if (one === ')' || one === ']' || one === '}') depth -= 1
120
- else if (one === ':' && depth === 0) return { key: text.slice(0, at).trim(), held: text.slice(at + 1).trim() }
121
- }
122
- return undefined
123
- }
124
-
125
- const OPENS = 'const $ = '
126
-
127
- const ROLES = {
128
- block: ['opened', 'held'],
129
- over: ['held'],
130
- item: ['item', 'opened', 'held'],
131
- again: ['pass', 'carried', 'opened', 'held'],
132
- } as const satisfies Readonly<Record<string, readonly string[]>>
133
-
134
- interface Read {
135
- readonly arrow: ts.ArrowFunction
136
- readonly held: string | undefined
137
- }
138
-
139
- function arrowIn(text: string, roles: readonly string[]): Read | string {
140
- const file = ts.createSourceFile('block.ts', `${OPENS}${text}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
141
- const first = file.statements[0]
142
- if (first === undefined || !ts.isVariableStatement(first)) return 'is not written as a function where it is used'
143
- const held = first.declarationList.declarations[0]?.initializer
144
- if (held === undefined || !ts.isArrowFunction(held)) return 'is not an arrow function written where it is used'
145
- if (held.parameters.length > roles.length) {
146
- return `takes ${String(held.parameters.length)} things, and this kind of block is handed ${roles.join(', ')}`
147
- }
148
- for (const one of held.parameters) {
149
- if (!ts.isIdentifier(one.name)) return 'names a parameter by taking it apart, and this needs the name itself'
150
- }
151
- const last = held.parameters.length === roles.length ? held.parameters[roles.length - 1] : undefined
152
- const named = last !== undefined && ts.isIdentifier(last.name) ? last.name.text : undefined
153
- return { arrow: held, held: named }
154
- }
155
-
156
- const BINDS = new Set<ts.SyntaxKind>([
157
- ts.SyntaxKind.Parameter,
158
- ts.SyntaxKind.VariableDeclaration,
159
- ts.SyntaxKind.BindingElement,
160
- ts.SyntaxKind.FunctionDeclaration,
161
- ts.SyntaxKind.ClassDeclaration,
162
- ])
163
-
164
- function bound(node: ts.Node, found: Set<string>): void {
165
- if (BINDS.has(node.kind)) {
166
- const said = (node as { readonly name?: ts.Node }).name
167
- if (said !== undefined && ts.isIdentifier(said)) found.add(said.text)
168
- }
169
- if (ts.isCatchClause(node) && node.variableDeclaration !== undefined && ts.isIdentifier(node.variableDeclaration.name)) {
170
- found.add(node.variableDeclaration.name.text)
171
- }
172
- node.forEachChild((one) => {
173
- bound(one, found)
174
- })
175
- }
176
-
177
- function answers(node: ts.Node, found: ts.ReturnStatement[]): void {
178
- if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) return
179
- if (ts.isReturnStatement(node)) found.push(node)
180
- node.forEachChild((one) => {
181
- answers(one, found)
182
- })
183
- }
184
-
185
- interface Spliced {
186
- readonly form: 'expression' | 'statements' | 'call'
187
-
188
- readonly text: string
189
-
190
- readonly whole: string
191
- }
192
-
193
- function spliced(text: string, shape: Shape, roles: readonly string[]): Spliced | string {
194
- const { names, mine } = shape
195
- const read = arrowIn(text, roles)
196
- if (typeof read === 'string') return read
197
-
198
- const inner = new Set<string>()
199
- bound(read.arrow.body, inner)
200
-
201
- if (read.held !== undefined && inner.has(read.held)) {
202
- return `declares its own \`${read.held}\` inside itself as well as taking one, and which of the two a \`${read.held}.something\` means cannot be told apart from here`
203
- }
204
-
205
- const shadows = new Set<string>(inner)
206
- for (const one of read.arrow.parameters) {
207
- if (ts.isIdentifier(one.name) && one.name.text !== read.held) shadows.add(one.name.text)
208
- }
209
-
210
- for (const one of shadows) {
211
- if (mine.has(one)) {
212
- return `uses the name \`${one}\` for something of its own, and that is also what a node of this operation is called — once both are locals one would shadow the other`
213
- }
214
- }
215
-
216
- interface Edit {
217
- readonly from: number
218
- readonly to: number
219
- readonly text: string
220
- }
221
-
222
- const edits: Edit[] = []
223
- let because: string | undefined
224
-
225
- const walk = (node: ts.Node): void => {
226
- if (because !== undefined) return
227
- if (read.held !== undefined && ts.isIdentifier(node) && node.text === read.held) {
228
- const up: ts.Node | undefined = node.parent
229
- if (up !== undefined && ts.isPropertyAccessExpression(up) && up.expression === node) {
230
- const said = up.name.text
231
- if (said !== 'input' && !names.has(said)) {
232
-
233
- because =
234
- `reads \`${read.held}.${said}\`, and no node before it answers to that name. ` +
235
- `\`${read.held}\` holds \`input\`, which is what this operation was called with, ` +
236
- (names.size === 0
237
- ? 'and nothing else — this is the first node, so `input` is all there is'
238
- : `and one name per node that has already answered: ${[...names].map((one) => `\`${one}\``).join(', ')}`)
239
- return
240
- }
241
- edits.push({ from: up.getStart(), to: up.getEnd(), text: said })
242
- shape.heard.add(said)
243
- return
244
- }
245
- because = `reaches for \`${read.held}\` itself rather than one thing on it, and once every node is a local there is no such thing to hand it`
246
- return
247
- }
248
- node.forEachChild(walk)
249
- }
250
- walk(read.arrow.body)
251
- if (because !== undefined) return because
252
-
253
- const held = read.arrow.parameters[roles.length - 1]
254
- const before = read.arrow.parameters[roles.length - 2]
255
- if (held !== undefined) {
256
- edits.push({ from: before?.getEnd() ?? held.getStart(), to: held.getEnd(), text: '' })
257
- }
258
-
259
- let said = `${OPENS}${text}`
260
- for (const one of [...edits].sort((a, b) => b.from - a.from)) {
261
- said = `${said.slice(0, one.from)}${one.text}${said.slice(one.to)}`
262
- }
263
-
264
- const whole = said.slice(OPENS.length)
265
- const again = arrowIn(whole, roles.slice(0, -1))
266
- if (typeof again === 'string') return again
267
- const body = again.arrow.body
268
-
269
- if (again.arrow.parameters.length > 1) return { form: 'call', text: whole, whole }
270
-
271
- const opens = again.arrow.parameters[0]
272
- if (opens !== undefined && ts.isIdentifier(opens.name) && opens.name.text !== OPENED) {
273
- return { form: 'call', text: whole, whole }
274
- }
275
-
276
- if (!ts.isBlock(body)) return { form: 'expression', text: body.getText(), whole }
277
-
278
- const found: ts.ReturnStatement[] = []
279
- answers(body, found)
280
- const last = body.statements[body.statements.length - 1]
281
- if (found.length === 1 && last !== undefined && found[0] === last && ts.isReturnStatement(last)) {
282
- const kept = body.statements.slice(0, -1).map((one) => one.getText())
283
- return { form: 'statements', text: [...kept, `${ANSWERS} = ${last.expression?.getText() ?? 'undefined'}`].join('\n'), whole }
284
- }
285
-
286
- return { form: 'call', text: whole, whole }
287
- }
288
-
289
- const ANSWERS = `${OWN}ANSWER`
290
-
291
- interface Made {
292
- readonly runs: readonly string[]
293
- }
294
-
295
- interface Shape {
296
- readonly all: string
297
- readonly reads: string
298
- readonly names: ReadonlySet<string>
299
- readonly mine: ReadonlySet<string>
300
- readonly heard: Set<string>
301
- readonly unread: ReadonlySet<string>
302
- }
303
-
304
- const linesOf = (one: Spliced, name: string, unread: boolean): readonly string[] =>
305
- (unread ? `${one.text.replace(`${ANSWERS} = `, 'void (')})` : one.text.replace(ANSWERS, name)).split('\n')
306
-
307
- function answered(name: string, one: Spliced, opened: string, kind: 'const' | 'let', unread: boolean): readonly string[] {
308
- if (one.form === 'expression') return [unread ? `void (${one.text})` : `${kind} ${name}: unknown = ${one.text}`]
309
- if (one.form === 'statements') {
310
- return [...(unread ? [] : [`let ${name}: unknown`]), '{', ...linesOf(one, name, unread).map((line) => ` ${line}`), '}']
311
- }
312
- return [`${unread ? '' : `${kind} ${name}: unknown = `}await ${asFn(one.whole, opened)}(opened)`]
313
- }
314
-
315
- const asFn = (text: string, opened: string, more: string = ''): string =>
316
- `((${text}) as (${more}opened: ${opened}) => Promise<unknown>)`
317
-
318
- function forked(name: string, said: readonly string[], shape: Shape): Made | string {
319
- if (said.length !== 2) return `takes a chooser and the ways it chooses between, and was handed ${String(said.length)} thing(s)`
320
-
321
- const chose = spliced(said[0] ?? '', shape, ROLES.block)
322
- if (typeof chose === 'string') return `chooses with a block that ${chose}`
323
-
324
- const held = inside(said[1] ?? '', '{', '}')
325
- const ways = held === undefined ? undefined : parts(held).map((one) => keyed(one))
326
- const named = ways?.every((one) => one !== undefined && PLAIN.test(one.key)) === true ? ways : undefined
327
- if (named === undefined || named.length === 0) return 'names its ways in a way this cannot read — write each one as `name: block`'
328
-
329
- const unread = shape.unread.has(name)
330
- const picked = `${OWN}${name}_said`
331
- const runs = [
332
- ...(unread ? [] : [`let ${name}: unknown`]),
333
- chose.form === 'expression'
334
- ? `const ${picked}: string = await (${chose.text})`
335
- : `const ${picked}: string = await ((${said[0] ?? ''}) as (opened: ${shape.reads}) => Promise<string>)(opened)`,
336
- `switch (${picked}) {`,
337
- ]
338
-
339
- for (const way of named) {
340
- const each = spliced(way?.held ?? '', shape, ROLES.block)
341
- if (typeof each === 'string') return `has a way \`${way?.key ?? ''}\` that ${each}`
342
- runs.push(` case ${(way?.key ?? '').startsWith("'") ? (way?.key ?? '') : quoted(way?.key ?? '')}: {`)
343
- if (each.form === 'expression') runs.push(unread ? ` void (${each.text})` : ` ${name} = ${each.text}`)
344
- else if (each.form === 'statements') runs.push(...linesOf(each, name, unread).map((line) => ` ${line}`))
345
- else runs.push(` ${unread ? '' : `${name} = `}await ${asFn(each.whole, shape.all)}(opened)`)
346
- runs.push(' break')
347
- runs.push(' }')
348
- }
349
-
350
- runs.push(` default: throw new Error(\`${name} chose \${${picked}}, which names no way forward\`)`)
351
- runs.push('}')
352
- return { runs }
353
- }
354
-
355
- const SHAPES: Readonly<Record<string, (name: string, said: readonly string[], shape: Shape) => Made | string>> = {
356
- nd_read: (name, said, shape) => plainly(name, said, shape, shape.reads, false),
357
- nd_action: (name, said, shape) => plainly(name, said, shape, shape.all, false),
358
- nd_stop: (name, said, shape) => plainly(name, said, shape, shape.reads, true),
359
-
360
- nd_each: (name, said, shape) => {
361
- if (said.length !== 2) return `takes what to run over and the block to run, and was handed ${String(said.length)} thing(s)`
362
- const over = spliced(said[0] ?? '', shape, ROLES.over)
363
- const block = spliced(said[1] ?? '', shape, ROLES.item)
364
- if (typeof block === 'string') return `runs a block that ${block}`
365
- if (typeof over === 'string') return `runs over a block that ${over}`
366
- const unread = shape.unread.has(name)
367
- const item = `${OWN}${name}_item`
368
- const all = `${OWN}${name}_all`
369
- const each = `${OWN}${name}_each`
370
- return {
371
- runs: [
372
- ...(unread ? [] : [`const ${all}: unknown[] = []`]),
373
- `const ${each} = (${block.whole}) as (item: unknown, opened: ${shape.all}) => Promise<unknown>`,
374
- `for (const ${item} of ${over.form === 'expression' ? `(${over.text})` : `((${over.whole}) as () => readonly unknown[])()`} as readonly unknown[]) {`,
375
- unread ? ` await ${each}(${item}, opened)` : ` ${all}.push(await ${each}(${item}, opened))`,
376
- '}',
377
- ...(unread ? [] : [`const ${name}: unknown = ${all}`]),
378
- ],
379
- }
380
- },
381
-
382
- nd_parallel: (name, said, shape) => {
383
- if (said.length !== 1) return `takes one list of blocks, and was handed ${String(said.length)} thing(s)`
384
- const held = inside(said[0] ?? '', '[', ']')
385
- if (held === undefined) return 'takes a list of blocks written out where it is called, and this is not one'
386
- const every = parts(held)
387
- if (every.length === 0) return 'was handed no blocks to run at once'
388
-
389
- const calls: string[] = []
390
- for (const [order, one] of every.entries()) {
391
- const each = spliced(one, shape, ROLES.block)
392
- if (typeof each === 'string') return `runs a block at position ${String(order + 1)} that ${each}`
393
- calls.push(each.form === 'expression' ? `(async () => ${each.text})()` : `${asFn(each.whole, shape.all)}(opened)`)
394
- }
395
- return { runs: [`${shape.unread.has(name) ? '' : `const ${name}: unknown = `}await Promise.all([${calls.join(', ')}])`] }
396
- },
397
-
398
- nd_repeat: (name, said, shape) => {
399
- if (said.length !== 2) return `takes a ceiling and the block to repeat, and was handed ${String(said.length)} thing(s)`
400
- const block = spliced(said[1] ?? '', shape, ROLES.again)
401
- if (typeof block === 'string') return `repeats a block that ${block}`
402
- const ceiling = `${OWN}${name}_ceiling`
403
- const again = `${OWN}${name}_again`
404
- const carried = `${OWN}${name}_carried`
405
- const done = `${OWN}${name}_done`
406
- const pass = `${OWN}${name}_pass`
407
- const answer = `${OWN}${name}_said`
408
- const unread = shape.unread.has(name)
409
- return {
410
- runs: [
411
- ...(unread ? [] : [`let ${name}: unknown`]),
412
- `const ${ceiling}: number = ${said[0] ?? ''}`,
413
- `const ${again} = (${block.whole}) as (pass: number, carried: unknown, opened: ${shape.all}) => Promise<{ readonly done: boolean; readonly value: unknown }>`,
414
- `let ${carried}: unknown`,
415
- `let ${done} = false`,
416
- `for (let ${pass} = 0; ${pass} < ${ceiling}; ${pass} += 1) {`,
417
- ` const ${answer} = await ${again}(${pass}, ${carried}, opened)`,
418
- ` if (${answer}.done) { ${unread ? '' : `${name} = ${answer}.value; `}${done} = true; break }`,
419
- ` ${carried} = ${answer}.value`,
420
- '}',
421
- `if (!${done}) throw new Error(\`${name} reached its ceiling of \${String(${ceiling})} passes without answering done\`)`,
422
- ],
423
- }
424
- },
425
-
426
- nd_branch: forked,
427
- }
428
-
429
- const STOPS = "Symbol.for('thread.operation.stopped')"
430
-
431
- function plainly(name: string, said: readonly string[], shape: Shape, opened: string, ends: boolean): Made | string {
432
- if (said.length !== 1) return `takes one block, and was handed ${String(said.length)} thing(s)`
433
- const one = spliced(said[0] ?? '', shape, ROLES.block)
434
- if (typeof one === 'string') return one
435
-
436
- const unread = shape.unread.has(name)
437
- if (!ends) return { runs: answered(name, one, opened, 'const', unread) }
438
-
439
- return {
440
- runs: [
441
- ...answered(name, one, opened, 'const', unread),
442
- `throw Object.assign(new Error(${quoted(`${name} ended the operation`)}), { [${STOPS}]: true, value: ${name} })`,
443
- ],
444
- }
445
- }
446
- const saidIn = (held: ts.Expression | undefined): string | undefined =>
447
- held !== undefined && ts.isStringLiteral(held) ? held.text : undefined
448
-
449
- const sourceIn = (held: ts.Expression | undefined): string | undefined => held?.getText()
450
-
451
- function compiled(
452
- at: string,
453
- id: string,
454
- one: ReturnType<typeof operations> extends ReadonlyMap<string, infer W> ? W : never,
455
- known: ReadonlySet<string>,
456
- unread?: ReadonlySet<string>,
457
- ): { readonly text?: string; readonly because?: string } {
458
- const because = problem(id, one)
459
- if (because !== undefined) return { because }
460
-
461
- const takes = one.takes[0]?.at
462
- if (takes === undefined) return { because: `\`${id}\` seeds what it takes and states no file for it` }
463
-
464
- const { timeout } = one
465
-
466
- const fields = descend().stated.get(id)?.fields
467
- const entitled = saidIn(fields?.get('entitled'))
468
- const rate = sourceIn(fields?.get('rate'))
469
- const quota = sourceIn(fields?.get('quota'))
470
- if (timeout === undefined) {
471
- return {
472
- because:
473
- `\`${id}\` states no \`timeout\`, and an operation says how long it waits. Add \`timeout: 10_000\` to its row in ` +
474
- '_threads/operations.ts, or change it with `thread edit ' + id + ' --timeout <milliseconds>`. How long a call may ' +
475
- 'run is part of the operation, not something the place it reaches decides',
476
- }
477
- }
478
-
479
- const only = one.requires.length === 0 ? 'never' : one.requires.map((each) => quoted(each)).join(' | ')
480
- const mine = new Set<string>(['input', ...one.nodes.map((row) => String(row.id))])
481
-
482
- const runs: string[] = []
483
- const names = new Set<string>()
484
- const heard = new Set<string>()
485
- let last: string | undefined
486
-
487
- for (const [order, row] of one.nodes.entries()) {
488
- const name = row.id
489
- const from = builtFrom(row, known)
490
-
491
- if (from === 'nd_stop' && order !== one.nodes.length - 1) {
492
- const after = one.nodes.slice(order + 1).map((each) => `\`${String(each.id)}\``)
493
- return {
494
- because:
495
- `the node \`${String(name)}\` ends \`${id}\`, and ${after.join(', ')} ${after.length === 1 ? 'is' : 'are'} declared after it. ` +
496
- 'Nothing runs after the node that ends an operation — move it last, or take off what follows it',
497
- }
498
- }
499
-
500
- if (name === undefined) return { because: `\`${id}\` declares a node with no id of its own` }
501
- if (row.at === undefined) return { because: `the node \`${name}\` names no file of its own` }
502
- if (from === undefined) return { because: `the node \`${name}\` names no type this app declares` }
503
-
504
- const path = resolve(ROOT, row.at)
505
- if (!existsSync(path)) return { because: `the node \`${name}\` seeds ${row.at} and nothing is there — run \`thread seed\`` }
506
- const held = blockIn(readFileSync(path, 'utf8'))
507
- if (held === undefined) {
508
- return { because: `the node \`${name}\` states no block this can read in ${row.at} — run \`thread seed\` and write it again` }
509
- }
510
-
511
- const shape: Shape = { all: `Opened<${only}>`, reads: `Reads<Opened<${only}>>`, names, mine, heard, unread: unread ?? new Set<string>() }
512
- const made = (SHAPES[from] ?? ownType)(name, parts(held), shape)
513
- if (typeof made === 'string') return { because: `the node \`${name}\` ${made}` }
514
-
515
- runs.push(` ${ENTERED} = ${quoted(name)}`)
516
- for (const line of made.runs) runs.push(` ${line}`)
517
- names.add(name)
518
- last = from === 'nd_stop' ? undefined : name
519
- }
520
-
521
- if (unread === undefined) {
522
- const final = one.nodes.at(-1)?.id
523
- return compiled(at, id, one, known, new Set([...mine].filter((name) => name !== final && !heard.has(name))))
524
- }
525
-
526
- const typed = ['Opened', 'Reads'].filter((word) => runs.some((line) => line.includes(`${word}<`)))
527
-
528
- return {
529
- text: [
530
- "import type { z } from 'zod'",
531
- '',
532
- `import type { Operation } from ${quoted(runtime(at, '../operations.ts'))}`,
533
- ...(typed.length === 0 ? [] : [`import type { ${typed.join(', ')} } from ${quoted(runtime(at, '../operations.ts'))}`]),
534
- '',
535
- `import { input, resource } from ${quoted(beside(at, takes))}`,
536
- '',
537
- `export const operation: Operation<z.infer<typeof input>, ${only}> = {`,
538
- ` id: ${quoted(id)},`,
539
- ' input,',
540
- ` timeout: ${String(timeout)},`,
541
- ' resource,',
542
- ` requires: [${one.requires.map((each) => quoted(each)).join(', ')}],`,
543
- ...(entitled === undefined ? [] : [` entitled: ${quoted(entitled)},`]),
544
- ...(rate === undefined ? [] : [` rate: ${rate},`]),
545
- ...(quota === undefined ? [] : [` quota: ${quota},`]),
546
- '',
547
- ' run: async (opened, held) => {',
548
- ...(unread.has('input') ? [] : [' const input = held.input']),
549
- ` let ${ENTERED} = ''`,
550
- ' try {',
551
- ...runs,
552
- ...(last === undefined ? [] : [` return ${last}`]),
553
- ' } catch (thrown) {',
554
- ` if (typeof thrown === 'object' && thrown !== null) Object.assign(thrown, { node: ${ENTERED} })`,
555
- ' throw thrown',
556
- ' }',
557
- ' },',
558
- '}',
559
- '',
560
- ].join('\n'),
561
- }
562
- }
563
-
564
- const ownType = (name: string, said: readonly string[], shape: Shape): Made | string =>
565
- plainly(name, said, shape, shape.all, false)
566
-
567
- interface Wanted {
568
- readonly at: string
569
- readonly text?: string
570
- readonly because?: string
571
- }
572
-
573
- async function planned(): Promise<readonly Wanted[]> {
574
- const pass = descend()
575
- const known = types(pass)
576
- const reach = await reaching()
577
- const found: Wanted[] = []
578
-
579
- const answers = Object.keys(reach).filter(
580
- (one) => Object.keys(reach[one]?.calls ?? {}).length > 0 || holdsOf(one)?.takes !== 'calls',
581
- )
582
- const may = [...answers]
583
- for (const [id, one] of operations(pass, known)) {
584
- if (one.body === undefined) continue
585
- const silent = one.requires.filter((each) => reach[each] !== undefined && !answers.includes(each))
586
- if (silent.length > 0) {
587
- found.push({
588
- at: one.body,
589
- because: silent
590
- .map((each) => {
591
- const how = reach[each]?.reach ?? 'nothing known'
592
- const instead = 'declare what it answers with `thread create target call ' + each + ' <name>`'
593
-
594
-
595
-
596
- return (
597
- '`' + id + '` requires `' + each + '`, which is reached over ' + how + ' and answers no call, so there ' +
598
- 'is nothing for a node to be handed. ' + instead + ', ' +
599
- 'or take the word off that operation\'s row'
600
- )
601
- })
602
- .join('; '),
603
- })
604
- continue
605
- }
606
- const stray = one.requires.filter((each) => !may.includes(each))
607
- if (stray.length > 0) {
608
- found.push({
609
- at: one.body,
610
- because:
611
- `\`${id}\` requires ${stray.map((each) => `\`${each}\``).join(', ')}, and this app has no such capability and ` +
612
- `declares no such target. What may be required is ${may.join(', ')} — declare the target with ` +
613
- '`thread create target`, or take the word off that operation\'s row',
614
- })
615
- continue
616
- }
617
- found.push({ at: one.body, ...compiled(one.body, id, one, known) })
618
- }
619
-
620
- if (found.length > 0) found.push({ at: DISPATCH_AT, text: dispatched(reach) })
621
- return found
622
- }
623
-
624
- function checked(plan: readonly Wanted[]): void {
625
- if (plan.length === 0) {
626
- stdout.write('compile: no operation here states a body to compile, so there is nothing to check.\n')
627
- exit(0)
628
- }
629
-
630
- const findings: Finding[] = plan.flatMap((one): Finding[] => {
631
- const path = resolve(ROOT, one.at)
632
-
633
- if (one.because !== undefined) {
634
- return [
635
- {
636
- file: one.at,
637
- detail: one.because,
638
- instead: 'declare it properly — this file is compiled from those rows and cannot be written without them',
639
- },
640
- ]
641
- }
642
- if (!existsSync(path)) {
643
- return [{ file: one.at, detail: 'a row declares this file and nothing is there', instead: 'run `thread compile`' }]
644
- }
645
- if (readFileSync(path, 'utf8') === one.text) return []
646
-
647
- if (one.at === DISPATCH_AT) {
648
- return [
649
- {
650
- file: one.at,
651
- detail: 'this is the one way every operation of this app runs, and it is no longer the bytes that write it',
652
- instead:
653
- 'run `thread compile`. Dispatch has no author: it is the same for every app, so a change made here is a change nothing else in this framework knows about and the next compile takes it back',
654
- },
655
- ]
656
- }
657
-
658
- return [
659
- {
660
- file: one.at,
661
- detail: 'this operation is compiled from its nodes, and this is no longer the bytes that compile it',
662
- instead:
663
- 'run `thread compile`. A compiled operation has no author: to change what it does, change the rows that declare its nodes, and to change what a node does, write it in the file that node seeds',
664
- },
665
- ]
666
- })
667
-
668
- runChecker('compile', findings, `${String(plan.length)} file(s) compiled`)
669
- exit(findings.length === 0 ? 0 : 1)
670
- }
671
-
672
- export async function compile(say: boolean = true): Promise<number> {
673
- const plan = await planned()
674
- let written = 0
675
- let refused = 0
676
- for (const one of plan) {
677
- if (one.text === undefined) {
678
- refused += 1
679
- if (say) stdout.write(`compile: ${one.at} was not written — ${one.because ?? 'the rows behind it are incomplete'}\n`)
680
- continue
681
- }
682
- const path = resolve(ROOT, one.at)
683
- if (existsSync(path) && readFileSync(path, 'utf8') === one.text) continue
684
- writeFileSync(path, one.text, 'utf8')
685
- written += 1
686
- if (say) stdout.write(`compile: wrote ${one.at}\n`)
687
- }
688
- if (say) {
689
- stdout.write(
690
- `compile: ${String(written)} of ${String(plan.length)} file(s) written, ${String(plan.length - written - refused)} already as compiled` +
691
- `${refused === 0 ? '' : `, ${String(refused)} refused`}.\n`,
692
- )
693
- }
694
- if (refused > 0) exit(1)
695
- return written
696
- }
697
-
698
- if (invoked(import.meta.url)) {
699
- if (argv.includes('--check')) checked(await planned())
700
- else await compile()
701
- }