@prestack/thread 0.1.21 → 0.1.22

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 (137) hide show
  1. package/CHANGELOG.md +109 -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 +68 -0
  7. package/dist/core/tree.d.ts +1 -0
  8. package/dist/core/tree.js +7 -1
  9. package/dist/derivation/generators/build.d.ts +3 -1
  10. package/dist/derivation/generators/build.js +103 -22
  11. package/dist/derivation/generators/release.js +74 -26
  12. package/dist/derivation/release.js +3 -3
  13. package/dist/integrations/cli.js +69 -10
  14. package/dist/integrations/pglite.js +13 -3
  15. package/dist/integrations/target.d.ts +3 -0
  16. package/dist/integrity/attach.js +1 -5
  17. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  18. package/dist/operations/access.js +3 -2
  19. package/dist/operations/capabilities.d.ts +2 -0
  20. package/dist/operations/capabilities.js +2 -0
  21. package/dist/operations/generators/capability.js +234 -24
  22. package/dist/operations/generators/compile.js +150 -50
  23. package/dist/operations/generators/seed.d.ts +3 -0
  24. package/dist/operations/generators/seed.js +25 -3
  25. package/dist/operations/nodes.d.ts +2 -2
  26. package/package.json +1 -1
  27. package/dist/core/actor.ts +0 -21
  28. package/dist/core/attach.ts +0 -613
  29. package/dist/core/chain.ts +0 -145
  30. package/dist/core/condition.ts +0 -227
  31. package/dist/core/descend.ts +0 -1185
  32. package/dist/core/errors/catalogue.ts +0 -314
  33. package/dist/core/errors/classes.ts +0 -51
  34. package/dist/core/errors/error.ts +0 -152
  35. package/dist/core/errors/namespaces.ts +0 -19
  36. package/dist/core/finding.ts +0 -37
  37. package/dist/core/generators/commit.ts +0 -157
  38. package/dist/core/init.ts +0 -222
  39. package/dist/core/invoked.ts +0 -52
  40. package/dist/core/round.ts +0 -74
  41. package/dist/core/rows.ts +0 -506
  42. package/dist/core/scope.ts +0 -20
  43. package/dist/core/thread.ts +0 -29
  44. package/dist/core/tree.ts +0 -90
  45. package/dist/core/types.ts +0 -351
  46. package/dist/derivation/attach.ts +0 -57
  47. package/dist/derivation/generators/build.ts +0 -371
  48. package/dist/derivation/generators/check.ts +0 -118
  49. package/dist/derivation/generators/determinism.ts +0 -145
  50. package/dist/derivation/generators/release.ts +0 -236
  51. package/dist/derivation/release.ts +0 -43
  52. package/dist/integrations/attach.ts +0 -187
  53. package/dist/integrations/cli.ts +0 -606
  54. package/dist/integrations/declared.ts +0 -34
  55. package/dist/integrations/disk.ts +0 -466
  56. package/dist/integrations/generators/read.ts +0 -127
  57. package/dist/integrations/generators/vault.ts +0 -210
  58. package/dist/integrations/https.ts +0 -216
  59. package/dist/integrations/pglite.ts +0 -206
  60. package/dist/integrations/secret.ts +0 -54
  61. package/dist/integrations/target.ts +0 -672
  62. package/dist/integrations/vault.ts +0 -148
  63. package/dist/integrity/attach.ts +0 -150
  64. package/dist/integrity/check-threads.ts +0 -166
  65. package/dist/integrity/constraint.ts +0 -1246
  66. package/dist/integrity/generators/compiler.ts +0 -138
  67. package/dist/integrity/generators/gate.ts +0 -227
  68. package/dist/integrity/generators/issues.ts +0 -160
  69. package/dist/integrity/generators/publish.ts +0 -233
  70. package/dist/integrity/generators/trial.ts +0 -359
  71. package/dist/integrity/lint.ts +0 -214
  72. package/dist/integrity/lsp/bin.ts +0 -44
  73. package/dist/integrity/lsp/diagnostics.ts +0 -92
  74. package/dist/integrity/lsp/findings.ts +0 -63
  75. package/dist/integrity/lsp/lint.ts +0 -124
  76. package/dist/integrity/lsp/protocol.ts +0 -78
  77. package/dist/integrity/lsp/server.ts +0 -224
  78. package/dist/integrity/slopnator.ts +0 -324
  79. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  80. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  81. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  82. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  83. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  84. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  85. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  86. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  87. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  88. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  89. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  90. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  91. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  92. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  93. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  94. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  95. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  96. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  97. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  98. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  99. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  100. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  101. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  102. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  103. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  104. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  105. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  106. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  107. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  108. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  109. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  110. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  111. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  112. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  113. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  114. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  115. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  116. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  117. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  118. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  119. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  120. package/dist/integrity/sweep.ts +0 -395
  121. package/dist/knowledge/attach.ts +0 -327
  122. package/dist/knowledge/generators/docsgen.ts +0 -186
  123. package/dist/knowledge/generators/glossary.ts +0 -73
  124. package/dist/knowledge/glossary.ts +0 -6
  125. package/dist/operations/access.ts +0 -131
  126. package/dist/operations/attach.ts +0 -408
  127. package/dist/operations/capabilities.ts +0 -88
  128. package/dist/operations/dispatch.ts +0 -294
  129. package/dist/operations/error-results.ts +0 -31
  130. package/dist/operations/generators/capability.ts +0 -1054
  131. package/dist/operations/generators/compile.ts +0 -701
  132. package/dist/operations/generators/permits.ts +0 -229
  133. package/dist/operations/generators/seed.ts +0 -359
  134. package/dist/operations/generators/trial.ts +0 -666
  135. package/dist/operations/nodes.ts +0 -53
  136. package/dist/operations/operations.ts +0 -91
  137. 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
- }