@prestack/thread 0.1.20 → 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 (148) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.d.ts +2 -0
  4. package/dist/core/descend.js +18 -3
  5. package/dist/core/generators/commit.js +55 -3
  6. package/dist/core/generators/dev.d.ts +1 -0
  7. package/dist/core/generators/dev.js +68 -0
  8. package/dist/core/rows.d.ts +5 -0
  9. package/dist/core/rows.js +45 -0
  10. package/dist/core/tree.d.ts +1 -0
  11. package/dist/core/tree.js +7 -1
  12. package/dist/derivation/generators/build.d.ts +3 -1
  13. package/dist/derivation/generators/build.js +103 -22
  14. package/dist/derivation/generators/release.js +74 -26
  15. package/dist/derivation/release.js +3 -3
  16. package/dist/integrations/cli.d.ts +3 -9
  17. package/dist/integrations/cli.js +96 -36
  18. package/dist/integrations/declared.d.ts +8 -0
  19. package/dist/integrations/declared.js +22 -0
  20. package/dist/integrations/pglite.js +13 -3
  21. package/dist/integrations/target.d.ts +4 -1
  22. package/dist/integrations/target.js +20 -6
  23. package/dist/integrity/attach.js +1 -5
  24. package/dist/integrity/generators/publish.js +2 -1
  25. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  26. package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
  27. package/dist/operations/access.js +3 -2
  28. package/dist/operations/capabilities.d.ts +2 -0
  29. package/dist/operations/capabilities.js +2 -0
  30. package/dist/operations/generators/capability.js +234 -24
  31. package/dist/operations/generators/compile.d.ts +21 -0
  32. package/dist/operations/generators/compile.js +151 -51
  33. package/dist/operations/generators/permits.d.ts +2 -1
  34. package/dist/operations/generators/permits.js +52 -47
  35. package/dist/operations/generators/seed.d.ts +3 -0
  36. package/dist/operations/generators/seed.js +25 -3
  37. package/dist/operations/nodes.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/core/actor.ts +0 -21
  40. package/dist/core/attach.ts +0 -613
  41. package/dist/core/chain.ts +0 -145
  42. package/dist/core/condition.ts +0 -227
  43. package/dist/core/descend.ts +0 -1173
  44. package/dist/core/errors/catalogue.ts +0 -314
  45. package/dist/core/errors/classes.ts +0 -51
  46. package/dist/core/errors/error.ts +0 -152
  47. package/dist/core/errors/namespaces.ts +0 -19
  48. package/dist/core/finding.ts +0 -37
  49. package/dist/core/generators/commit.ts +0 -157
  50. package/dist/core/init.ts +0 -222
  51. package/dist/core/invoked.ts +0 -52
  52. package/dist/core/round.ts +0 -74
  53. package/dist/core/rows.ts +0 -459
  54. package/dist/core/scope.ts +0 -20
  55. package/dist/core/thread.ts +0 -29
  56. package/dist/core/tree.ts +0 -90
  57. package/dist/core/types.ts +0 -351
  58. package/dist/derivation/attach.ts +0 -57
  59. package/dist/derivation/generators/build.ts +0 -371
  60. package/dist/derivation/generators/check.ts +0 -118
  61. package/dist/derivation/generators/determinism.ts +0 -145
  62. package/dist/derivation/generators/release.ts +0 -236
  63. package/dist/derivation/release.ts +0 -43
  64. package/dist/integrations/attach.ts +0 -187
  65. package/dist/integrations/cli.ts +0 -615
  66. package/dist/integrations/disk.ts +0 -466
  67. package/dist/integrations/generators/read.ts +0 -127
  68. package/dist/integrations/generators/vault.ts +0 -210
  69. package/dist/integrations/https.ts +0 -216
  70. package/dist/integrations/pglite.ts +0 -206
  71. package/dist/integrations/secret.ts +0 -54
  72. package/dist/integrations/target.ts +0 -656
  73. package/dist/integrations/vault.ts +0 -148
  74. package/dist/integrity/attach.ts +0 -150
  75. package/dist/integrity/check-threads.ts +0 -166
  76. package/dist/integrity/constraint.ts +0 -1246
  77. package/dist/integrity/generators/compiler.ts +0 -138
  78. package/dist/integrity/generators/gate.ts +0 -227
  79. package/dist/integrity/generators/issues.ts +0 -160
  80. package/dist/integrity/generators/publish.ts +0 -232
  81. package/dist/integrity/generators/trial.ts +0 -359
  82. package/dist/integrity/lint.ts +0 -214
  83. package/dist/integrity/lsp/bin.ts +0 -44
  84. package/dist/integrity/lsp/diagnostics.ts +0 -92
  85. package/dist/integrity/lsp/findings.ts +0 -63
  86. package/dist/integrity/lsp/lint.ts +0 -124
  87. package/dist/integrity/lsp/protocol.ts +0 -78
  88. package/dist/integrity/lsp/server.ts +0 -224
  89. package/dist/integrity/slopnator.ts +0 -324
  90. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  91. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  92. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  93. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  94. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  95. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  96. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  97. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  98. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  99. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  100. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  101. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  102. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  103. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  104. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  105. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  106. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  107. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  108. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  109. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  110. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  111. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  112. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  113. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  114. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  115. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  116. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  117. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  118. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  119. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  120. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  121. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  122. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  123. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  124. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  125. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  126. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  127. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  128. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  129. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  130. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  131. package/dist/integrity/sweep.ts +0 -395
  132. package/dist/knowledge/attach.ts +0 -327
  133. package/dist/knowledge/generators/docsgen.ts +0 -186
  134. package/dist/knowledge/generators/glossary.ts +0 -73
  135. package/dist/knowledge/glossary.ts +0 -6
  136. package/dist/operations/access.ts +0 -131
  137. package/dist/operations/attach.ts +0 -408
  138. package/dist/operations/capabilities.ts +0 -88
  139. package/dist/operations/dispatch.ts +0 -294
  140. package/dist/operations/error-results.ts +0 -31
  141. package/dist/operations/generators/capability.ts +0 -1054
  142. package/dist/operations/generators/compile.ts +0 -701
  143. package/dist/operations/generators/permits.ts +0 -224
  144. package/dist/operations/generators/seed.ts +0 -359
  145. package/dist/operations/generators/trial.ts +0 -666
  146. package/dist/operations/nodes.ts +0 -53
  147. package/dist/operations/operations.ts +0 -91
  148. package/dist/operations/result.ts +0 -28
@@ -1,224 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from '../../integrations/disk.ts'
2
- import { valuesIn } from '../../core/rows.ts'
3
- import { resolve } from 'node:path'
4
- import { argv, stderr, stdout } from 'node:process'
5
-
6
- import { exit } from '../../core/invoked.ts'
7
- import { PUBLIC_ROLE as PUBLIC } from '../../core/actor.ts'
8
- import { ROOT, descend } from '../../core/descend.ts'
9
- import { forbids, runChecker, type Finding } from '../../core/finding.ts'
10
- import type { Condition, Operand } from '../../core/condition.ts'
11
- import { findTree, inTree } from '../../core/tree.ts'
12
-
13
- import { PERMITS_AT, grantedTwice, opensToEverybody, unanswerable, type Grant } from '../access.ts'
14
- import { operations as woven, runtime, types } from './seed.ts'
15
-
16
- function stop(why: string): never {
17
- stderr.write(`permits: ${why}\n`)
18
- exit(1)
19
- }
20
-
21
- const CHECKING = argv.includes('--check')
22
-
23
- const pass = await descend()
24
-
25
- const stopped = [...pass.broken, ...pass.findings].filter(forbids)[0]
26
- if (stopped !== undefined) {
27
- stop(
28
- 'the descent is broken, so who may ask for what is only partly known, and a gate written from half of it ' +
29
- `would permit callers nobody granted — ${stopped.detail} (${stopped.file ?? ''})`,
30
- )
31
- }
32
-
33
- async function taken<T>(file: string, name: string, what: string): Promise<readonly T[]> {
34
- const at = inTree(findTree(), file)
35
- if (!existsSync(at)) stop(`${at} is where this app states ${what}, and it is not there`)
36
- let held: Record<string, unknown>
37
- try {
38
- held = valuesIn(readFileSync(at)) as Record<string, unknown>
39
- } catch (thrown) {
40
- stop(
41
- `${at} could not be read: ${thrown instanceof Error ? thrown.message : String(thrown)}. A tree whose access ` +
42
- 'cannot be read reports exactly like a tree that granted nothing, and a gate written from that would refuse ' +
43
- 'every caller rather than say so',
44
- )
45
- }
46
- const list = held[name]
47
- if (!Array.isArray(list)) stop(`${at} exports no \`${name}\` list, so nothing there states ${what}`)
48
- return list as readonly T[]
49
- }
50
-
51
- const grants = await taken<Grant>('access.ts', 'GRANTS', 'who may ask for what')
52
-
53
- const operations = [...woven(pass, types(pass))].map(([id]) => ({
54
- id,
55
- describes: pass.stated.get(id)?.describes ?? '',
56
- }))
57
-
58
- for (const found of [unanswerable(grants)]) {
59
- if (found.length > 0) stop(found.join('; '))
60
- }
61
-
62
- const twice = grantedTwice(grants)
63
- if (twice.length > 0) {
64
- stop(
65
- `${twice.map((one) => `\`${one}\``).join(', ')} ${twice.length === 1 ? 'is granted' : 'are granted'} more than once, ` +
66
- 'so two arms answer to one id and either may permit. One id, one policy — rename one of them, or delete the ' +
67
- 'arm that is not meant to stand',
68
- )
69
- }
70
-
71
- const open = opensToEverybody(grants)
72
- if (open.length > 0) {
73
- stop(
74
- `${open.map((one) => `\`${one.id}\``).join(', ')} names no roles or entitlements and states no condition that ` +
75
- 'reads the caller, so who may run this is left unsaid rather than decided. If it is meant to be open, say so: ' +
76
- `\`to: { roles: ['${PUBLIC}'] }\` is the role an unauthenticated caller already wears, and naming it puts ` +
77
- 'that decision in the sheet. Otherwise name the roles that may, or state a `when` saying whose row it is',
78
- )
79
- }
80
-
81
- const said = (held: unknown): string => JSON.stringify(held)
82
-
83
- const used = new Set<readonly string[]>()
84
-
85
- function reads(one: Operand, grant: Grant): string {
86
- if ('value' in one) return said(one.value)
87
- if ('now' in one) return 'Date.now()'
88
- if ('ago' in one) {
89
- const size = Number(one.ago.slice(0, -1))
90
- const unit = one.ago.slice(-1)
91
- const seconds = size * (unit === 's' ? 1 : unit === 'm' ? 60 : unit === 'h' ? 3_600 : 86_400)
92
- return `(Date.now() - ${String(seconds * 1000)})`
93
- }
94
- if ('actor' in one) return `actor.${one.actor}`
95
- if ('field' in one) {
96
- if (grant.on === undefined) {
97
- stop(
98
- `\`${grant.id}\` reads the field "${one.field}" and names no entity in \`on\`, so there is nothing saying what ` +
99
- 'the row in front of the gate is. Name the entity the resource is, or state the rule without reading a field',
100
- )
101
- }
102
- if (one.field.includes('.')) {
103
- stop(
104
- `\`${grant.id}\` reads "${one.field}", which reaches through a link. The gate is handed one row and has ` +
105
- 'nothing to follow a link with — name a value of "' +
106
- grant.on +
107
- '" itself, or state the rule as an invariant where the rows are',
108
- )
109
- }
110
- used.add(READS)
111
- return `at(resource, ${said(one.field)})`
112
- }
113
- stop(`\`${grant.id}\` states an operand this gate cannot read`)
114
- }
115
-
116
- function predicate(condition: Condition, grant: Grant): string {
117
- if ('all' in condition) {
118
- return condition.all.length === 0 ? 'true' : `(${condition.all.map((one) => predicate(one, grant)).join(' && ')})`
119
- }
120
- if ('any' in condition) {
121
- return condition.any.length === 0 ? 'false' : `(${condition.any.map((one) => predicate(one, grant)).join(' || ')})`
122
- }
123
- if ('not' in condition) return `(!${predicate(condition.not, grant)})`
124
- used.add(COMPARES)
125
- return `held(${said(condition.op)}, ${reads(condition.is, grant)}, ${reads(condition.than, grant)})`
126
- }
127
-
128
- function wears(grant: Grant): readonly string[] {
129
- const held: string[] = []
130
- const roles = grant.to?.roles
131
- if (roles !== undefined && !roles.includes(PUBLIC)) held.push(`some(actor.roles, ${said(roles)})`)
132
- if (grant.to?.entitlements !== undefined) held.push(`some(actor.entitlements, ${said(grant.to.entitlements)})`)
133
- if (held.length > 0) used.add(WEARS)
134
- return held
135
- }
136
-
137
- const READS = ['const at = (row: Row | null, name: string): unknown => (row === null ? undefined : row[name])']
138
-
139
- const WEARS = [
140
- 'const some = (worn: readonly string[], named: readonly string[]): boolean => named.some((one) => worn.includes(one))',
141
- ]
142
-
143
- const COMPARES = [
144
- 'const held = (op: string, left: unknown, right: unknown): boolean => {',
145
- ' if (op === \'in\') return Array.isArray(right) && right.includes(left)',
146
- ' if (op === \'has\') return Array.isArray(left) && left.includes(right)',
147
- ' if (op === \'eq\') return left === right',
148
- ' if (op === \'neq\') return left !== right',
149
- ' if (typeof left !== typeof right) return false',
150
- ' if (typeof left !== \'number\' && typeof left !== \'string\') return false',
151
- ' const other = right as number | string',
152
- ' if (op === \'lt\') return left < other',
153
- ' if (op === \'lte\') return left <= other',
154
- ' if (op === \'gt\') return left > other',
155
- ' return left >= other',
156
- '}',
157
- ]
158
-
159
- function permitting(): string {
160
- const arms = grants.flatMap((one) => {
161
- const parts = [...wears(one), ...(one.when === undefined ? [] : [predicate(one.when, one)])]
162
- return [
163
- ` if (${said(one.may)}.includes(action)) {`,
164
- ` if (${parts.length === 0 ? 'true' : parts.join(' && ')}) return { allow: true, because: ${said(one.describes)} }`,
165
- ` denied.push(${said(one.describes)})`,
166
- ' }',
167
- ]
168
- })
169
-
170
- const body =
171
- grants.length === 0
172
- ? [
173
- ' return {',
174
- ' allow: false,',
175
- " because: 'this app declares no grants, so nothing permits any operation. Declare one with `thread grant`',",
176
- ' }',
177
- ]
178
- : [
179
- ' const denied: string[] = []',
180
- ...arms,
181
- ' return { allow: false, because: denied[0] ?? `nothing granted permits ${action}` }',
182
- ]
183
-
184
- return [
185
- `import type { Actor } from '${runtime(PERMITS_AT, '../../core/actor.ts')}'`,
186
- "import type { Row } from '" + runtime(PERMITS_AT, '../../integrations/target.ts') + "'",
187
- '',
188
- ...[READS, WEARS, COMPARES].filter((one) => used.has(one)).flatMap((one) => [...one, '']),
189
-
190
- 'export function permits(actor: Actor, action: string, resource: Row | null): { allow: boolean; because?: string } {',
191
- ...(grants.length === 0 ? [' void actor', ' void resource'] : []),
192
- ...body,
193
- '}',
194
- '',
195
- ].join('\n')
196
- }
197
-
198
- const RUNS = 'thread permits'
199
-
200
- const permitted = resolve(ROOT, PERMITS_AT)
201
-
202
- const content = permitting()
203
-
204
- const count = `${String(grants.length)} grant(s) over ${String(operations.length)} operation(s)`
205
-
206
- if (CHECKING) {
207
- const findings: Finding[] = []
208
- const current = existsSync(permitted) ? readFileSync(permitted, 'utf8') : ''
209
- if (current !== content) {
210
- findings.push({
211
- file: PERMITS_AT,
212
- detail: 'is not what this app grants',
213
- instead: `run \`${RUNS}\` and commit what it writes. Never edit this file — a branch here exists because a grant was declared, and an edit widens or narrows access nobody agreed to`,
214
- })
215
- }
216
- runChecker('permits', findings, count)
217
- exit(findings.length === 0 ? 0 : 1)
218
- }
219
-
220
- writeFileSync(permitted, content)
221
- stdout.write(
222
- `permits: wrote ${PERMITS_AT} — ${count}. Dispatch imports this check itself, so every operation passes it; ` +
223
- '`thread grant` adds a grant, and a refusal no grant can say is an invariant on the entity, or a rule from `thread create rule`.\n',
224
- )
@@ -1,359 +0,0 @@
1
- import { existsSync, readFileSync, writeFileSync } from '../../integrations/disk.ts'
2
- import { dirname, relative, resolve } from 'node:path'
3
- import { argv, stdout } from 'node:process'
4
- import { fileURLToPath } from 'node:url'
5
-
6
- import ts from 'typescript'
7
-
8
- import { ROOT, descend, under, type Pass, type Row } from '../../core/descend.ts'
9
- import { runChecker, type Finding } from '../../core/finding.ts'
10
- import { BESIDE, exit, invoked } from '../../core/invoked.ts'
11
- import { PREFIXES } from '../../core/thread.ts'
12
-
13
- const COMPILES = 'thread compile'
14
-
15
- const OPERATIONS_ID = 'operations'
16
-
17
- const NODES_ID = 'nodes'
18
-
19
- const NAMED = /\btype:\s*'((?:[^'\\]|\\.)*)'/
20
-
21
- import { quoted } from '../../core/rows.ts'
22
-
23
- export { quoted }
24
-
25
- const slash = (one: string): string => one.split('\\').join('/')
26
-
27
- export const beside = (from: string, to: string): string => {
28
- const said = slash(relative(dirname(from), to))
29
- return said.startsWith('.') ? said : `./${said}`
30
- }
31
-
32
- export const runtime = (at: string, file: string): string =>
33
- beside(resolve(ROOT, at), fileURLToPath(new URL(BESIDE(file.replace(/\.ts$/, '')), import.meta.url)))
34
-
35
- const FRESH: Readonly<Record<string, (throws: string) => string>> = {
36
- nd_read: (throws) => `async () => {\n ${throws}\n}`,
37
- nd_action: (throws) => `async () => {\n ${throws}\n}`,
38
- nd_stop: (throws) => `async () => {\n ${throws}\n}`,
39
- nd_each: (throws) => `() => [], async () => {\n ${throws}\n}`,
40
- nd_repeat: (throws) => `1, async (pass, carried) => {\n ${throws}\n}`,
41
- nd_parallel: (throws) => `[\n async () => {\n ${throws}\n },\n]`,
42
- nd_branch: (throws) => `async () => 'one', {\n one: async () => {\n ${throws}\n },\n}`,
43
- }
44
-
45
- const SHIPPED = new Set(Object.keys(FRESH))
46
-
47
- const isType = (pass: Pass, id: string): boolean => under(pass, id, NODES_ID)
48
-
49
- const isOurs = (pass: Pass, id: string): boolean => under(pass, id, OPERATIONS_ID)
50
-
51
- const isOperation = (pass: Pass, id: string): boolean =>
52
- Object.values(PREFIXES).some((one) => id.startsWith(one)) && isOurs(pass, id)
53
-
54
- export function types(pass: Pass): ReadonlySet<string> {
55
- const found = new Set<string>(SHIPPED)
56
- const named = new Set<string>()
57
- for (const threadings of pass.threaded.values()) {
58
- for (const one of threadings) if (one.row.id !== undefined) named.add(one.row.id)
59
- }
60
-
61
- for (const one of pass.coined.keys()) {
62
- if (one !== NODES_ID && !named.has(one) && under(pass, one, NODES_ID)) found.add(one)
63
- }
64
- return found
65
- }
66
-
67
- export function builtFrom(row: Row, known: ReadonlySet<string>): string | undefined {
68
- const found = (row.requires ?? []).filter((one) => known.has(one))
69
- return found.length === 1 ? found[0] : undefined
70
- }
71
-
72
- interface Woven {
73
- readonly nodes: readonly Row[]
74
- readonly takes: readonly Row[]
75
- readonly body?: string
76
-
77
- readonly requires: readonly string[]
78
- readonly stray: readonly string[]
79
-
80
- readonly timeout?: number
81
- }
82
-
83
- const reaches = (pass: Pass, one: string): boolean => pass.stated.has(one) && !under(pass, one, NODES_ID)
84
-
85
- const waiting = (held: ts.Expression | undefined): number | undefined =>
86
- held !== undefined && ts.isNumericLiteral(held) ? Number(held.text.replace(/_/g, '')) : undefined
87
-
88
- export function operations(pass: Pass, known: ReadonlySet<string>): ReadonlyMap<string, Woven> {
89
- const found = new Map<string, Woven & { nodes: Row[]; takes: Row[]; body?: string; wants: ReadonlySet<string> }>()
90
- const held = (id: string): { nodes: Row[]; takes: Row[]; body?: string; wants: ReadonlySet<string> } => {
91
- const one = found.get(id)
92
- if (one !== undefined) return one
93
- const stated = pass.stated.get(id)
94
- const asked = stated?.requires ?? []
95
- const made = {
96
- nodes: [] as Row[],
97
- takes: [] as Row[],
98
- body: undefined as string | undefined,
99
- wants: new Set(stated?.takes ?? []),
100
- requires: asked.filter((one) => reaches(pass, one)),
101
- stray: asked.filter((one) => !reaches(pass, one)),
102
- timeout: waiting(stated?.fields.get('timeout')),
103
- }
104
- found.set(id, made)
105
- return made
106
- }
107
-
108
- for (const [at, threadings] of pass.threaded) {
109
- for (const one of threadings) {
110
- if (!isOperation(pass, one.by)) continue
111
-
112
- if (one.list === 'derives' && one.row.by === COMPILES) held(one.by).body = at
113
- else if (one.list === 'seeds') {
114
- const mine = held(one.by)
115
- if (mine.wants.has(one.row.id ?? '')) mine.takes.push(one.row)
116
- else mine.nodes.push(one.row)
117
- }
118
- }
119
- }
120
- return found
121
- }
122
-
123
- export const problem = (id: string, one: Woven): string | undefined => {
124
- if (one.nodes.length === 0) return `\`${id}\` is an operation that seeds no nodes, and an operation is its nodes`
125
- if (one.takes.length === 0) {
126
- return (
127
- `\`${id}\` names nothing it takes, and an operation takes one thing — its input and the row it touches. ` +
128
- `Write \`takes: ['${id}_takes']\` on its row beside the file that row seeds`
129
- )
130
- }
131
- if (one.takes.length > 1) return `\`${id}\` takes ${String(one.takes.length)} things, and it takes one`
132
- if (one.stray.length > 0) {
133
- return (
134
- `\`${id}\` requires ${one.stray.join(', ')}, and dispatch can supply neither a capability nor a target by that ` +
135
- `name. What an operation may require is a target this app declares — ` +
136
- 'declare one with `thread create target`'
137
- )
138
- }
139
- return undefined
140
- }
141
-
142
- const takesFile = (at: string): string =>
143
- [
144
- "import { z } from 'zod'",
145
- '',
146
- 'import type { Row } from ' + quoted(runtime(at, '../../integrations/target.ts')),
147
- '',
148
- 'export const input = z.unknown()',
149
- '',
150
- 'export const resource = (): Row | null => null',
151
- '',
152
- 'export type In = z.infer<typeof input>',
153
- '',
154
- ].join('\n')
155
-
156
- const OPENS = 'export const node = '
157
-
158
- export function blockIn(text: string): string | undefined {
159
- const at = text.indexOf(OPENS)
160
- if (at === -1) return undefined
161
- const open = text.indexOf('(', at)
162
- const shut = text.lastIndexOf(')')
163
- if (open === -1 || shut <= open) return undefined
164
- return text.slice(open + 1, shut)
165
- }
166
-
167
- export function scaffold(from: string, name: string): string {
168
- const throws = `throw new Error(${quoted(`the node ${name} was scaffolded and never written`)})`
169
- return (FRESH[from] ?? ((one: string) => `async () => {\n ${one}\n}`))(throws)
170
- }
171
-
172
- export function nodeText(
173
- at: string,
174
- name: string,
175
- takes: string,
176
- from: string,
177
- requires: readonly string[],
178
- body?: string,
179
- ): string {
180
- const held = SHIPPED.has(from)
181
- ? `import { ${from} } from ${quoted(runtime(at, '../nodes.ts'))}`
182
- : `import { node as ${from} } from ${quoted(beside(at, `nodes/${from}.ts`))}`
183
-
184
- const only = requires.length === 0 ? 'never' : requires.map((one) => quoted(one)).join(' | ')
185
-
186
- return [
187
- held,
188
- '',
189
- `import type { In } from ${quoted(beside(at, takes))}`,
190
- '',
191
- `${OPENS}${from}<In, ${only}>(${body ?? scaffold(from, name)})`,
192
- '',
193
- ].join('\n')
194
- }
195
-
196
- const nodeFile = (at: string, row: Row, takes: string, from: string, requires: readonly string[], body?: string): string =>
197
- nodeText(at, row.id ?? '', takes, from, requires, body)
198
-
199
- const ownType = (at: string, id: string): string =>
200
- [
201
- `import type { Held } from ${quoted(runtime(at, '../nodes.ts'))}`,
202
- 'import type { Opened } from ' + quoted(runtime(at, '../operations.ts')),
203
- '',
204
- 'export const node = <I, R extends string = never>(block: (opened: Opened<R>, held: Held<I>) => Promise<unknown>) => ({',
205
- ` type: ${quoted(id)} as const,`,
206
- '})',
207
- '',
208
- ].join('\n')
209
-
210
- interface Wanted {
211
- readonly at: string
212
- readonly derived: boolean
213
- readonly wraps?: string
214
- readonly text?: string
215
- readonly because?: string
216
- readonly type?: string
217
- }
218
-
219
- function planned(bodies: ReadonlyMap<string, string>): readonly Wanted[] {
220
- const pass = descend()
221
- const known = types(pass)
222
- const mine = operations(pass, known)
223
- const found: Wanted[] = []
224
-
225
- for (const [id, one] of mine) {
226
- void id
227
- for (const row of one.takes) if (row.at !== undefined) found.push({ at: row.at, derived: false, text: takesFile(row.at) })
228
- for (const row of one.nodes) {
229
- const from = builtFrom(row, known)
230
- const takes = one.takes[0]?.at
231
- if (row.at === undefined || from === undefined || takes === undefined) continue
232
- const path = resolve(ROOT, row.at)
233
- const held = existsSync(path) ? readFileSync(path, 'utf8') : undefined
234
- const block = bodies.get(row.at) ?? (held === undefined ? undefined : blockIn(held))
235
- if (held !== undefined && block === undefined) {
236
- found.push({
237
- at: row.at,
238
- derived: true,
239
- because:
240
- `\`${String(row.id)}\` states no \`${OPENS}\` this can read, so the block it holds cannot be told from the ` +
241
- 'wrapper its rows own. Delete the file and run `thread seed` to scaffold it again, then write the block back into it',
242
- })
243
- continue
244
- }
245
- found.push({ at: row.at, derived: true, wraps: String(row.id), text: nodeFile(row.at, row, takes, from, one.requires, block) })
246
- }
247
- }
248
-
249
- for (const [at, threadings] of pass.threaded) {
250
- for (const one of threadings) {
251
- if (one.list !== 'seeds' || !isType(pass, one.by) || SHIPPED.has(one.by)) continue
252
- found.push({ at, derived: false, text: ownType(at, one.by), type: one.by })
253
- }
254
- }
255
-
256
- return found
257
- }
258
-
259
- function checked(plan: readonly Wanted[]): void {
260
- if (plan.length === 0) {
261
- stdout.write('seed: this app declares no operation and no node type of its own, so there is no file to write or check.\n')
262
- exit(0)
263
- }
264
-
265
- const findings: Finding[] = plan.flatMap((one): Finding[] => {
266
- const path = resolve(ROOT, one.at)
267
-
268
- if (one.because !== undefined) {
269
- return [
270
- {
271
- file: one.at,
272
- detail: one.because,
273
- instead: 'declare it properly — this file is written from those rows and cannot be written without them',
274
- },
275
- ]
276
- }
277
-
278
- if (!existsSync(path)) {
279
- return [
280
- {
281
- file: one.at,
282
- detail: 'a row declares this file and nothing is there',
283
- instead: 'run `thread seed` and write what it leaves — a row that seeded nothing is half a declaration',
284
- },
285
- ]
286
- }
287
-
288
- if (one.type !== undefined) {
289
- const said = NAMED.exec(readFileSync(path, 'utf8'))?.[1]
290
- if (said === one.type) return []
291
- return [
292
- {
293
- file: one.at,
294
- detail:
295
- said === undefined
296
- ? `\`${one.type}\` seeds this type and the file states no name of its own`
297
- : `\`${one.type}\` seeds this type and the file calls itself \`${said}\``,
298
- instead:
299
- "state the row's own id as the type's name. A node declares which type it is built from, and the compiler holds the two together by that name",
300
- },
301
- ]
302
- }
303
-
304
- if (!one.derived) return []
305
-
306
- if (readFileSync(path, 'utf8') === one.text) return []
307
-
308
- if (one.wraps !== undefined) {
309
- return [
310
- {
311
- file: one.at,
312
- detail: `the line that opens \`${one.wraps}\` is written from its rows, and this is no longer the bytes they write`,
313
- instead:
314
- 'run `thread seed`. Which type builds this node and what its operation lets it touch are the rows to say, not this line — the block inside the call is yours and is put back exactly as you left it',
315
- },
316
- ]
317
- }
318
-
319
- return [
320
- {
321
- file: one.at,
322
- detail: 'this operation derives its body, and this is no longer the bytes that derive it',
323
- instead:
324
- 'run `thread seed`, or put back what it writes. A derived body has no author: to change what an operation does, change the rows that declare its nodes, and to change what a node does, write it in the file that node seeds',
325
- },
326
- ]
327
- })
328
-
329
- runChecker('seed', findings, `${String(plan.length)} file(s) declared`)
330
- exit(findings.length === 0 ? 0 : 1)
331
- }
332
-
333
- export function seed(bodies: ReadonlyMap<string, string> = new Map(), say: boolean = true): number {
334
- const plan = planned(bodies)
335
- let written = 0
336
- for (const one of plan) {
337
- const path = resolve(ROOT, one.at)
338
- const held = existsSync(path) ? readFileSync(path, 'utf8') : undefined
339
-
340
- if (held !== undefined && !one.derived && !bodies.has(one.at)) continue
341
-
342
- if (one.text === undefined) {
343
- if (say) stdout.write(`seed: ${one.at} was not written — ${one.because ?? 'the rows behind it are incomplete'}\n`)
344
- continue
345
- }
346
- if (held === one.text) continue
347
-
348
- writeFileSync(path, one.text, 'utf8')
349
- written += 1
350
- if (say) stdout.write(`seed: wrote ${one.at}\n`)
351
- }
352
- if (say) stdout.write(`seed: ${String(written)} of ${String(plan.length)} file(s) written, ${String(plan.length - written)} already as declared.\n`)
353
- return written
354
- }
355
-
356
- if (invoked(import.meta.url)) {
357
- if (argv.includes('--check')) checked(planned(new Map()))
358
- else seed()
359
- }