@prestack/thread 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.js +6 -3
  4. package/dist/core/generators/commit.js +55 -3
  5. package/dist/core/generators/dev.d.ts +1 -0
  6. package/dist/core/generators/dev.js +69 -0
  7. package/dist/derivation/generators/build.d.ts +3 -1
  8. package/dist/derivation/generators/build.js +104 -22
  9. package/dist/derivation/generators/release.js +74 -25
  10. package/dist/derivation/release.js +3 -3
  11. package/dist/integrations/cli.d.ts +0 -1
  12. package/dist/integrations/cli.js +69 -11
  13. package/dist/integrations/declared.d.ts +2 -0
  14. package/dist/integrations/declared.js +6 -1
  15. package/dist/integrations/pglite.js +13 -3
  16. package/dist/integrations/target.d.ts +3 -0
  17. package/dist/integrity/attach.js +1 -5
  18. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  19. package/dist/operations/access.js +3 -2
  20. package/dist/operations/capabilities.d.ts +2 -0
  21. package/dist/operations/capabilities.js +2 -0
  22. package/dist/operations/generators/capability.js +237 -24
  23. package/dist/operations/generators/compile.js +150 -50
  24. package/dist/operations/generators/seed.d.ts +3 -0
  25. package/dist/operations/generators/seed.js +25 -3
  26. package/dist/operations/nodes.d.ts +2 -2
  27. package/package.json +1 -1
  28. package/dist/core/actor.ts +0 -21
  29. package/dist/core/attach.ts +0 -613
  30. package/dist/core/chain.ts +0 -145
  31. package/dist/core/condition.ts +0 -227
  32. package/dist/core/descend.ts +0 -1185
  33. package/dist/core/errors/catalogue.ts +0 -314
  34. package/dist/core/errors/classes.ts +0 -51
  35. package/dist/core/errors/error.ts +0 -152
  36. package/dist/core/errors/namespaces.ts +0 -19
  37. package/dist/core/finding.ts +0 -37
  38. package/dist/core/generators/commit.ts +0 -157
  39. package/dist/core/init.ts +0 -222
  40. package/dist/core/invoked.ts +0 -52
  41. package/dist/core/round.ts +0 -74
  42. package/dist/core/rows.ts +0 -506
  43. package/dist/core/scope.ts +0 -20
  44. package/dist/core/thread.ts +0 -29
  45. package/dist/core/tree.ts +0 -90
  46. package/dist/core/types.ts +0 -351
  47. package/dist/derivation/attach.ts +0 -57
  48. package/dist/derivation/generators/build.ts +0 -371
  49. package/dist/derivation/generators/check.ts +0 -118
  50. package/dist/derivation/generators/determinism.ts +0 -145
  51. package/dist/derivation/generators/release.ts +0 -236
  52. package/dist/derivation/release.ts +0 -43
  53. package/dist/integrations/attach.ts +0 -187
  54. package/dist/integrations/cli.ts +0 -606
  55. package/dist/integrations/declared.ts +0 -34
  56. package/dist/integrations/disk.ts +0 -466
  57. package/dist/integrations/generators/read.ts +0 -127
  58. package/dist/integrations/generators/vault.ts +0 -210
  59. package/dist/integrations/https.ts +0 -216
  60. package/dist/integrations/pglite.ts +0 -206
  61. package/dist/integrations/secret.ts +0 -54
  62. package/dist/integrations/target.ts +0 -672
  63. package/dist/integrations/vault.ts +0 -148
  64. package/dist/integrity/attach.ts +0 -150
  65. package/dist/integrity/check-threads.ts +0 -166
  66. package/dist/integrity/constraint.ts +0 -1246
  67. package/dist/integrity/generators/compiler.ts +0 -138
  68. package/dist/integrity/generators/gate.ts +0 -227
  69. package/dist/integrity/generators/issues.ts +0 -160
  70. package/dist/integrity/generators/publish.ts +0 -233
  71. package/dist/integrity/generators/trial.ts +0 -359
  72. package/dist/integrity/lint.ts +0 -214
  73. package/dist/integrity/lsp/bin.ts +0 -44
  74. package/dist/integrity/lsp/diagnostics.ts +0 -92
  75. package/dist/integrity/lsp/findings.ts +0 -63
  76. package/dist/integrity/lsp/lint.ts +0 -124
  77. package/dist/integrity/lsp/protocol.ts +0 -78
  78. package/dist/integrity/lsp/server.ts +0 -224
  79. package/dist/integrity/slopnator.ts +0 -324
  80. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  81. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  82. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  83. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  84. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  85. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  86. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  87. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  88. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  89. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  90. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  91. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  92. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  93. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  94. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  95. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  96. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  97. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  98. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  99. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  100. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  101. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  102. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  103. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  104. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  105. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  106. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  107. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  108. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  109. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  110. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  111. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  112. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  113. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  114. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  115. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  116. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  117. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  118. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  119. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  120. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  121. package/dist/integrity/sweep.ts +0 -395
  122. package/dist/knowledge/attach.ts +0 -327
  123. package/dist/knowledge/generators/docsgen.ts +0 -186
  124. package/dist/knowledge/generators/glossary.ts +0 -73
  125. package/dist/knowledge/glossary.ts +0 -6
  126. package/dist/operations/access.ts +0 -131
  127. package/dist/operations/attach.ts +0 -408
  128. package/dist/operations/capabilities.ts +0 -88
  129. package/dist/operations/dispatch.ts +0 -294
  130. package/dist/operations/error-results.ts +0 -31
  131. package/dist/operations/generators/capability.ts +0 -1054
  132. package/dist/operations/generators/compile.ts +0 -701
  133. package/dist/operations/generators/permits.ts +0 -229
  134. package/dist/operations/generators/seed.ts +0 -359
  135. package/dist/operations/generators/trial.ts +0 -666
  136. package/dist/operations/nodes.ts +0 -53
  137. package/dist/operations/operations.ts +0 -91
  138. package/dist/operations/result.ts +0 -28
@@ -1,229 +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, invoked } 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
- async function taken<T>(file: string, name: string, what: string): Promise<readonly T[]> {
24
- const at = inTree(findTree(), file)
25
- if (!existsSync(at)) stop(`${at} is where this app states ${what}, and it is not there`)
26
- let held: Record<string, unknown>
27
- try {
28
- held = valuesIn(readFileSync(at)) as Record<string, unknown>
29
- } catch (thrown) {
30
- stop(
31
- `${at} could not be read: ${thrown instanceof Error ? thrown.message : String(thrown)}. A tree whose access ` +
32
- 'cannot be read reports exactly like a tree that granted nothing, and a gate written from that would refuse ' +
33
- 'every caller rather than say so',
34
- )
35
- }
36
- const list = held[name]
37
- if (!Array.isArray(list)) stop(`${at} exports no \`${name}\` list, so nothing there states ${what}`)
38
- return list as readonly T[]
39
- }
40
-
41
- const said = (held: unknown): string => JSON.stringify(held)
42
-
43
- const used = new Set<readonly string[]>()
44
-
45
- function reads(one: Operand, grant: Grant): string {
46
- if ('value' in one) return said(one.value)
47
- if ('now' in one) return 'Date.now()'
48
- if ('ago' in one) {
49
- const size = Number(one.ago.slice(0, -1))
50
- const unit = one.ago.slice(-1)
51
- const seconds = size * (unit === 's' ? 1 : unit === 'm' ? 60 : unit === 'h' ? 3_600 : 86_400)
52
- return `(Date.now() - ${String(seconds * 1000)})`
53
- }
54
- if ('actor' in one) return `actor.${one.actor}`
55
- if ('field' in one) {
56
- if (grant.on === undefined) {
57
- stop(
58
- `\`${grant.id}\` reads the field "${one.field}" and names no entity in \`on\`, so there is nothing saying what ` +
59
- 'the row in front of the gate is. Name the entity the resource is, or state the rule without reading a field',
60
- )
61
- }
62
- if (one.field.includes('.')) {
63
- stop(
64
- `\`${grant.id}\` reads "${one.field}", which reaches through a link. The gate is handed one row and has ` +
65
- 'nothing to follow a link with — name a value of "' +
66
- grant.on +
67
- '" itself, or state the rule as an invariant where the rows are',
68
- )
69
- }
70
- used.add(READS)
71
- return `at(resource, ${said(one.field)})`
72
- }
73
- stop(`\`${grant.id}\` states an operand this gate cannot read`)
74
- }
75
-
76
- function predicate(condition: Condition, grant: Grant): string {
77
- if ('all' in condition) {
78
- return condition.all.length === 0 ? 'true' : `(${condition.all.map((one) => predicate(one, grant)).join(' && ')})`
79
- }
80
- if ('any' in condition) {
81
- return condition.any.length === 0 ? 'false' : `(${condition.any.map((one) => predicate(one, grant)).join(' || ')})`
82
- }
83
- if ('not' in condition) return `(!${predicate(condition.not, grant)})`
84
- used.add(COMPARES)
85
- return `held(${said(condition.op)}, ${reads(condition.is, grant)}, ${reads(condition.than, grant)})`
86
- }
87
-
88
- function wears(grant: Grant): readonly string[] {
89
- const held: string[] = []
90
- const roles = grant.to?.roles
91
- if (roles !== undefined && !roles.includes(PUBLIC)) held.push(`some(actor.roles, ${said(roles)})`)
92
- if (grant.to?.entitlements !== undefined) held.push(`some(actor.entitlements, ${said(grant.to.entitlements)})`)
93
- if (held.length > 0) used.add(WEARS)
94
- return held
95
- }
96
-
97
- const READS = ['const at = (row: Row | null, name: string): unknown => (row === null ? undefined : row[name])']
98
-
99
- const WEARS = [
100
- 'const some = (worn: readonly string[], named: readonly string[]): boolean => named.some((one) => worn.includes(one))',
101
- ]
102
-
103
- const COMPARES = [
104
- 'const held = (op: string, left: unknown, right: unknown): boolean => {',
105
- ' if (op === \'in\') return Array.isArray(right) && right.includes(left)',
106
- ' if (op === \'has\') return Array.isArray(left) && left.includes(right)',
107
- ' if (op === \'eq\') return left === right',
108
- ' if (op === \'neq\') return left !== right',
109
- ' if (typeof left !== typeof right) return false',
110
- ' if (typeof left !== \'number\' && typeof left !== \'string\') return false',
111
- ' const other = right as number | string',
112
- ' if (op === \'lt\') return left < other',
113
- ' if (op === \'lte\') return left <= other',
114
- ' if (op === \'gt\') return left > other',
115
- ' return left >= other',
116
- '}',
117
- ]
118
-
119
- export function permitting(grants: readonly Grant[]): string {
120
- used.clear()
121
- const arms = grants.flatMap((one) => {
122
- const parts = [...wears(one), ...(one.when === undefined ? [] : [predicate(one.when, one)])]
123
- return [
124
- ` if (${said(one.may)}.includes(action)) {`,
125
- ` if (${parts.length === 0 ? 'true' : parts.join(' && ')}) return { allow: true, because: ${said(one.describes)} }`,
126
- ` denied.push(${said(one.describes)})`,
127
- ' }',
128
- ]
129
- })
130
-
131
- const body =
132
- grants.length === 0
133
- ? [
134
- ' return {',
135
- ' allow: false,',
136
- " because: 'this app declares no grants, so nothing permits any operation. Declare one with `thread grant`',",
137
- ' }',
138
- ]
139
- : [
140
- ' const denied: string[] = []',
141
- ...arms,
142
- ' return { allow: false, because: denied[0] ?? `nothing granted permits ${action}` }',
143
- ]
144
-
145
- return [
146
- `import type { Actor } from '${runtime(PERMITS_AT, '../../core/actor.ts')}'`,
147
- "import type { Row } from '" + runtime(PERMITS_AT, '../../integrations/target.ts') + "'",
148
- '',
149
- ...[READS, WEARS, COMPARES].filter((one) => used.has(one)).flatMap((one) => [...one, '']),
150
-
151
- 'export function permits(actor: Actor, action: string, resource: Row | null): { allow: boolean; because?: string } {',
152
- ...(grants.length === 0 ? [' void actor', ' void resource'] : []),
153
- ...body,
154
- '}',
155
- '',
156
- ].join('\n')
157
- }
158
-
159
- const RUNS = 'thread permits'
160
-
161
- async function run(): Promise<void> {
162
- const pass = await descend()
163
-
164
- const stopped = [...pass.broken, ...pass.findings].filter(forbids)[0]
165
- if (stopped !== undefined) {
166
- stop(
167
- 'the descent is broken, so who may ask for what is only partly known, and a gate written from half of it ' +
168
- `would permit callers nobody granted — ${stopped.detail} (${stopped.file ?? ''})`,
169
- )
170
- }
171
-
172
- const grants = await taken<Grant>('access.ts', 'GRANTS', 'who may ask for what')
173
-
174
- const operations = [...woven(pass, types(pass))].map(([id]) => ({
175
- id,
176
- describes: pass.stated.get(id)?.describes ?? '',
177
- }))
178
-
179
- for (const found of [unanswerable(grants)]) {
180
- if (found.length > 0) stop(found.join('; '))
181
- }
182
-
183
- const twice = grantedTwice(grants)
184
- if (twice.length > 0) {
185
- stop(
186
- `${twice.map((one) => `\`${one}\``).join(', ')} ${twice.length === 1 ? 'is granted' : 'are granted'} more than once, ` +
187
- 'so two arms answer to one id and either may permit. One id, one policy — rename one of them, or delete the ' +
188
- 'arm that is not meant to stand',
189
- )
190
- }
191
-
192
- const open = opensToEverybody(grants)
193
- if (open.length > 0) {
194
- stop(
195
- `${open.map((one) => `\`${one.id}\``).join(', ')} names no roles or entitlements and states no condition that ` +
196
- 'reads the caller, so who may run this is left unsaid rather than decided. If it is meant to be open, say so: ' +
197
- `\`to: { roles: ['${PUBLIC}'] }\` is the role an unauthenticated caller already wears, and naming it puts ` +
198
- 'that decision in the sheet. Otherwise name the roles that may, or state a `when` saying whose row it is',
199
- )
200
- }
201
-
202
- const permitted = resolve(ROOT, PERMITS_AT)
203
-
204
- const content = permitting(grants)
205
-
206
- const count = `${String(grants.length)} grant(s) over ${String(operations.length)} operation(s)`
207
-
208
- if (CHECKING) {
209
- const findings: Finding[] = []
210
- const current = existsSync(permitted) ? readFileSync(permitted, 'utf8') : ''
211
- if (current !== content) {
212
- findings.push({
213
- file: PERMITS_AT,
214
- detail: 'is not what this app grants',
215
- 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`,
216
- })
217
- }
218
- runChecker('permits', findings, count)
219
- exit(findings.length === 0 ? 0 : 1)
220
- }
221
-
222
- writeFileSync(permitted, content)
223
- stdout.write(
224
- `permits: wrote ${PERMITS_AT} — ${count}. Dispatch imports this check itself, so every operation passes it; ` +
225
- '`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',
226
- )
227
- }
228
-
229
- if (invoked(import.meta.url)) await run()
@@ -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
- }