@prestack/thread 0.0.1 → 0.0.3
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/dist/core/actor.ts +20 -0
- package/dist/core/asking.d.ts +0 -1
- package/dist/core/asking.js +0 -1
- package/dist/core/asking.ts +1342 -0
- package/dist/core/attach.js +2 -2
- package/dist/core/attach.ts +585 -0
- package/dist/core/chain.js +8 -1
- package/dist/core/chain.ts +148 -0
- package/dist/core/cli.js +97 -18
- package/dist/core/cli.ts +2004 -0
- package/dist/core/condition.ts +227 -0
- package/dist/core/descend.d.ts +1 -0
- package/dist/core/descend.js +36 -0
- package/dist/core/descend.ts +1001 -0
- package/dist/core/finding.ts +38 -0
- package/dist/core/generators/meta.js +6 -0
- package/dist/core/generators/meta.ts +1006 -0
- package/dist/core/init.ts +170 -0
- package/dist/core/invoked.ts +47 -0
- package/dist/core/round.ts +57 -0
- package/dist/core/rows.ts +369 -0
- package/dist/core/scope.ts +18 -0
- package/dist/core/thread.ts +29 -0
- package/dist/core/tree.ts +67 -0
- package/dist/core/types.ts +350 -0
- package/dist/derivation/attach.js +1 -1
- package/dist/derivation/attach.ts +68 -0
- package/dist/derivation/generators/build.ts +232 -0
- package/dist/derivation/generators/check.ts +118 -0
- package/dist/derivation/generators/derivation.ts +347 -0
- package/dist/derivation/generators/determinism.ts +145 -0
- package/dist/derivation/generators/release.js +19 -9
- package/dist/derivation/generators/release.ts +213 -0
- package/dist/derivation/makers/release.ts +135 -0
- package/dist/derivation/release.ts +23 -0
- package/dist/integrations/attach.js +2 -2
- package/dist/integrations/attach.ts +207 -0
- package/dist/integrations/generators/read.ts +127 -0
- package/dist/integrations/generators/secret.ts +289 -0
- package/dist/integrations/generators/target.js +16 -6
- package/dist/integrations/generators/target.ts +944 -0
- package/dist/integrations/generators/vault.js +46 -14
- package/dist/integrations/generators/vault.ts +212 -0
- package/dist/integrations/{sdk.js → module.js} +4 -4
- package/dist/integrations/module.ts +189 -0
- package/dist/integrations/secret.ts +50 -0
- package/dist/integrations/target.d.ts +3 -1
- package/dist/integrations/target.js +14 -11
- package/dist/integrations/target.ts +340 -0
- package/dist/integrations/vault.d.ts +2 -0
- package/dist/integrations/vault.js +4 -2
- package/dist/integrations/vault.ts +136 -0
- package/dist/integrity/attach.ts +234 -0
- package/dist/integrity/check-threads.js +2 -26
- package/dist/integrity/check-threads.ts +166 -0
- package/dist/integrity/constraint.ts +1202 -0
- package/dist/integrity/generators/compiler.ts +161 -0
- package/dist/integrity/generators/gate.ts +201 -0
- package/dist/integrity/generators/issues.ts +160 -0
- package/dist/integrity/generators/publish.js +14 -5
- package/dist/integrity/generators/publish.ts +247 -0
- package/dist/integrity/generators/rule.ts +271 -0
- package/dist/integrity/generators/seal.ts +117 -0
- package/dist/integrity/generators/trial.ts +320 -0
- package/dist/integrity/lint.ts +227 -0
- package/dist/integrity/lsp/bin.ts +42 -0
- package/dist/integrity/lsp/diagnostics.ts +92 -0
- package/dist/integrity/lsp/findings.ts +63 -0
- package/dist/integrity/lsp/lint.ts +140 -0
- package/dist/integrity/lsp/protocol.ts +78 -0
- package/dist/integrity/lsp/server.ts +224 -0
- package/dist/integrity/sealed.js +2 -2
- package/dist/integrity/sealed.ts +56 -0
- package/dist/integrity/slopnator.ts +350 -0
- package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
- package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
- package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
- package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
- package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
- package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
- package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
- package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
- package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
- package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
- package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
- package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
- package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
- package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
- package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
- package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
- package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
- package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
- package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
- package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
- package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
- package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
- package/dist/integrity/sweep.ts +307 -0
- package/dist/knowledge/attach.ts +350 -0
- package/dist/knowledge/generators/docsgen.ts +174 -0
- package/dist/knowledge/generators/glossary.ts +73 -0
- package/dist/knowledge/generators/skills.ts +263 -0
- package/dist/knowledge/glossary.ts +6 -0
- package/dist/operations/access.ts +88 -0
- package/dist/operations/attach.ts +475 -0
- package/dist/operations/capabilities.ts +65 -0
- package/dist/operations/cost.ts +58 -0
- package/dist/operations/dispatch.ts +348 -0
- package/dist/operations/error-results.ts +31 -0
- package/dist/operations/errors/catalog.ts +168 -0
- package/dist/operations/errors/classes.ts +51 -0
- package/dist/operations/errors/errors.ts +314 -0
- package/dist/operations/generators/capability.js +29 -19
- package/dist/operations/generators/capability.ts +704 -0
- package/dist/operations/generators/compile.js +20 -1
- package/dist/operations/generators/compile.ts +643 -0
- package/dist/operations/generators/grant.ts +336 -0
- package/dist/operations/generators/operation.ts +972 -0
- package/dist/operations/generators/permits.ts +223 -0
- package/dist/operations/generators/seed.ts +347 -0
- package/dist/operations/generators/trial.js +4 -1
- package/dist/operations/generators/trial.ts +529 -0
- package/dist/operations/http.ts +164 -0
- package/dist/operations/makers/capability.ts +166 -0
- package/dist/operations/makers/trial.ts +520 -0
- package/dist/operations/memory.ts +470 -0
- package/dist/operations/metering.ts +328 -0
- package/dist/operations/nodes.ts +54 -0
- package/dist/operations/operations.ts +32 -0
- package/dist/operations/queue-policy.ts +10 -0
- package/dist/operations/result.ts +28 -0
- package/dist/operations/seams.d.ts +4 -4
- package/dist/operations/seams.ts +364 -0
- package/dist/operations/store.ts +29 -0
- package/package.json +8 -1
- package/dist/integrations/generators/sdk.d.ts +0 -1
- package/dist/integrations/generators/sdk.js +0 -148
- /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
|
+
import { mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { dirname, join, relative } from 'node:path'
|
|
4
|
+
import { argv, cwd, env, execPath, stdout } from 'node:process'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import { SKILLS_AT, declared, nouns, skillOf } from '../../core/asking.ts'
|
|
8
|
+
import { generators } from '../../core/chain.ts'
|
|
9
|
+
import { TREE } from '../../core/descend.ts'
|
|
10
|
+
import { LOADER, exit, invoked } from '../../core/invoked.ts'
|
|
11
|
+
import { ORGANISATION, named } from '../../core/scope.ts'
|
|
12
|
+
import type { Tree } from '../../core/tree.ts'
|
|
13
|
+
import { findTree } from '../../core/tree.ts'
|
|
14
|
+
|
|
15
|
+
const WHERE = SKILLS_AT
|
|
16
|
+
|
|
17
|
+
const RECEIPT_LINE = /^[a-z]+: receipt .*$/gm
|
|
18
|
+
|
|
19
|
+
const NONCE = /--receipt [0-9a-f]{6,}/g
|
|
20
|
+
|
|
21
|
+
const CRLF = /\r\n/g
|
|
22
|
+
|
|
23
|
+
const META = fileURLToPath(new URL('../../core/generators/meta.ts', import.meta.url))
|
|
24
|
+
|
|
25
|
+
const fileFor = (noun: string): string => skillOf(noun)
|
|
26
|
+
|
|
27
|
+
async function offering(tree: Tree): Promise<ReadonlyMap<string, string>> {
|
|
28
|
+
const answers = relative(tree.at, META).split('\\').join('/')
|
|
29
|
+
const found = new Map(nouns(await generators(tree)))
|
|
30
|
+
found.set('meta', answers)
|
|
31
|
+
for (const noun of (await declared(tree)).keys()) if (!found.has(noun)) found.set(noun, answers)
|
|
32
|
+
return found
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function printed(root: string, at: string, noun: string): { readonly refused: string; readonly body: string } {
|
|
36
|
+
if (at === '') return { refused: `nothing in this tree is written by \`thread create ${noun}\``, body: '' }
|
|
37
|
+
|
|
38
|
+
const ran = spawnSync(execPath, [...LOADER, join(root, at)], {
|
|
39
|
+
cwd: root,
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
env: { ...env, THREAD_NOUN: noun },
|
|
42
|
+
|
|
43
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
})
|
|
45
|
+
const body = String(ran.stdout ?? '').replace(CRLF, '\n').replace(RECEIPT_LINE, '').trimEnd()
|
|
46
|
+
const said = String(ran.stderr ?? '').replace(CRLF, '\n').trim().split('\n')[0] ?? ''
|
|
47
|
+
return { refused: body === '' ? said || 'it printed nothing' : '', body }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function skill(noun: string, body: string): string {
|
|
51
|
+
return [
|
|
52
|
+
`# ${noun}`,
|
|
53
|
+
'',
|
|
54
|
+
`Everything \`npx thread create ${noun}\` knows, written out by \`npx thread skills\`.`,
|
|
55
|
+
'',
|
|
56
|
+
'**Do not edit this file.** It is derived from the tree, so an edit here is reverted the next',
|
|
57
|
+
'time the command runs, and `npx thread skills --check` fails until it is. If something below is',
|
|
58
|
+
'wrong or missing, the command that printed it is what to change.',
|
|
59
|
+
'',
|
|
60
|
+
`**Read this before you declare a ${noun}.** What follows is what already exists. The reason it`,
|
|
61
|
+
'exists is that things get built twice by people who did not look — thirteen elements were once',
|
|
62
|
+
'declared in one app that every one of them already shipped, and the cost was two days of fixes',
|
|
63
|
+
'landing on a page nobody was looking at. Open what it names before you add to it.',
|
|
64
|
+
'',
|
|
65
|
+
'When you are ready to write, run the command yourself for a live receipt. The one printed here',
|
|
66
|
+
'has been removed on purpose: a receipt is spent by one run and goes stale the moment its sheet',
|
|
67
|
+
'changes, so a copy kept in a file is always the wrong one.',
|
|
68
|
+
'',
|
|
69
|
+
'```',
|
|
70
|
+
body.replace(NONCE, '--receipt <receipt>'),
|
|
71
|
+
'```',
|
|
72
|
+
'',
|
|
73
|
+
].join('\n')
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const SHEET = `${TREE}_generated/skills.ts`
|
|
77
|
+
|
|
78
|
+
function sheet(files: readonly string[]): string {
|
|
79
|
+
const rows = files.map((at) => {
|
|
80
|
+
const noun = at.slice(WHERE.length + 1, -'.md'.length)
|
|
81
|
+
return [
|
|
82
|
+
' {',
|
|
83
|
+
` id: 'skill-${noun}',`,
|
|
84
|
+
` at: '${at}',`,
|
|
85
|
+
` describes: 'What \`thread create ${noun}\` teaches: the ${noun}s this tree already has, with the sentence each one states, and the rules for declaring another. Written from what that command prints about itself, so the skill and the command cannot disagree.',`,
|
|
86
|
+
' version: 1,',
|
|
87
|
+
" by: 'thread skills',",
|
|
88
|
+
" runs: 'thread skills --check',",
|
|
89
|
+
' },',
|
|
90
|
+
].join('\n')
|
|
91
|
+
})
|
|
92
|
+
return [
|
|
93
|
+
"import type { Threaded } from '../../thread/core/thread.ts'",
|
|
94
|
+
'',
|
|
95
|
+
'export const SKILLS: Threaded = {',
|
|
96
|
+
" id: 'skills',",
|
|
97
|
+
` at: '${SHEET}',`,
|
|
98
|
+
" describes: 'One row per skill `thread skills` writes, so every `skills/<noun>.md` is declared by the command that wrote it instead of being refused as a file nothing named. It sits beside the files it names because the nouns are not known until the tree is read, and a list somebody keeps by hand would go stale the first time a package brings a new maker.',",
|
|
99
|
+
' version: 1,',
|
|
100
|
+
' derives: [',
|
|
101
|
+
...rows,
|
|
102
|
+
' ],',
|
|
103
|
+
'}',
|
|
104
|
+
'',
|
|
105
|
+
].join('\n')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const HERE = (): string => cwd()
|
|
109
|
+
|
|
110
|
+
const COINS = /by:\s*'thread create ([a-z][a-z0-9-]*)'/g
|
|
111
|
+
|
|
112
|
+
const ANSWERS_AT = new RegExp(`at:\\s*'node_modules/${ORGANISATION}/[^/]+/(dist/[^']+)'`, 'g')
|
|
113
|
+
|
|
114
|
+
function ownMakers(root: string): ReadonlyMap<string, string> {
|
|
115
|
+
const found = new Map<string, string>()
|
|
116
|
+
|
|
117
|
+
let names: readonly string[]
|
|
118
|
+
try {
|
|
119
|
+
names = readdirSync(join(root, 'src'), { recursive: true }) as readonly string[]
|
|
120
|
+
} catch {
|
|
121
|
+
return found
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const one of names) {
|
|
125
|
+
const path = String(one).split('\\').join('/')
|
|
126
|
+
if (!path.endsWith('attach.ts')) continue
|
|
127
|
+
|
|
128
|
+
let text: string
|
|
129
|
+
try {
|
|
130
|
+
text = readFileSync(join(root, 'src', String(one)), 'utf8')
|
|
131
|
+
} catch {
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
for (const row of text.split(/\{\s*$/m)) {
|
|
136
|
+
const noun = [...row.matchAll(COINS)].at(0)?.[1]
|
|
137
|
+
const at = [...row.matchAll(ANSWERS_AT)].at(-1)?.[1]
|
|
138
|
+
if (noun === undefined || at === undefined || found.has(noun)) continue
|
|
139
|
+
found.set(noun, join(root, at))
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return found
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function printedOwn(at: string): { readonly refused: string; readonly body: string } {
|
|
147
|
+
const ran = spawnSync(execPath, ['--experimental-strip-types', at, '--own'], {
|
|
148
|
+
encoding: 'utf8',
|
|
149
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
150
|
+
})
|
|
151
|
+
const body = String(ran.stdout ?? '').replace(CRLF, '\n').replace(RECEIPT_LINE, '').trimEnd()
|
|
152
|
+
const said = String(ran.stderr ?? '').replace(CRLF, '\n').trim().split('\n')[0] ?? ''
|
|
153
|
+
return { refused: ran.status === 0 && body !== '' ? '' : said || 'it printed nothing', body }
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function run(): Promise<number> {
|
|
157
|
+
const own = argv.includes('--own')
|
|
158
|
+
const checking = argv.includes('--check')
|
|
159
|
+
|
|
160
|
+
const root = own ? HERE() : (findTree() as Tree).at
|
|
161
|
+
|
|
162
|
+
const mine = own ? ownMakers(root) : new Map<string, string>()
|
|
163
|
+
const theirs = own ? new Map<string, string>() : await offering(findTree() as Tree)
|
|
164
|
+
const known = [...(own ? mine : theirs).keys()].sort()
|
|
165
|
+
|
|
166
|
+
if (known.length === 0) {
|
|
167
|
+
stdout.write(
|
|
168
|
+
own
|
|
169
|
+
? 'skills: this package ships no maker, so it coins no noun and has no skill to write.\n'
|
|
170
|
+
: 'skills: this tree declares no `create` command yet, so there is no method to write down. ' +
|
|
171
|
+
`Attach a package that brings one — \`npx thread attach ${named('ui')}\` — and run this again.\n`,
|
|
172
|
+
)
|
|
173
|
+
return 0
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const wanted = new Map<string, string>()
|
|
177
|
+
const skipped: string[] = []
|
|
178
|
+
for (const noun of known) {
|
|
179
|
+
const { refused, body } = own ? printedOwn(mine.get(noun) ?? '') : printed(root, theirs.get(noun) ?? '', noun)
|
|
180
|
+
if (refused !== '') {
|
|
181
|
+
skipped.push(` ${noun} — ${refused}`)
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
wanted.set(fileFor(noun), skill(noun, body))
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (wanted.size === 0) {
|
|
188
|
+
stdout.write(
|
|
189
|
+
`skills: every \`create\` this tree declares refused, so there is no method to write down yet:\n${skipped.join('\n')}\n`,
|
|
190
|
+
)
|
|
191
|
+
return 1
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const made = wanted.size
|
|
195
|
+
const sheetAt = own ? '' : SHEET
|
|
196
|
+
if (sheetAt !== '') wanted.set(sheetAt, sheet([...wanted.keys()].sort()))
|
|
197
|
+
|
|
198
|
+
const held = new Map<string, string>()
|
|
199
|
+
try {
|
|
200
|
+
for (const name of readdirSync(join(root,WHERE))) {
|
|
201
|
+
if (!name.endsWith('.md')) continue
|
|
202
|
+
held.set(join(WHERE, name), readFileSync(join(root,WHERE, name), 'utf8').replace(CRLF, '\n'))
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (sheetAt !== '') {
|
|
209
|
+
try {
|
|
210
|
+
held.set(sheetAt, readFileSync(join(root, sheetAt), 'utf8').replace(CRLF, '\n'))
|
|
211
|
+
} catch {
|
|
212
|
+
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const stale = [...held.keys()].filter((one) => !wanted.has(one)).sort()
|
|
217
|
+
const differs = [...wanted.entries()].filter(([one, text]) => held.get(one) !== text).map(([one]) => one).sort()
|
|
218
|
+
|
|
219
|
+
if (checking) {
|
|
220
|
+
if (stale.length === 0 && differs.length === 0) {
|
|
221
|
+
stdout.write(
|
|
222
|
+
`skills: ${String(made)} skill(s), each matching what its own command prints` +
|
|
223
|
+
(sheetAt === '' ? '' : `, and each declared by a row in ${sheetAt}`) +
|
|
224
|
+
'.\n',
|
|
225
|
+
)
|
|
226
|
+
return 0
|
|
227
|
+
}
|
|
228
|
+
for (const one of differs) {
|
|
229
|
+
stdout.write(
|
|
230
|
+
`skills: ${one} is not what \`npx thread skills\` writes. A skill is derived from the command ` +
|
|
231
|
+
'it describes, so an edit here is a second statement of the method and will drift from the ' +
|
|
232
|
+
'first. Change the command, then run `npx thread skills`.\n',
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
for (const one of stale) {
|
|
236
|
+
stdout.write(
|
|
237
|
+
`skills: ${one} names a \`create\` this tree no longer declares, so it teaches a command that ` +
|
|
238
|
+
'will refuse. Run `npx thread skills`.\n',
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
return 1
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const [one, text] of wanted) {
|
|
245
|
+
mkdirSync(dirname(join(root,one)), { recursive: true })
|
|
246
|
+
writeFileSync(join(root,one), text, 'utf8')
|
|
247
|
+
}
|
|
248
|
+
for (const one of stale) rmSync(join(root,one), { force: true })
|
|
249
|
+
|
|
250
|
+
stdout.write(
|
|
251
|
+
`skills: wrote ${String(made)} skill(s) into ${WHERE}/` +
|
|
252
|
+
(sheetAt === '' ? '' : `, and the ${String(made)} row(s) declaring them into ${sheetAt}`) +
|
|
253
|
+
(stale.length === 0
|
|
254
|
+
? ''
|
|
255
|
+
: `, and removed ${String(stale.length)} naming a command this tree no longer has`) +
|
|
256
|
+
'.\n' +
|
|
257
|
+
|
|
258
|
+
(skipped.length === 0 ? '' : `skills: no method to keep yet for:\n${skipped.join('\n')}\n`),
|
|
259
|
+
)
|
|
260
|
+
return 0
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (invoked(import.meta.url)) exit(await run())
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { Actor } from '../core/actor.ts'
|
|
2
|
+
import { mentionsActor, walkOperands, type Condition } from '../core/condition.ts'
|
|
3
|
+
import type { Member } from '../core/thread.ts'
|
|
4
|
+
|
|
5
|
+
export const PERMITS_AT = 'src/_generated/permits.ts'
|
|
6
|
+
|
|
7
|
+
export function declaredWords(
|
|
8
|
+
entities: readonly {
|
|
9
|
+
readonly holds: readonly { readonly id: string; readonly options?: readonly string[] }[]
|
|
10
|
+
readonly actor?: { readonly roles: string; readonly entitlements?: string }
|
|
11
|
+
}[],
|
|
12
|
+
which: 'roles' | 'entitlements',
|
|
13
|
+
everybody: string,
|
|
14
|
+
): readonly string[] {
|
|
15
|
+
const found = entities.flatMap((one) => {
|
|
16
|
+
const at = one.actor?.[which]
|
|
17
|
+
return at === undefined ? [] : (one.holds.find((each) => each.id === at)?.options ?? [])
|
|
18
|
+
})
|
|
19
|
+
return [...new Set(which === 'roles' ? [everybody, ...found] : found)]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function undeclared(answered: string, offers: readonly string[], which: 'roles' | 'entitlements'): string | undefined {
|
|
23
|
+
const stray = answered
|
|
24
|
+
.split(',')
|
|
25
|
+
.map((one) => one.trim())
|
|
26
|
+
.filter((one) => one !== '' && !offers.includes(one))
|
|
27
|
+
if (stray.length === 0) return undefined
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
`this app declares no ${which === 'roles' ? 'role' : 'entitlement'} called ${stray.map((one) => `\`${one}\``).join(', ')} — ` +
|
|
31
|
+
(offers.length === 0
|
|
32
|
+
? `it declares none at all. A ${which === 'roles' ? 'role' : 'entitlement'} is one of the words the value an ` +
|
|
33
|
+
'entity names as saying what a caller may do is held to. Declare them with `thread create entity edit` — ' +
|
|
34
|
+
'name that value, give it the `choice` format, and state its options'
|
|
35
|
+
: `it declares ${offers.map((one) => `\`${one}\``).join(', ')}`)
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface Grant extends Member {
|
|
40
|
+
|
|
41
|
+
readonly may: readonly string[]
|
|
42
|
+
|
|
43
|
+
readonly on?: string
|
|
44
|
+
|
|
45
|
+
readonly to?: {
|
|
46
|
+
readonly roles?: Actor['roles']
|
|
47
|
+
readonly entitlements?: Actor['entitlements']
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
readonly when?: Condition
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function opensToEverybody(grants: readonly Grant[]): readonly Grant[] {
|
|
54
|
+
return grants.filter((one) => one.to === undefined && (one.when === undefined || !mentionsActor(one.when)))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function grantedTwice(grants: readonly Grant[]): readonly string[] {
|
|
58
|
+
const seen = new Set<string>()
|
|
59
|
+
const twice = new Set<string>()
|
|
60
|
+
for (const one of grants) {
|
|
61
|
+
if (seen.has(one.id)) twice.add(one.id)
|
|
62
|
+
seen.add(one.id)
|
|
63
|
+
}
|
|
64
|
+
return [...twice].sort()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function operandsOf(one: Grant): readonly string[] {
|
|
68
|
+
if (one.when === undefined) return []
|
|
69
|
+
const found: string[] = []
|
|
70
|
+
walkOperands(one.when, (each) => {
|
|
71
|
+
if ('agg' in each) found.push('agg')
|
|
72
|
+
if ('was' in each) found.push('was')
|
|
73
|
+
if ('outer' in each) found.push('outer')
|
|
74
|
+
})
|
|
75
|
+
return found
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function unanswerable(grants: readonly Grant[]): readonly string[] {
|
|
79
|
+
return grants.flatMap((one) => {
|
|
80
|
+
const held = [...new Set(operandsOf(one))]
|
|
81
|
+
if (held.length === 0) return []
|
|
82
|
+
const why = held.includes('agg')
|
|
83
|
+
? 'it counts other rows, and a gate is asked about the one in front of it. Put the aggregate in an invariant on the entity, where it runs against the rows themselves'
|
|
84
|
+
: 'it reads a value as it was before the write, and at the moment this is asked the write has not happened. Only an invariant, which runs on the write itself, can see a before'
|
|
85
|
+
return [`\`${one.id}\` states a condition this gate cannot answer — ${why}`]
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|