@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.
- package/CHANGELOG.md +193 -0
- package/dist/core/attach.js +21 -0
- package/dist/core/descend.d.ts +2 -0
- package/dist/core/descend.js +18 -3
- package/dist/core/generators/commit.js +55 -3
- package/dist/core/generators/dev.d.ts +1 -0
- package/dist/core/generators/dev.js +68 -0
- package/dist/core/rows.d.ts +5 -0
- package/dist/core/rows.js +45 -0
- package/dist/core/tree.d.ts +1 -0
- package/dist/core/tree.js +7 -1
- package/dist/derivation/generators/build.d.ts +3 -1
- package/dist/derivation/generators/build.js +103 -22
- package/dist/derivation/generators/release.js +74 -26
- package/dist/derivation/release.js +3 -3
- package/dist/integrations/cli.d.ts +3 -9
- package/dist/integrations/cli.js +96 -36
- package/dist/integrations/declared.d.ts +8 -0
- package/dist/integrations/declared.js +22 -0
- package/dist/integrations/pglite.js +13 -3
- package/dist/integrations/target.d.ts +4 -1
- package/dist/integrations/target.js +20 -6
- package/dist/integrity/attach.js +1 -5
- package/dist/integrity/generators/publish.js +2 -1
- package/dist/integrity/slopnators/sl_stale_path.js +10 -0
- package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
- 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 +234 -24
- package/dist/operations/generators/compile.d.ts +21 -0
- package/dist/operations/generators/compile.js +151 -51
- package/dist/operations/generators/permits.d.ts +2 -1
- package/dist/operations/generators/permits.js +52 -47
- 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 -1173
- 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 -459
- 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 -615
- 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 -656
- 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 -232
- 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 -224
- 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,666 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, type Answering, type Listed } from '../../integrations/disk.ts'
|
|
2
|
-
import { resolve } from 'node:path'
|
|
3
|
-
import { argv, stdout } from 'node:process'
|
|
4
|
-
import { pathToFileURL } from 'node:url'
|
|
5
|
-
|
|
6
|
-
import { ANONYMOUS, type Actor } from '../../core/actor.ts'
|
|
7
|
-
import { ROOT, descend } from '../../core/descend.ts'
|
|
8
|
-
import { exit, invoked } from '../../core/invoked.ts'
|
|
9
|
-
import { reachOf, standsHere, type Call, type Row, type Target } from '../../integrations/target.ts'
|
|
10
|
-
|
|
11
|
-
import { recordingHttp, type RecordedCall, type Scripted, type ScriptedAnswer } from '../../integrations/https.ts'
|
|
12
|
-
import { standing, type Kept, type Stood } from '../../integrations/pglite.ts'
|
|
13
|
-
import { TRIALS_AT, type Operation } from '../operations.ts'
|
|
14
|
-
import { PERMITS_AT } from '../access.ts'
|
|
15
|
-
import type { Result } from '../result.ts'
|
|
16
|
-
import type { Vault } from '../../integrations/secret.ts'
|
|
17
|
-
import { operations, types } from './seed.ts'
|
|
18
|
-
|
|
19
|
-
export { TRIALS_AT }
|
|
20
|
-
|
|
21
|
-
const DISPATCH_AT = 'operations/_dispatch.ts'
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const ACCESS_AT = '_threads/access.ts'
|
|
26
|
-
|
|
27
|
-
const WHEN = 1_767_225_600_000
|
|
28
|
-
|
|
29
|
-
export interface Scenario {
|
|
30
|
-
readonly id: string
|
|
31
|
-
|
|
32
|
-
readonly describes: string
|
|
33
|
-
|
|
34
|
-
readonly input: unknown
|
|
35
|
-
|
|
36
|
-
readonly actor?: Actor
|
|
37
|
-
|
|
38
|
-
readonly rows?: readonly { readonly entity: string; readonly row: Row }[]
|
|
39
|
-
|
|
40
|
-
readonly answers?: Readonly<Record<string, Scripted>>
|
|
41
|
-
|
|
42
|
-
readonly views?: Readonly<Record<string, readonly unknown[]>>
|
|
43
|
-
|
|
44
|
-
readonly files?: Readonly<Record<string, string>>
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface Trial {
|
|
48
|
-
readonly operation: string
|
|
49
|
-
readonly scenario: string
|
|
50
|
-
|
|
51
|
-
readonly refused: boolean
|
|
52
|
-
|
|
53
|
-
readonly measured: boolean
|
|
54
|
-
|
|
55
|
-
readonly result: unknown
|
|
56
|
-
|
|
57
|
-
readonly kept: unknown
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
readonly called: unknown
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface Outcome {
|
|
64
|
-
readonly id: string
|
|
65
|
-
readonly operation: string
|
|
66
|
-
readonly passed: boolean
|
|
67
|
-
readonly refused: boolean
|
|
68
|
-
readonly measured: boolean
|
|
69
|
-
readonly failures: readonly string[]
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export interface Run {
|
|
73
|
-
readonly outcomes: readonly Outcome[]
|
|
74
|
-
readonly passed: number
|
|
75
|
-
readonly failed: number
|
|
76
|
-
readonly measured: number
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
|
|
80
|
-
|
|
81
|
-
function stable(value: unknown): unknown {
|
|
82
|
-
if (Array.isArray(value)) return value.map(stable)
|
|
83
|
-
if (value instanceof Map) return stable(Object.fromEntries(value))
|
|
84
|
-
if (value instanceof Set) return [...value].map(stable)
|
|
85
|
-
if (typeof value === 'object' && value !== null) {
|
|
86
|
-
const found: Record<string, unknown> = {}
|
|
87
|
-
for (const key of Object.keys(value as Record<string, unknown>).sort(compare)) {
|
|
88
|
-
found[key] = stable((value as Record<string, unknown>)[key])
|
|
89
|
-
}
|
|
90
|
-
return found
|
|
91
|
-
}
|
|
92
|
-
if (typeof value === 'bigint') return String(value)
|
|
93
|
-
if (typeof value === 'function') return '[function]'
|
|
94
|
-
if (value === undefined) return null
|
|
95
|
-
return value
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const printed = (value: unknown): string => `${JSON.stringify(stable(value), null, 2)}\n`
|
|
99
|
-
|
|
100
|
-
const asking = (scenario: Scenario): Actor => ({
|
|
101
|
-
...(scenario.actor ?? ANONYMOUS),
|
|
102
|
-
run: 'trial',
|
|
103
|
-
replay: `trial_${scenario.id}`,
|
|
104
|
-
moment: new Date(WHEN).toISOString(),
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
function programmed(word: string, names: readonly string[]): Record<string, unknown> {
|
|
108
|
-
const made: Record<string, unknown> = {}
|
|
109
|
-
for (const order of names) {
|
|
110
|
-
made[order] = (): never => {
|
|
111
|
-
throw new Error(
|
|
112
|
-
word + ' was asked to run ' + order + ', and this scenario says nothing about what that did. A trial stands a ' +
|
|
113
|
-
'disk up out of the files its scenario states, and there is nowhere yet to state what a program or a package answered, so ' +
|
|
114
|
-
'a scenario cannot give either an order. This says that rather than inventing a result for one.',
|
|
115
|
-
)
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
return made
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function packaged(
|
|
122
|
-
word: string,
|
|
123
|
-
names: readonly string[],
|
|
124
|
-
script: Readonly<Record<string, Scripted>>,
|
|
125
|
-
into: RecordedCall[],
|
|
126
|
-
): Record<string, unknown> {
|
|
127
|
-
const spent = new Map<string, number>()
|
|
128
|
-
const made: Record<string, unknown> = {}
|
|
129
|
-
for (const name of names) {
|
|
130
|
-
made[name] = async (given?: unknown): Promise<unknown> => {
|
|
131
|
-
const key = word + ' ' + name
|
|
132
|
-
into.push({ target: word, method: 'CALL', path: name, headers: {}, body: JSON.stringify(stable(given ?? null)) })
|
|
133
|
-
const held = script[key]
|
|
134
|
-
if (held === undefined) {
|
|
135
|
-
throw new Error(
|
|
136
|
-
'this scenario scripts no answer for "' + key + '", so nothing here knows what ' + word + ' would hand back. Add ' +
|
|
137
|
-
'that key to the scenario\'s answers, with what the call returns written as JSON in its body.',
|
|
138
|
-
)
|
|
139
|
-
}
|
|
140
|
-
const each = Array.isArray(held) ? (held as readonly ScriptedAnswer[]) : undefined
|
|
141
|
-
const at = spent.get(key) ?? 0
|
|
142
|
-
spent.set(key, at + 1)
|
|
143
|
-
const answer = each === undefined ? (held as ScriptedAnswer) : each[at]
|
|
144
|
-
if (answer === undefined) {
|
|
145
|
-
throw new Error(
|
|
146
|
-
'this scenario scripts ' + String(each?.length ?? 0) + ' answer(s) for "' + key + '" and it has now been called ' +
|
|
147
|
-
String(at + 1) + ' time(s). A node that calls the same thing again needs one answer per call, in the order they come back.',
|
|
148
|
-
)
|
|
149
|
-
}
|
|
150
|
-
if (answer.status !== 200) throw new Error(answer.body ?? word + ' ' + name + ' failed with ' + String(answer.status))
|
|
151
|
-
return answer.body === undefined ? undefined : (JSON.parse(answer.body) as unknown)
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
return made
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function disked(files: Readonly<Record<string, string>>): Answering {
|
|
158
|
-
const held = new Map<string, string>(Object.entries(files))
|
|
159
|
-
const under = (at: string): boolean => [...held.keys()].some((one) => one.startsWith(at + '/'))
|
|
160
|
-
const gone = (at: string): Error =>
|
|
161
|
-
new Error(at + ' is not in this scenario, and a trial answers a file order against the files its scenario states')
|
|
162
|
-
return {
|
|
163
|
-
kind: (at) => (held.has(at) ? 'file' : under(at) ? 'folder' : 'none'),
|
|
164
|
-
read: (at) => {
|
|
165
|
-
const said = held.get(at)
|
|
166
|
-
if (said === undefined) throw gone(at)
|
|
167
|
-
return said
|
|
168
|
-
},
|
|
169
|
-
write: (at, said) => {
|
|
170
|
-
held.set(at, said)
|
|
171
|
-
},
|
|
172
|
-
list: (at) => {
|
|
173
|
-
const inside = new Map<string, Listed>()
|
|
174
|
-
for (const one of held.keys()) {
|
|
175
|
-
if (!one.startsWith(at + '/')) continue
|
|
176
|
-
const rest = one.slice(at.length + 1)
|
|
177
|
-
const cut = rest.indexOf('/')
|
|
178
|
-
const name = cut === -1 ? rest : rest.slice(0, cut)
|
|
179
|
-
inside.set(name, { name, kind: cut === -1 ? 'file' : 'folder' })
|
|
180
|
-
}
|
|
181
|
-
return [...inside.values()]
|
|
182
|
-
},
|
|
183
|
-
stat: (at) => {
|
|
184
|
-
const said = held.get(at)
|
|
185
|
-
if (said === undefined) throw gone(at)
|
|
186
|
-
return { size: said.length, modified: 0 }
|
|
187
|
-
},
|
|
188
|
-
remove: (at) => {
|
|
189
|
-
held.delete(at)
|
|
190
|
-
},
|
|
191
|
-
real: (at) => at,
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function vaulting(stood: Readonly<Record<string, string>>): Vault {
|
|
196
|
-
return {
|
|
197
|
-
async resolve(refs) {
|
|
198
|
-
const found: Record<string, string> = {}
|
|
199
|
-
for (const ref of refs) {
|
|
200
|
-
const value = stood[ref]
|
|
201
|
-
if (value === undefined) {
|
|
202
|
-
throw new Error(
|
|
203
|
-
ref + ' is not a secret this app declares, so a trial has nothing to stand in for it. Declare it with ' +
|
|
204
|
-
'`thread create secret`, or stop requiring the target that asks for it.',
|
|
205
|
-
)
|
|
206
|
-
}
|
|
207
|
-
found[ref] = value
|
|
208
|
-
}
|
|
209
|
-
return found
|
|
210
|
-
},
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async function standingStore(row: Target): Promise<Stood | string> {
|
|
215
|
-
const said = row.derives?.find((each) => each.id === row.id + '_store')?.at
|
|
216
|
-
if (said === undefined) {
|
|
217
|
-
return row.id + ' keeps rows and its row derives no store, so a trial has nothing to stand up. Run `thread schema`'
|
|
218
|
-
}
|
|
219
|
-
const at = resolve(ROOT, said)
|
|
220
|
-
if (!existsSync(at)) return said + ' is not there, so ' + row.id + ' cannot be stood up. Run `thread schema`'
|
|
221
|
-
const made = (await import(pathToFileURL(at).href)) as { readonly kept?: Kept }
|
|
222
|
-
const held = made.kept
|
|
223
|
-
if (held === undefined) {
|
|
224
|
-
return said + ' says nothing about what ' + row.id + ' keeps or with what schema. Run `thread schema`'
|
|
225
|
-
}
|
|
226
|
-
return standing(held)
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
async function openedFor(
|
|
230
|
-
one: Operation<unknown>,
|
|
231
|
-
scenario: Scenario,
|
|
232
|
-
stood: Readonly<Record<string, string>>,
|
|
233
|
-
targets: readonly Target[] = [],
|
|
234
|
-
calls: readonly Call[] = [],
|
|
235
|
-
): Promise<{
|
|
236
|
-
readonly opened: Readonly<Record<string, unknown>>
|
|
237
|
-
readonly opening: Vault
|
|
238
|
-
readonly stores: readonly Stood[]
|
|
239
|
-
readonly calls: () => unknown
|
|
240
|
-
} | string> {
|
|
241
|
-
const recorder = recordingHttp(scenario.answers ?? {})
|
|
242
|
-
const packages: RecordedCall[] = []
|
|
243
|
-
const opening = vaulting(stood)
|
|
244
|
-
const opened: Record<string, unknown> = {}
|
|
245
|
-
const stores: Stood[] = []
|
|
246
|
-
|
|
247
|
-
const answering = (row: Target): readonly string[] =>
|
|
248
|
-
calls.filter((each) => each.of === row.id && row.takes.includes(each.id)).map((each) => each.id.replace(/^cl_/, ''))
|
|
249
|
-
for (const word of one.requires) {
|
|
250
|
-
const row = targets.find((each) => each.id === word)
|
|
251
|
-
if (row === undefined || !standsHere(row, targets)) continue
|
|
252
|
-
const reach = reachOf(row.id)
|
|
253
|
-
if (reach === 'disk') opened[word] = disked(scenario.files ?? {})
|
|
254
|
-
else if (reach === 'cli') opened[word] = programmed(word, answering(row))
|
|
255
|
-
else if (reach === 'https') opened[word] = recorder
|
|
256
|
-
else if (reach === 'sdk') opened[word] = packaged(word, answering(row), scenario.answers ?? {}, packages)
|
|
257
|
-
else {
|
|
258
|
-
const held = await standingStore(row)
|
|
259
|
-
if (typeof held === 'string') return held
|
|
260
|
-
opened[word] = held.store
|
|
261
|
-
stores.push(held)
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return { opened, opening, stores, calls: () => [...recorder['calls'], ...packages] }
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
const BEFORE = new Set(['denied', 'invalid'])
|
|
271
|
-
|
|
272
|
-
function entered(result: unknown): boolean {
|
|
273
|
-
if (typeof result !== 'object' || result === null) return false
|
|
274
|
-
const held = result as { readonly ok?: unknown; readonly code?: unknown }
|
|
275
|
-
if (held.ok === true) return true
|
|
276
|
-
return held.ok === false && !BEFORE.has(String(held.code))
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function denied(result: unknown): boolean {
|
|
280
|
-
if (typeof result !== 'object' || result === null) return false
|
|
281
|
-
const held = result as { readonly ok?: unknown; readonly code?: unknown }
|
|
282
|
-
return held.ok === false && BEFORE.has(String(held.code))
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
export async function runScenario(
|
|
286
|
-
one: Operation<unknown>,
|
|
287
|
-
scenario: Scenario,
|
|
288
|
-
dispatch: (
|
|
289
|
-
operation: Operation<unknown>,
|
|
290
|
-
input: unknown,
|
|
291
|
-
actor: Actor,
|
|
292
|
-
opened: Readonly<Record<string, unknown>>,
|
|
293
|
-
opening: Vault,
|
|
294
|
-
) => Promise<Result<unknown>>,
|
|
295
|
-
stood: Readonly<Record<string, string>> = {},
|
|
296
|
-
targets: readonly Target[] = [],
|
|
297
|
-
calls: readonly Call[] = [],
|
|
298
|
-
): Promise<Trial | string> {
|
|
299
|
-
|
|
300
|
-
const made = await openedFor(one, scenario, stood, targets, calls)
|
|
301
|
-
if (typeof made === 'string') return made
|
|
302
|
-
|
|
303
|
-
const actor = asking(scenario)
|
|
304
|
-
|
|
305
|
-
for (const each of scenario.rows ?? []) {
|
|
306
|
-
const held = made.stores.find((store) => store.keeps.includes(each.entity))
|
|
307
|
-
if (held === undefined) {
|
|
308
|
-
return (
|
|
309
|
-
'`' + one.id + '` is tried with a row of "' + each.entity + '" and no target it requires keeps that entity, ' +
|
|
310
|
-
'so there is nowhere to put it. Require the target that keeps it, or take that row off the scenario'
|
|
311
|
-
)
|
|
312
|
-
}
|
|
313
|
-
await held.seed(actor, [each])
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
let result: unknown
|
|
317
|
-
try {
|
|
318
|
-
result = await dispatch(one, scenario['input'], actor, made.opened, made.opening)
|
|
319
|
-
} catch (thrown) {
|
|
320
|
-
result = { threw: thrown instanceof Error ? thrown.message : String(thrown) }
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const kept: Record<string, unknown> = {}
|
|
324
|
-
for (const held of made.stores) Object.assign(kept, await held.taken(actor))
|
|
325
|
-
|
|
326
|
-
return {
|
|
327
|
-
operation: one.id,
|
|
328
|
-
scenario: scenario.id,
|
|
329
|
-
refused: denied(result),
|
|
330
|
-
measured: entered(result),
|
|
331
|
-
result,
|
|
332
|
-
kept,
|
|
333
|
-
|
|
334
|
-
called: made.calls(),
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
export async function scenariosFor(
|
|
339
|
-
one: Operation<unknown>,
|
|
340
|
-
takes: string,
|
|
341
|
-
granted: readonly string[] = [],
|
|
342
|
-
): Promise<readonly Scenario[] | string> {
|
|
343
|
-
const from = resolve(ROOT, takes)
|
|
344
|
-
if (existsSync(from)) {
|
|
345
|
-
const made = (await import(pathToFileURL(from).href)) as { readonly SCENARIOS?: readonly Scenario[] }
|
|
346
|
-
const found = made.SCENARIOS
|
|
347
|
-
if (found !== undefined) {
|
|
348
|
-
if (found.length === 0) {
|
|
349
|
-
return `${takes} states an empty \`SCENARIOS\`, so nothing would be measured — remove it, or state one`
|
|
350
|
-
}
|
|
351
|
-
const named = new Set(found.map((each) => each.id))
|
|
352
|
-
if (named.size !== found.length) {
|
|
353
|
-
return `${takes} states two scenarios with the same id, and a trial is named after the one it ran`
|
|
354
|
-
}
|
|
355
|
-
return [...found].sort((a, b) => compare(a.id, b.id))
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
if (granted.length > 0) {
|
|
360
|
-
return (
|
|
361
|
-
`\`${one.id}\` is granted to ${granted.join(', ')}, and no scenario says who runs it. A caller is not ` +
|
|
362
|
-
`something to derive — a trial run as somebody the grant must allow proves nothing about access. State one ` +
|
|
363
|
-
`with \`thread run trial ${one.id}\`, which asks for the roles it arrives wearing, and export \`SCENARIOS\` ` +
|
|
364
|
-
`from ${takes}`
|
|
365
|
-
)
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const empty = one.input.safeParse({})
|
|
369
|
-
if (empty.success) {
|
|
370
|
-
return [{ id: 'empty', describes: `runs ${one.id} with no input, as whoever arrives having said nothing`, input: {} }]
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return (
|
|
374
|
-
`\`${one.id}\` takes an input that no empty object satisfies, and states no scenario to try it ` +
|
|
375
|
-
`with. Export \`SCENARIOS\` from ${takes} — until then this operation is not proven`
|
|
376
|
-
)
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export function compared(was: readonly Trial[], now: readonly Trial[]): Run {
|
|
380
|
-
const before = new Map(was.map((one) => [`${one.operation}/${one.scenario}`, one]))
|
|
381
|
-
const outcomes: Outcome[] = []
|
|
382
|
-
|
|
383
|
-
for (const one of now) {
|
|
384
|
-
const id = `${one.operation}/${one.scenario}`
|
|
385
|
-
const failures: string[] = []
|
|
386
|
-
const older = before.get(id)
|
|
387
|
-
|
|
388
|
-
if (!one.refused && !one.measured) {
|
|
389
|
-
failures.push(
|
|
390
|
-
'the run never entered the operation and nothing denied it, so nothing about the operation was ' +
|
|
391
|
-
`measured and no refusal was proven either. What it answered was ${JSON.stringify(one.result)}`,
|
|
392
|
-
)
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (one.refused && one.measured) {
|
|
396
|
-
failures.push(
|
|
397
|
-
'this run was refused before the operation could start and REACHED THE OPERATION anyway — dispatch let a ' +
|
|
398
|
-
'refused request through to the compiled function, which is the gate not holding rather than a trial drifting',
|
|
399
|
-
)
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
if (older === undefined) {
|
|
403
|
-
failures.push('no trial holds this scenario. Run `thread trials` and commit the result')
|
|
404
|
-
} else {
|
|
405
|
-
for (const part of ['result', 'kept', 'called'] as const) {
|
|
406
|
-
const then = printed(older[part])
|
|
407
|
-
const said = printed(one[part])
|
|
408
|
-
if (then === said) continue
|
|
409
|
-
failures.push(
|
|
410
|
-
`${part} is not what was recorded — the operation's behaviour has changed.\n was: ${then.trim().slice(0, 400)}\n now: ${said.trim().slice(0, 400)}`,
|
|
411
|
-
)
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
outcomes.push({
|
|
416
|
-
id,
|
|
417
|
-
operation: one.operation,
|
|
418
|
-
passed: failures.length === 0,
|
|
419
|
-
refused: one.refused,
|
|
420
|
-
measured: one.measured,
|
|
421
|
-
failures,
|
|
422
|
-
})
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
for (const one of was) {
|
|
426
|
-
const id = `${one.operation}/${one.scenario}`
|
|
427
|
-
if (now.some((each) => `${each.operation}/${each.scenario}` === id)) continue
|
|
428
|
-
outcomes.push({
|
|
429
|
-
id,
|
|
430
|
-
operation: one.operation,
|
|
431
|
-
passed: false,
|
|
432
|
-
refused: one.refused,
|
|
433
|
-
measured: false,
|
|
434
|
-
failures: ['this scenario is recorded and no longer runs — an operation or a scenario was removed without the trials being regenerated'],
|
|
435
|
-
})
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
return {
|
|
439
|
-
outcomes,
|
|
440
|
-
passed: outcomes.filter((one) => one.passed).length,
|
|
441
|
-
failed: outcomes.filter((one) => !one.passed).length,
|
|
442
|
-
measured: outcomes.filter((one) => one.measured).length,
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
export function unproven(run: Run): readonly string[] {
|
|
447
|
-
const each = new Map<string, Outcome[]>()
|
|
448
|
-
for (const one of run.outcomes) {
|
|
449
|
-
const held = each.get(one.operation)
|
|
450
|
-
if (held === undefined) each.set(one.operation, [one])
|
|
451
|
-
else held.push(one)
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const said: string[] = []
|
|
455
|
-
for (const [operation, outcomes] of [...each].sort((one, two) => compare(one[0], two[0]))) {
|
|
456
|
-
if (outcomes.some((one) => one.measured)) continue
|
|
457
|
-
|
|
458
|
-
const denied = outcomes.filter((one) => one.refused).length
|
|
459
|
-
const neither = outcomes.length - denied
|
|
460
|
-
const how = [
|
|
461
|
-
denied === 0
|
|
462
|
-
? ''
|
|
463
|
-
: `${String(denied)} were refused before the operation could start — by the gate, or because what they were given is not what it takes — which proves the refusal and nothing whatever about the operation`,
|
|
464
|
-
neither === 0 ? '' : `${String(neither)} neither reached it nor were denied`,
|
|
465
|
-
].filter((one) => one !== '')
|
|
466
|
-
|
|
467
|
-
said.push(
|
|
468
|
-
`\`${operation}\` is UNPROVEN — ${String(outcomes.length)} scenario(s) ran and not one of them reached it: ${how.join(', and ')}. ` +
|
|
469
|
-
'Grant a caller the trial can run as, or give it a scenario the gate admits',
|
|
470
|
-
)
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
return said
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
async function standIns(): Promise<Readonly<Record<string, string>> | string> {
|
|
477
|
-
const at = resolve(ROOT, '_threads/secrets.ts')
|
|
478
|
-
if (!existsSync(at)) return {}
|
|
479
|
-
let held: readonly { readonly id: string; readonly sends?: unknown; readonly signs?: unknown }[]
|
|
480
|
-
try {
|
|
481
|
-
const taken = (await import(pathToFileURL(at).href)) as {
|
|
482
|
-
readonly SECRETS?: readonly { readonly id: string; readonly sends?: unknown; readonly signs?: unknown }[]
|
|
483
|
-
}
|
|
484
|
-
held = taken.SECRETS ?? []
|
|
485
|
-
} catch (thrown) {
|
|
486
|
-
return (
|
|
487
|
-
`_threads/secrets.ts is there and could not be read: ${thrown instanceof Error ? thrown.message : String(thrown)}. ` +
|
|
488
|
-
'A secrets sheet that cannot be read reports exactly like an app that declares none, so every trial would run ' +
|
|
489
|
-
'as though nothing were sealed and pass having proved that'
|
|
490
|
-
)
|
|
491
|
-
}
|
|
492
|
-
const found: Record<string, string> = {}
|
|
493
|
-
for (const one of held) {
|
|
494
|
-
found[one.id] = one.sends === undefined && one.signs === undefined ? `https://${one.id.replace(/_/g, '-')}.invalid` : `${one.id}-stood-in-for`
|
|
495
|
-
}
|
|
496
|
-
return found
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
async function reaching(): Promise<{ readonly targets: readonly Target[]; readonly calls: readonly Call[] } | string> {
|
|
500
|
-
const at = resolve(ROOT, '_threads/targets.ts')
|
|
501
|
-
if (!existsSync(at)) return { targets: [], calls: [] }
|
|
502
|
-
try {
|
|
503
|
-
const taken = (await import(pathToFileURL(at).href)) as { readonly TARGETS?: readonly Target[]; readonly CALLS?: readonly Call[] }
|
|
504
|
-
return { targets: taken.TARGETS ?? [], calls: taken.CALLS ?? [] }
|
|
505
|
-
} catch (thrown) {
|
|
506
|
-
return (
|
|
507
|
-
`_threads/targets.ts is there and could not be read: ${thrown instanceof Error ? thrown.message : String(thrown)}. ` +
|
|
508
|
-
'Which disk a trial hands an operation is read from that sheet, so every trial would run with no disk at all and ' +
|
|
509
|
-
'the failures would name the operation instead of the sheet'
|
|
510
|
-
)
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
export async function record(): Promise<{ readonly trials: readonly Trial[]; readonly refusals: readonly string[] }> {
|
|
515
|
-
const pass = descend()
|
|
516
|
-
const known = types(pass)
|
|
517
|
-
const refusals: string[] = []
|
|
518
|
-
const trials: Trial[] = []
|
|
519
|
-
|
|
520
|
-
const declared = [...operations(pass, known)].filter(([, one]) => one.body !== undefined)
|
|
521
|
-
if (declared.length === 0) return { trials: [], refusals: [] }
|
|
522
|
-
|
|
523
|
-
const stood = await standIns()
|
|
524
|
-
if (typeof stood === 'string') return { trials: [], refusals: [stood] }
|
|
525
|
-
|
|
526
|
-
const reached = await reaching()
|
|
527
|
-
if (typeof reached === 'string') return { trials: [], refusals: [reached] }
|
|
528
|
-
const { targets, calls } = reached
|
|
529
|
-
|
|
530
|
-
const at = resolve(ROOT, DISPATCH_AT)
|
|
531
|
-
if (!existsSync(at)) {
|
|
532
|
-
return {
|
|
533
|
-
trials: [],
|
|
534
|
-
refusals: [
|
|
535
|
-
`this app has no ${DISPATCH_AT}. Run \`thread compile\` — an operation runs through dispatch and there is nothing here to run it with`,
|
|
536
|
-
],
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
const made = (await import(pathToFileURL(at).href)) as {
|
|
540
|
-
readonly dispatch?: (
|
|
541
|
-
operation: Operation<unknown>,
|
|
542
|
-
input: unknown,
|
|
543
|
-
actor: Actor,
|
|
544
|
-
opened: Readonly<Record<string, unknown>>,
|
|
545
|
-
opening: Vault,
|
|
546
|
-
) => Promise<Result<unknown>>
|
|
547
|
-
}
|
|
548
|
-
const dispatch = made.dispatch
|
|
549
|
-
if (dispatch === undefined) {
|
|
550
|
-
return { trials: [], refusals: [`${DISPATCH_AT} exports no \`dispatch\`, so nothing here can run an operation`] }
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
const permitted = resolve(ROOT, ACCESS_AT)
|
|
554
|
-
const may = new Map<string, string[]>()
|
|
555
|
-
if (existsSync(permitted)) {
|
|
556
|
-
const sheet = (await import(pathToFileURL(permitted).href)) as {
|
|
557
|
-
readonly GRANTS?: readonly {
|
|
558
|
-
readonly may?: readonly string[]
|
|
559
|
-
readonly to?: { readonly roles?: readonly string[] }
|
|
560
|
-
}[]
|
|
561
|
-
}
|
|
562
|
-
for (const one of sheet.GRANTS ?? []) {
|
|
563
|
-
for (const id of one.may ?? []) {
|
|
564
|
-
may.set(id, [...new Set([...(may.get(id) ?? []), ...(one.to?.roles ?? [])])])
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
const compiled = resolve(ROOT, PERMITS_AT)
|
|
570
|
-
const permits = existsSync(compiled)
|
|
571
|
-
? ((await import(pathToFileURL(compiled).href)) as {
|
|
572
|
-
readonly permits?: (actor: Actor, action: string, resource: Row | null) => { allow: boolean; because?: string }
|
|
573
|
-
}).permits
|
|
574
|
-
: undefined
|
|
575
|
-
if (existsSync(compiled) && permits === undefined) {
|
|
576
|
-
return {
|
|
577
|
-
trials: [],
|
|
578
|
-
refusals: [`${PERMITS_AT} exports no \`permits\`, so a trial cannot be answered by the gate this app derives`],
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
for (const [id, one] of [...declared].sort((a, b) => compare(a[0], b[0]))) {
|
|
582
|
-
if (one.body === undefined) continue
|
|
583
|
-
const from = resolve(ROOT, one.body)
|
|
584
|
-
if (!existsSync(from)) {
|
|
585
|
-
refusals.push(`\`${id}\` derives ${one.body} and that file is not there. Run \`thread compile\``)
|
|
586
|
-
continue
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
const held = (await import(pathToFileURL(from).href)) as { readonly operation?: Operation<unknown> }
|
|
590
|
-
const operation = held.operation
|
|
591
|
-
if (operation === undefined) {
|
|
592
|
-
refusals.push(`${one.body} exports no \`operation\`, so \`${id}\` cannot be run`)
|
|
593
|
-
continue
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
const takes = one.takes[0]?.at
|
|
597
|
-
if (takes === undefined) {
|
|
598
|
-
refusals.push(`\`${id}\` names nothing it takes, so nothing states its input or what to try it with`)
|
|
599
|
-
continue
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
const scenarios = await scenariosFor(operation, takes, may.get(id) ?? [])
|
|
603
|
-
if (typeof scenarios === 'string') {
|
|
604
|
-
refusals.push(scenarios)
|
|
605
|
-
continue
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
for (const scenario of scenarios) {
|
|
609
|
-
const ran = await runScenario(operation, scenario, dispatch, stood, targets, calls)
|
|
610
|
-
if (typeof ran === 'string') refusals.push(ran)
|
|
611
|
-
else trials.push(ran)
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
return { trials, refusals }
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
function kept(): readonly Trial[] {
|
|
619
|
-
const at = resolve(ROOT, TRIALS_AT)
|
|
620
|
-
if (!existsSync(at)) return []
|
|
621
|
-
return JSON.parse(readFileSync(at, 'utf8')) as readonly Trial[]
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
if (invoked(import.meta.url)) {
|
|
625
|
-
const { trials, refusals } = await record()
|
|
626
|
-
|
|
627
|
-
for (const one of refusals) console.error(`trial: ${one}`)
|
|
628
|
-
|
|
629
|
-
if (argv.includes('--check')) {
|
|
630
|
-
const run = compared(kept(), trials)
|
|
631
|
-
for (const outcome of run.outcomes) {
|
|
632
|
-
if (outcome.passed) continue
|
|
633
|
-
for (const failure of outcome.failures) console.error(`trial: ${outcome.id} — ${failure}`)
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
if (run.outcomes.length === 0 && refusals.length === 0) {
|
|
637
|
-
console.log('trial: this app declares no operation, so there is nothing to prove.')
|
|
638
|
-
exit(0)
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
const short = unproven(run)
|
|
642
|
-
for (const one of short) console.error(`trial: ${one}`)
|
|
643
|
-
|
|
644
|
-
console.log(
|
|
645
|
-
`trial: ${String(run.passed)} of ${String(run.outcomes.length)} passed, ${String(run.measured)} measured, ` +
|
|
646
|
-
`${String(short.length)} operation(s) unproven.`,
|
|
647
|
-
)
|
|
648
|
-
exit(run.failed === 0 && refusals.length === 0 && short.length === 0 ? 0 : 1)
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
if (refusals.length > 0) {
|
|
652
|
-
console.error(
|
|
653
|
-
`trial: ${String(refusals.length)} refusal(s) above, so ${TRIALS_AT} is left as it was — ` +
|
|
654
|
-
'a run that was refused has nothing to record, and recording it would erase what does',
|
|
655
|
-
)
|
|
656
|
-
exit(1)
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
const at = resolve(ROOT, TRIALS_AT)
|
|
660
|
-
writeFileSync(at, printed(trials))
|
|
661
|
-
const measured = trials.filter((one) => one.measured).length
|
|
662
|
-
stdout.write(
|
|
663
|
-
`trial: wrote ${TRIALS_AT} — ${String(trials.length)} trial(s) from ${String(measured)} run(s) that reached an operation.\n`,
|
|
664
|
-
)
|
|
665
|
-
exit(0)
|
|
666
|
-
}
|