@prestack/thread 0.0.1 → 0.0.2

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
@@ -448,10 +448,29 @@ async function planned() {
448
448
  const known = types(pass);
449
449
  const reach = await reaching();
450
450
  const found = [];
451
- const may = [...SUPPLIES, ...Object.keys(reach)];
451
+ const answers = Object.keys(reach).filter((one) => Object.keys(reach[one]?.calls ?? {}).length > 0);
452
+ const may = [...SUPPLIES, ...answers];
452
453
  for (const [id, one] of operations(pass, known)) {
453
454
  if (one.body === undefined)
454
455
  continue;
456
+ const silent = one.requires.filter((each) => reach[each] !== undefined && !answers.includes(each));
457
+ if (silent.length > 0) {
458
+ found.push({
459
+ at: one.body,
460
+ because: silent
461
+ .map((each) => {
462
+ const how = reach[each]?.reach ?? 'nothing known';
463
+ const instead = how === 'module' || how === 'cli'
464
+ ? 'run it with `thread run ' + each.replace(/^[a-z]+_/, '') + '`'
465
+ : 'reach the rows it keeps with `sm_store`';
466
+ return ('`' + id + '` requires `' + each + '`, which is reached over ' + how + ' and answers no call, so there ' +
467
+ 'is nothing for a node to be handed. ' + instead + ', give it a call with `thread create target call`, ' +
468
+ 'or take the word off that operation\'s row');
469
+ })
470
+ .join('; '),
471
+ });
472
+ continue;
473
+ }
455
474
  const stray = one.requires.filter((each) => !may.includes(each));
456
475
  if (stray.length > 0) {
457
476
  found.push({
@@ -0,0 +1,643 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, 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 } from '../../core/rows.ts'
11
+
12
+ import { addressOf, credentialsOf, 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 { SUPPLIES } from '../seams.ts'
17
+ import { beside, blockIn, builtFrom, operations, problem, quoted, runtime, types } from './seed.ts'
18
+
19
+ async function reaching(): Promise<Readonly<Record<string, ReturnType<typeof reached>[string]>>> {
20
+ const held = async <T>(file: string, list: string): Promise<readonly T[]> => {
21
+ const path = resolve(ROOT, `_threads/${file}`)
22
+ if (!existsSync(path)) return []
23
+ try {
24
+ const taken = (await import(new URL(`file://${path}`).href)) as Record<string, readonly T[] | undefined>
25
+ return taken[list] ?? []
26
+ } catch {
27
+ return []
28
+ }
29
+ }
30
+ const targets = await held<Target>('targets.ts', 'TARGETS')
31
+ const calls = await held<Call>('targets.ts', 'CALLS')
32
+ const secrets = await held<Secret>('secrets.ts', 'SECRETS')
33
+ return reached(targets, calls, secrets)
34
+ }
35
+
36
+ function reached(
37
+ targets: readonly Target[],
38
+ calls: readonly Call[],
39
+ secrets: readonly Secret[],
40
+ ): Readonly<
41
+ Record<
42
+ string,
43
+ {
44
+ readonly reach: string
45
+ readonly address: string | null
46
+ readonly sends: readonly { readonly secret: string; readonly header: string; readonly prefix: string }[]
47
+ readonly calls: Readonly<Record<string, { readonly method: string; readonly path: string }>>
48
+
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: addressOf(one, secrets)?.id ?? null,
58
+ sends: credentialsOf(one, secrets).map((each) => ({
59
+ secret: each.id,
60
+ header: each.sends?.header ?? '',
61
+ prefix: each.sends?.prefix ?? '',
62
+ })),
63
+
64
+ calls: Object.fromEntries(
65
+ mine.map((each) => [each.id.replace(/^cl_/, ''), { method: each.method, path: each.path }]),
66
+ ),
67
+ }
68
+ }
69
+ return out
70
+ }
71
+
72
+ const OWN = '$'
73
+
74
+ const ENTERED = `${OWN}at`
75
+
76
+ const PLAIN = /^(?:[A-Za-z_][A-Za-z0-9_]*|'(?:[^'\\]|\\.)*')$/
77
+
78
+ export function parts(text: string): readonly string[] {
79
+ const code = inCode(text)
80
+ const found: string[] = []
81
+ let depth = 0
82
+ let from = 0
83
+ for (let at = 0; at < text.length; at += 1) {
84
+ if (code[at] !== true) continue
85
+ const one = text[at]
86
+ if (one === '(' || one === '[' || one === '{') depth += 1
87
+ else if (one === ')' || one === ']' || one === '}') depth -= 1
88
+ else if (one === ',' && depth === 0) {
89
+ found.push(text.slice(from, at))
90
+ from = at + 1
91
+ }
92
+ }
93
+ found.push(text.slice(from))
94
+ return found.map((one) => one.trim()).filter((one) => one !== '')
95
+ }
96
+
97
+ const inside = (text: string, open: string, shut: string): string | undefined => {
98
+ const said = text.trim()
99
+ return said.startsWith(open) && said.endsWith(shut) ? said.slice(1, -1) : undefined
100
+ }
101
+
102
+ function keyed(text: string): { readonly key: string; readonly held: string } | undefined {
103
+ const code = inCode(text)
104
+ let depth = 0
105
+ for (let at = 0; at < text.length; at += 1) {
106
+ if (code[at] !== true) continue
107
+ const one = text[at]
108
+ if (one === '(' || one === '[' || one === '{') depth += 1
109
+ else if (one === ')' || one === ']' || one === '}') depth -= 1
110
+ else if (one === ':' && depth === 0) return { key: text.slice(0, at).trim(), held: text.slice(at + 1).trim() }
111
+ }
112
+ return undefined
113
+ }
114
+
115
+ const OPENS = 'const $ = '
116
+
117
+ const ROLES = {
118
+ block: ['hands', 'held'],
119
+ over: ['held'],
120
+ item: ['item', 'hands', 'held'],
121
+ again: ['pass', 'carried', 'hands', 'held'],
122
+ } as const satisfies Readonly<Record<string, readonly string[]>>
123
+
124
+ interface Read {
125
+ readonly arrow: ts.ArrowFunction
126
+ readonly held: string | undefined
127
+ }
128
+
129
+ function arrowIn(text: string, roles: readonly string[]): Read | string {
130
+ const file = ts.createSourceFile('block.ts', `${OPENS}${text}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
131
+ const first = file.statements[0]
132
+ if (first === undefined || !ts.isVariableStatement(first)) return 'is not written as a function where it is used'
133
+ const held = first.declarationList.declarations[0]?.initializer
134
+ if (held === undefined || !ts.isArrowFunction(held)) return 'is not an arrow function written where it is used'
135
+ if (held.parameters.length > roles.length) {
136
+ return `takes ${String(held.parameters.length)} things, and this kind of block is handed ${roles.join(', ')}`
137
+ }
138
+ for (const one of held.parameters) {
139
+ if (!ts.isIdentifier(one.name)) return 'names a parameter by taking it apart, and this needs the name itself'
140
+ }
141
+ const last = held.parameters.length === roles.length ? held.parameters[roles.length - 1] : undefined
142
+ const named = last !== undefined && ts.isIdentifier(last.name) ? last.name.text : undefined
143
+ return { arrow: held, held: named }
144
+ }
145
+
146
+ const BINDS = new Set<ts.SyntaxKind>([
147
+ ts.SyntaxKind.Parameter,
148
+ ts.SyntaxKind.VariableDeclaration,
149
+ ts.SyntaxKind.BindingElement,
150
+ ts.SyntaxKind.FunctionDeclaration,
151
+ ts.SyntaxKind.ClassDeclaration,
152
+ ])
153
+
154
+ function bound(node: ts.Node, found: Set<string>): void {
155
+ if (BINDS.has(node.kind)) {
156
+ const said = (node as { readonly name?: ts.Node }).name
157
+ if (said !== undefined && ts.isIdentifier(said)) found.add(said.text)
158
+ }
159
+ if (ts.isCatchClause(node) && node.variableDeclaration !== undefined && ts.isIdentifier(node.variableDeclaration.name)) {
160
+ found.add(node.variableDeclaration.name.text)
161
+ }
162
+ node.forEachChild((one) => {
163
+ bound(one, found)
164
+ })
165
+ }
166
+
167
+ function answers(node: ts.Node, found: ts.ReturnStatement[]): void {
168
+ if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node)) return
169
+ if (ts.isReturnStatement(node)) found.push(node)
170
+ node.forEachChild((one) => {
171
+ answers(one, found)
172
+ })
173
+ }
174
+
175
+ interface Spliced {
176
+ readonly form: 'expression' | 'statements' | 'call'
177
+
178
+ readonly text: string
179
+
180
+ readonly whole: string
181
+ }
182
+
183
+ function spliced(
184
+ text: string,
185
+ names: ReadonlySet<string>,
186
+ mine: ReadonlySet<string>,
187
+ roles: readonly string[],
188
+ ): Spliced | string {
189
+ const read = arrowIn(text, roles)
190
+ if (typeof read === 'string') return read
191
+
192
+ const inner = new Set<string>()
193
+ bound(read.arrow.body, inner)
194
+
195
+ if (read.held !== undefined && inner.has(read.held)) {
196
+ 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`
197
+ }
198
+
199
+ const shadows = new Set<string>(inner)
200
+ for (const one of read.arrow.parameters) {
201
+ if (ts.isIdentifier(one.name) && one.name.text !== read.held) shadows.add(one.name.text)
202
+ }
203
+
204
+ for (const one of shadows) {
205
+ if (mine.has(one)) {
206
+ 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`
207
+ }
208
+ }
209
+
210
+ interface Edit {
211
+ readonly from: number
212
+ readonly to: number
213
+ readonly text: string
214
+ }
215
+
216
+ const edits: Edit[] = []
217
+ let because: string | undefined
218
+
219
+ const walk = (node: ts.Node): void => {
220
+ if (because !== undefined) return
221
+ if (read.held !== undefined && ts.isIdentifier(node) && node.text === read.held) {
222
+ const up: ts.Node | undefined = node.parent
223
+ if (up !== undefined && ts.isPropertyAccessExpression(up) && up.expression === node) {
224
+ const said = up.name.text
225
+ if (said !== 'input' && !names.has(said)) {
226
+
227
+ because =
228
+ `reads \`${read.held}.${said}\`, and no node before it answers to that name. ` +
229
+ `\`${read.held}\` holds \`input\`, which is what this operation was called with, ` +
230
+ (names.size === 0
231
+ ? 'and nothing else — this is the first node, so `input` is all there is'
232
+ : `and one name per node that has already answered: ${[...names].map((one) => `\`${one}\``).join(', ')}`)
233
+ return
234
+ }
235
+ edits.push({ from: up.getStart(), to: up.getEnd(), text: said })
236
+ return
237
+ }
238
+ 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`
239
+ return
240
+ }
241
+ node.forEachChild(walk)
242
+ }
243
+ walk(read.arrow.body)
244
+ if (because !== undefined) return because
245
+
246
+ const held = read.arrow.parameters[roles.length - 1]
247
+ const before = read.arrow.parameters[roles.length - 2]
248
+ if (held !== undefined) {
249
+ edits.push({ from: before?.getEnd() ?? held.getStart(), to: held.getEnd(), text: '' })
250
+ }
251
+
252
+ let said = `${OPENS}${text}`
253
+ for (const one of [...edits].sort((a, b) => b.from - a.from)) {
254
+ said = `${said.slice(0, one.from)}${one.text}${said.slice(one.to)}`
255
+ }
256
+
257
+ const whole = said.slice(OPENS.length)
258
+ const again = arrowIn(whole, roles.slice(0, -1))
259
+ if (typeof again === 'string') return again
260
+ const body = again.arrow.body
261
+
262
+ if (again.arrow.parameters.length > 1) return { form: 'call', text: whole, whole }
263
+
264
+ if (!ts.isBlock(body)) return { form: 'expression', text: body.getText(), whole }
265
+
266
+ const found: ts.ReturnStatement[] = []
267
+ answers(body, found)
268
+ const last = body.statements[body.statements.length - 1]
269
+ if (found.length === 1 && last !== undefined && found[0] === last && ts.isReturnStatement(last)) {
270
+ const kept = body.statements.slice(0, -1).map((one) => one.getText())
271
+ return { form: 'statements', text: [...kept, `${ANSWERS} = ${last.expression?.getText() ?? 'undefined'}`].join('\n'), whole }
272
+ }
273
+
274
+ return { form: 'call', text: whole, whole }
275
+ }
276
+
277
+ const ANSWERS = `${OWN}ANSWER`
278
+
279
+ interface Made {
280
+ readonly runs: readonly string[]
281
+ }
282
+
283
+ interface Shape {
284
+ readonly all: string
285
+ readonly reads: string
286
+ readonly names: ReadonlySet<string>
287
+ readonly mine: ReadonlySet<string>
288
+ }
289
+
290
+ function answered(name: string, one: Spliced, hands: string, kind: 'const' | 'let'): readonly string[] {
291
+ if (one.form === 'expression') return [`${kind} ${name}: unknown = ${one.text}`]
292
+ if (one.form === 'statements') {
293
+ return [`let ${name}: unknown`, '{', ...one.text.split('\n').map((line) => ` ${line.replace(ANSWERS, name)}`), '}']
294
+ }
295
+ return [`${kind} ${name}: unknown = await ${asFn(one.whole, hands)}(hands)`]
296
+ }
297
+
298
+ const asFn = (text: string, hands: string, more: string = ''): string =>
299
+ `((${text}) as (${more}hands: ${hands}) => Promise<unknown>)`
300
+
301
+ function forked(name: string, said: readonly string[], shape: Shape): Made | string {
302
+ if (said.length !== 2) return `takes a chooser and the ways it chooses between, and was handed ${String(said.length)} thing(s)`
303
+
304
+ const chose = spliced(said[0] ?? '', shape.names, shape.mine, ROLES.block)
305
+ if (typeof chose === 'string') return `chooses with a block that ${chose}`
306
+
307
+ const held = inside(said[1] ?? '', '{', '}')
308
+ const ways = held === undefined ? undefined : parts(held).map((one) => keyed(one))
309
+ const named = ways?.every((one) => one !== undefined && PLAIN.test(one.key)) === true ? ways : undefined
310
+ if (named === undefined || named.length === 0) return 'names its ways in a way this cannot read — write each one as `name: block`'
311
+
312
+ const picked = `${OWN}${name}_said`
313
+ const runs = [
314
+ `let ${name}: unknown`,
315
+ chose.form === 'expression'
316
+ ? `const ${picked}: string = await (${chose.text})`
317
+ : `const ${picked}: string = await ((${said[0] ?? ''}) as (hands: ${shape.reads}) => Promise<string>)(hands)`,
318
+ `switch (${picked}) {`,
319
+ ]
320
+
321
+ for (const way of named) {
322
+ const each = spliced(way?.held ?? '', shape.names, shape.mine, ROLES.block)
323
+ if (typeof each === 'string') return `has a way \`${way?.key ?? ''}\` that ${each}`
324
+ runs.push(` case ${(way?.key ?? '').startsWith("'") ? (way?.key ?? '') : quoted(way?.key ?? '')}: {`)
325
+ if (each.form === 'expression') runs.push(` ${name} = ${each.text}`)
326
+ else if (each.form === 'statements') runs.push(...each.text.split('\n').map((line) => ` ${line.replace(ANSWERS, name)}`))
327
+ else runs.push(` ${name} = await ${asFn(each.whole, shape.all)}(hands)`)
328
+ runs.push(' break')
329
+ runs.push(' }')
330
+ }
331
+
332
+ runs.push(` default: throw new Error(\`${name} chose \${${picked}}, which names no way forward\`)`)
333
+ runs.push('}')
334
+ return { runs }
335
+ }
336
+
337
+ const SHAPES: Readonly<Record<string, (name: string, said: readonly string[], shape: Shape) => Made | string>> = {
338
+ nd_read: (name, said, shape) => plainly(name, said, shape, shape.reads, false),
339
+ nd_action: (name, said, shape) => plainly(name, said, shape, shape.all, false),
340
+ nd_stop: (name, said, shape) => plainly(name, said, shape, shape.reads, true),
341
+
342
+ nd_each: (name, said, shape) => {
343
+ if (said.length !== 2) return `takes what to run over and the block to run, and was handed ${String(said.length)} thing(s)`
344
+ const over = spliced(said[0] ?? '', shape.names, shape.mine, ROLES.over)
345
+ const block = spliced(said[1] ?? '', shape.names, shape.mine, ROLES.item)
346
+ if (typeof block === 'string') return `runs a block that ${block}`
347
+ if (typeof over === 'string') return `runs over a block that ${over}`
348
+ const item = `${OWN}${name}_item`
349
+ const all = `${OWN}${name}_all`
350
+ const each = `${OWN}${name}_each`
351
+ return {
352
+ runs: [
353
+ `const ${all}: unknown[] = []`,
354
+ `const ${each} = (${block.whole}) as (item: unknown, hands: ${shape.all}) => Promise<unknown>`,
355
+ `for (const ${item} of ${over.form === 'expression' ? `(${over.text})` : `((${over.whole}) as () => readonly unknown[])()`} as readonly unknown[]) {`,
356
+ ` ${all}.push(await ${each}(${item}, hands))`,
357
+ '}',
358
+ `const ${name}: unknown = ${all}`,
359
+ ],
360
+ }
361
+ },
362
+
363
+ nd_parallel: (name, said, shape) => {
364
+ if (said.length !== 1) return `takes one list of blocks, and was handed ${String(said.length)} thing(s)`
365
+ const held = inside(said[0] ?? '', '[', ']')
366
+ if (held === undefined) return 'takes a list of blocks written out where it is called, and this is not one'
367
+ const every = parts(held)
368
+ if (every.length === 0) return 'was handed no blocks to run at once'
369
+
370
+ const calls: string[] = []
371
+ for (const [order, one] of every.entries()) {
372
+ const each = spliced(one, shape.names, shape.mine, ROLES.block)
373
+ if (typeof each === 'string') return `runs a block at position ${String(order + 1)} that ${each}`
374
+ calls.push(each.form === 'expression' ? `(async () => ${each.text})()` : `${asFn(each.whole, shape.all)}(hands)`)
375
+ }
376
+ return { runs: [`const ${name}: unknown = await Promise.all([${calls.join(', ')}])`] }
377
+ },
378
+
379
+ nd_repeat: (name, said, shape) => {
380
+ if (said.length !== 2) return `takes a ceiling and the block to repeat, and was handed ${String(said.length)} thing(s)`
381
+ const block = spliced(said[1] ?? '', shape.names, shape.mine, ROLES.again)
382
+ if (typeof block === 'string') return `repeats a block that ${block}`
383
+ const ceiling = `${OWN}${name}_ceiling`
384
+ const again = `${OWN}${name}_again`
385
+ const carried = `${OWN}${name}_carried`
386
+ const done = `${OWN}${name}_done`
387
+ const pass = `${OWN}${name}_pass`
388
+ const answer = `${OWN}${name}_said`
389
+ return {
390
+ runs: [
391
+ `let ${name}: unknown`,
392
+ `const ${ceiling}: number = ${said[0] ?? ''}`,
393
+ `const ${again} = (${block.whole}) as (pass: number, carried: unknown, hands: ${shape.all}) => Promise<{ readonly done: boolean; readonly value: unknown }>`,
394
+ `let ${carried}: unknown`,
395
+ `let ${done} = false`,
396
+ `for (let ${pass} = 0; ${pass} < ${ceiling}; ${pass} += 1) {`,
397
+ ` const ${answer} = await ${again}(${pass}, ${carried}, hands)`,
398
+ ` if (${answer}.done) { ${name} = ${answer}.value; ${done} = true; break }`,
399
+ ` ${carried} = ${answer}.value`,
400
+ '}',
401
+ `if (!${done}) throw new Error(\`${name} reached its ceiling of \${String(${ceiling})} passes without answering done\`)`,
402
+ ],
403
+ }
404
+ },
405
+
406
+ nd_branch: forked,
407
+ }
408
+
409
+ const STOPS = "Symbol.for('thread.operation.stopped')"
410
+
411
+ function plainly(name: string, said: readonly string[], shape: Shape, hands: string, ends: boolean): Made | string {
412
+ if (said.length !== 1) return `takes one block, and was handed ${String(said.length)} thing(s)`
413
+ const one = spliced(said[0] ?? '', shape.names, shape.mine, ROLES.block)
414
+ if (typeof one === 'string') return one
415
+
416
+ if (!ends) return { runs: answered(name, one, hands, 'const') }
417
+
418
+ return {
419
+ runs: [
420
+ ...answered(name, one, hands, 'const'),
421
+ `throw Object.assign(new Error(${quoted(`${name} ended the operation`)}), { [${STOPS}]: true, value: ${name} })`,
422
+ ],
423
+ }
424
+ }
425
+
426
+ function compiled(
427
+ at: string,
428
+ id: string,
429
+ one: ReturnType<typeof operations> extends ReadonlyMap<string, infer W> ? W : never,
430
+ known: ReadonlySet<string>,
431
+ ): { readonly text?: string; readonly because?: string } {
432
+ const because = problem(id, one)
433
+ if (because !== undefined) return { because }
434
+
435
+ const takes = one.takes[0]?.at
436
+ if (takes === undefined) return { because: `\`${id}\` seeds what it takes and states no file for it` }
437
+
438
+ const only = one.requires.length === 0 ? 'never' : one.requires.map((each) => quoted(each)).join(' | ')
439
+ const mine = new Set<string>(['input', ...one.nodes.map((row) => String(row.id))])
440
+
441
+ const runs: string[] = []
442
+ const names = new Set<string>()
443
+ let last: string | undefined
444
+
445
+ for (const [order, row] of one.nodes.entries()) {
446
+ const name = row.id
447
+ const from = builtFrom(row, known)
448
+
449
+ if (from === 'nd_stop' && order !== one.nodes.length - 1) {
450
+ const after = one.nodes.slice(order + 1).map((each) => `\`${String(each.id)}\``)
451
+ return {
452
+ because:
453
+ `the node \`${String(name)}\` ends \`${id}\`, and ${after.join(', ')} ${after.length === 1 ? 'is' : 'are'} declared after it. ` +
454
+ 'Nothing runs after the node that ends an operation — move it last, or take off what follows it',
455
+ }
456
+ }
457
+
458
+ if (name === undefined) return { because: `\`${id}\` declares a node with no id of its own` }
459
+ if (row.at === undefined) return { because: `the node \`${name}\` names no file of its own` }
460
+ if (from === undefined) return { because: `the node \`${name}\` names no type this app declares` }
461
+
462
+ const path = resolve(ROOT, row.at)
463
+ if (!existsSync(path)) return { because: `the node \`${name}\` seeds ${row.at} and nothing is there — run \`thread seed\`` }
464
+ const held = blockIn(readFileSync(path, 'utf8'))
465
+ if (held === undefined) {
466
+ return { because: `the node \`${name}\` states no block this can read in ${row.at} — run \`thread seed\` and write it again` }
467
+ }
468
+
469
+ const shape: Shape = { all: `Handed<${only}>`, reads: `Reads<Handed<${only}>>`, names, mine }
470
+ const made = (SHAPES[from] ?? ownType)(name, parts(held), shape)
471
+ if (typeof made === 'string') return { because: `the node \`${name}\` ${made}` }
472
+
473
+ runs.push(` ${ENTERED} = ${quoted(name)}`)
474
+ for (const line of made.runs) runs.push(` ${line}`)
475
+ names.add(name)
476
+ last = from === 'nd_stop' ? undefined : name
477
+ }
478
+
479
+ return {
480
+ text: [
481
+ "import type { z } from 'zod'",
482
+ '',
483
+ `import type { Operation } from ${quoted(runtime(at, '../operations.ts'))}`,
484
+ `import type { Handed, Reads } from ${quoted(runtime(at, '../seams.ts'))}`,
485
+ '',
486
+ `import { estimate, input, resource } from ${quoted(beside(at, takes))}`,
487
+ '',
488
+ `export const operation: Operation<z.infer<typeof input>, ${only}> = {`,
489
+ ` id: ${quoted(id)},`,
490
+ ' input,',
491
+ ' estimate,',
492
+ ' resource,',
493
+ ` requires: [${one.requires.map((each) => quoted(each)).join(', ')}],`,
494
+ '',
495
+ ' run: async (hands, held) => {',
496
+ ' const input = held.input',
497
+ ` let ${ENTERED} = ''`,
498
+ ' try {',
499
+ ...runs,
500
+ ...(last === undefined ? [] : [` return ${last}`]),
501
+ ' } catch (thrown) {',
502
+ ` if (typeof thrown === 'object' && thrown !== null) Object.assign(thrown, { node: ${ENTERED} })`,
503
+ ' throw thrown',
504
+ ' }',
505
+ ' },',
506
+ '}',
507
+ '',
508
+ ].join('\n'),
509
+ }
510
+ }
511
+
512
+ const ownType = (name: string, said: readonly string[], shape: Shape): Made | string =>
513
+ plainly(name, said, shape, shape.all, false)
514
+
515
+ interface Wanted {
516
+ readonly at: string
517
+ readonly text?: string
518
+ readonly because?: string
519
+ }
520
+
521
+ async function planned(): Promise<readonly Wanted[]> {
522
+ const pass = descend()
523
+ const known = types(pass)
524
+ const reach = await reaching()
525
+ const found: Wanted[] = []
526
+
527
+ const answers = Object.keys(reach).filter((one) => Object.keys(reach[one]?.calls ?? {}).length > 0)
528
+ const may = [...SUPPLIES, ...answers]
529
+ for (const [id, one] of operations(pass, known)) {
530
+ if (one.body === undefined) continue
531
+ const silent = one.requires.filter((each) => reach[each] !== undefined && !answers.includes(each))
532
+ if (silent.length > 0) {
533
+ found.push({
534
+ at: one.body,
535
+ because: silent
536
+ .map((each) => {
537
+ const how = reach[each]?.reach ?? 'nothing known'
538
+ const instead =
539
+ how === 'module' || how === 'cli'
540
+ ? 'run it with `thread run ' + each.replace(/^[a-z]+_/, '') + '`'
541
+ : 'reach the rows it keeps with `sm_store`'
542
+ return (
543
+ '`' + id + '` requires `' + each + '`, which is reached over ' + how + ' and answers no call, so there ' +
544
+ 'is nothing for a node to be handed. ' + instead + ', give it a call with `thread create target call`, ' +
545
+ 'or take the word off that operation\'s row'
546
+ )
547
+ })
548
+ .join('; '),
549
+ })
550
+ continue
551
+ }
552
+ const stray = one.requires.filter((each) => !may.includes(each))
553
+ if (stray.length > 0) {
554
+ found.push({
555
+ at: one.body,
556
+ because:
557
+ `\`${id}\` requires ${stray.map((each) => `\`${each}\``).join(', ')}, and this app has no such capability and ` +
558
+ `declares no such target. What may be required is ${may.join(', ')} — declare the target with ` +
559
+ '`thread create target`, or take the word off that operation\'s row',
560
+ })
561
+ continue
562
+ }
563
+ found.push({ at: one.body, ...compiled(one.body, id, one, known) })
564
+ }
565
+
566
+ if (found.length > 0) found.push({ at: DISPATCH_AT, text: dispatched(reach) })
567
+ return found
568
+ }
569
+
570
+ function checked(plan: readonly Wanted[]): void {
571
+ const findings: Finding[] = plan.flatMap((one): Finding[] => {
572
+ const path = resolve(ROOT, one.at)
573
+
574
+ if (one.because !== undefined) {
575
+ return [
576
+ {
577
+ file: one.at,
578
+ detail: one.because,
579
+ instead: 'declare it properly — this file is compiled from those rows and cannot be written without them',
580
+ },
581
+ ]
582
+ }
583
+ if (!existsSync(path)) {
584
+ return [{ file: one.at, detail: 'a row declares this file and nothing is there', instead: 'run `thread compile`' }]
585
+ }
586
+ if (readFileSync(path, 'utf8') === one.text) return []
587
+
588
+ if (one.at === DISPATCH_AT) {
589
+ return [
590
+ {
591
+ file: one.at,
592
+ detail: 'this is the one way every operation of this app runs, and it is no longer the bytes that write it',
593
+ instead:
594
+ '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',
595
+ },
596
+ ]
597
+ }
598
+
599
+ return [
600
+ {
601
+ file: one.at,
602
+ detail: 'this operation is compiled from its nodes, and this is no longer the bytes that compile it',
603
+ instead:
604
+ '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',
605
+ },
606
+ ]
607
+ })
608
+
609
+ runChecker('compile', findings, `${String(plan.length)} file(s) compiled`)
610
+ exit(findings.length === 0 ? 0 : 1)
611
+ }
612
+
613
+ export async function compile(say: boolean = true): Promise<number> {
614
+ const plan = await planned()
615
+ let written = 0
616
+ let refused = 0
617
+ for (const one of plan) {
618
+ if (one.text === undefined) {
619
+ refused += 1
620
+ if (say) stdout.write(`compile: ${one.at} was not written — ${one.because ?? 'the rows behind it are incomplete'}\n`)
621
+ continue
622
+ }
623
+ const path = resolve(ROOT, one.at)
624
+ if (existsSync(path) && readFileSync(path, 'utf8') === one.text) continue
625
+ mkdirSync(dirname(path), { recursive: true })
626
+ writeFileSync(path, one.text, 'utf8')
627
+ written += 1
628
+ if (say) stdout.write(`compile: wrote ${one.at}\n`)
629
+ }
630
+ if (say) {
631
+ stdout.write(
632
+ `compile: ${String(written)} of ${String(plan.length)} file(s) written, ${String(plan.length - written - refused)} already as compiled` +
633
+ `${refused === 0 ? '' : `, ${String(refused)} refused`}.\n`,
634
+ )
635
+ }
636
+ if (refused > 0) exit(1)
637
+ return written
638
+ }
639
+
640
+ if (invoked(import.meta.url)) {
641
+ if (argv.includes('--check')) checked(await planned())
642
+ else await compile()
643
+ }