@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.
- package/CHANGELOG.md +227 -0
- package/dist/core/attach.js +21 -0
- package/dist/core/descend.js +6 -3
- package/dist/core/generators/commit.js +55 -3
- package/dist/core/generators/dev.d.ts +1 -0
- package/dist/core/generators/dev.js +69 -0
- package/dist/derivation/generators/build.d.ts +3 -1
- package/dist/derivation/generators/build.js +104 -22
- package/dist/derivation/generators/release.js +74 -25
- package/dist/derivation/release.js +3 -3
- package/dist/integrations/cli.d.ts +0 -1
- package/dist/integrations/cli.js +69 -11
- package/dist/integrations/declared.d.ts +2 -0
- package/dist/integrations/declared.js +6 -1
- package/dist/integrations/pglite.js +13 -3
- package/dist/integrations/target.d.ts +3 -0
- package/dist/integrity/attach.js +1 -5
- package/dist/integrity/slopnators/sl_stale_path.js +10 -0
- package/dist/operations/access.js +3 -2
- package/dist/operations/capabilities.d.ts +2 -0
- package/dist/operations/capabilities.js +2 -0
- package/dist/operations/generators/capability.js +237 -24
- package/dist/operations/generators/compile.js +150 -50
- package/dist/operations/generators/seed.d.ts +3 -0
- package/dist/operations/generators/seed.js +25 -3
- package/dist/operations/nodes.d.ts +2 -2
- package/package.json +1 -1
- package/dist/core/actor.ts +0 -21
- package/dist/core/attach.ts +0 -613
- package/dist/core/chain.ts +0 -145
- package/dist/core/condition.ts +0 -227
- package/dist/core/descend.ts +0 -1185
- package/dist/core/errors/catalogue.ts +0 -314
- package/dist/core/errors/classes.ts +0 -51
- package/dist/core/errors/error.ts +0 -152
- package/dist/core/errors/namespaces.ts +0 -19
- package/dist/core/finding.ts +0 -37
- package/dist/core/generators/commit.ts +0 -157
- package/dist/core/init.ts +0 -222
- package/dist/core/invoked.ts +0 -52
- package/dist/core/round.ts +0 -74
- package/dist/core/rows.ts +0 -506
- package/dist/core/scope.ts +0 -20
- package/dist/core/thread.ts +0 -29
- package/dist/core/tree.ts +0 -90
- package/dist/core/types.ts +0 -351
- package/dist/derivation/attach.ts +0 -57
- package/dist/derivation/generators/build.ts +0 -371
- package/dist/derivation/generators/check.ts +0 -118
- package/dist/derivation/generators/determinism.ts +0 -145
- package/dist/derivation/generators/release.ts +0 -236
- package/dist/derivation/release.ts +0 -43
- package/dist/integrations/attach.ts +0 -187
- package/dist/integrations/cli.ts +0 -606
- package/dist/integrations/declared.ts +0 -34
- package/dist/integrations/disk.ts +0 -466
- package/dist/integrations/generators/read.ts +0 -127
- package/dist/integrations/generators/vault.ts +0 -210
- package/dist/integrations/https.ts +0 -216
- package/dist/integrations/pglite.ts +0 -206
- package/dist/integrations/secret.ts +0 -54
- package/dist/integrations/target.ts +0 -672
- package/dist/integrations/vault.ts +0 -148
- package/dist/integrity/attach.ts +0 -150
- package/dist/integrity/check-threads.ts +0 -166
- package/dist/integrity/constraint.ts +0 -1246
- package/dist/integrity/generators/compiler.ts +0 -138
- package/dist/integrity/generators/gate.ts +0 -227
- package/dist/integrity/generators/issues.ts +0 -160
- package/dist/integrity/generators/publish.ts +0 -233
- package/dist/integrity/generators/trial.ts +0 -359
- package/dist/integrity/lint.ts +0 -214
- package/dist/integrity/lsp/bin.ts +0 -44
- package/dist/integrity/lsp/diagnostics.ts +0 -92
- package/dist/integrity/lsp/findings.ts +0 -63
- package/dist/integrity/lsp/lint.ts +0 -124
- package/dist/integrity/lsp/protocol.ts +0 -78
- package/dist/integrity/lsp/server.ts +0 -224
- package/dist/integrity/slopnator.ts +0 -324
- package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
- package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
- package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
- package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
- package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
- package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
- package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
- package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
- package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
- package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
- package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
- package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
- package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
- package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
- package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
- package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
- package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
- package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
- package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
- package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
- package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
- package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
- package/dist/integrity/sweep.ts +0 -395
- package/dist/knowledge/attach.ts +0 -327
- package/dist/knowledge/generators/docsgen.ts +0 -186
- package/dist/knowledge/generators/glossary.ts +0 -73
- package/dist/knowledge/glossary.ts +0 -6
- package/dist/operations/access.ts +0 -131
- package/dist/operations/attach.ts +0 -408
- package/dist/operations/capabilities.ts +0 -88
- package/dist/operations/dispatch.ts +0 -294
- package/dist/operations/error-results.ts +0 -31
- package/dist/operations/generators/capability.ts +0 -1054
- package/dist/operations/generators/compile.ts +0 -701
- package/dist/operations/generators/permits.ts +0 -229
- package/dist/operations/generators/seed.ts +0 -359
- package/dist/operations/generators/trial.ts +0 -666
- package/dist/operations/nodes.ts +0 -53
- package/dist/operations/operations.ts +0 -91
- 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
|
-
}
|