@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
package/dist/core/chain.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { starts } from '../integrations/cli.ts'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { execPath } from 'node:process'
|
|
4
|
-
import { fileURLToPath } from 'node:url'
|
|
5
|
-
|
|
6
|
-
import type { Row } from './descend.ts'
|
|
7
|
-
import { existsSync, readFileSync, readdirSync } from '../integrations/disk.ts'
|
|
8
|
-
import { valuesIn } from './rows.ts'
|
|
9
|
-
import { findTree, type Tree } from './tree.ts'
|
|
10
|
-
|
|
11
|
-
const STATING = 'derivation.ts'
|
|
12
|
-
|
|
13
|
-
export async function generators(tree: Tree = findTree()): Promise<readonly Row[]> {
|
|
14
|
-
if (!existsSync(tree.dir)) return []
|
|
15
|
-
|
|
16
|
-
const sheets = readdirSync(tree.dir)
|
|
17
|
-
.filter((one) => one.endsWith('.ts'))
|
|
18
|
-
.sort((one, two) => (one === STATING ? -1 : two === STATING ? 1 : one < two ? -1 : one > two ? 1 : 0))
|
|
19
|
-
|
|
20
|
-
const found: Row[] = []
|
|
21
|
-
for (const name of sheets) {
|
|
22
|
-
const sheet = join(tree.dir, name)
|
|
23
|
-
try {
|
|
24
|
-
const held = valuesIn(readFileSync(sheet)) as { GENERATORS?: readonly Row[] }
|
|
25
|
-
found.push(...(held.GENERATORS ?? []))
|
|
26
|
-
} catch (thrown) {
|
|
27
|
-
throw new Error(
|
|
28
|
-
`${sheet} is one of the sheets where this tree states the commands it offers, and reading it threw: ` +
|
|
29
|
-
`${thrown instanceof Error ? thrown.message : String(thrown)}. A tree whose generators cannot be read ` +
|
|
30
|
-
'offers exactly what a tree declaring none offers, so everything derived from your declarations would be ' +
|
|
31
|
-
'left stale by a command reporting that it had finished. Fix that file.',
|
|
32
|
-
)
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
return found
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export const offers = (all: readonly Row[]): ReadonlySet<string> =>
|
|
39
|
-
new Set(all.map((one) => (typeof one.by === 'string' ? one.by : '')).filter((one) => one !== ''))
|
|
40
|
-
|
|
41
|
-
const ROUNDS = 4
|
|
42
|
-
|
|
43
|
-
const word = (held: unknown): string => (typeof held === 'string' ? held.trim() : '')
|
|
44
|
-
|
|
45
|
-
const FROM_SOURCE = import.meta.url.endsWith('.ts')
|
|
46
|
-
|
|
47
|
-
const RUNNER = fileURLToPath(new URL(FROM_SOURCE ? './round.ts' : './round.js', import.meta.url))
|
|
48
|
-
|
|
49
|
-
const LOADER: readonly string[] = FROM_SOURCE ? ['--import', 'tsx'] : []
|
|
50
|
-
|
|
51
|
-
export const RAN = 'round: ran'
|
|
52
|
-
export const REFUSED_BY = 'round: refused'
|
|
53
|
-
|
|
54
|
-
export function batched(
|
|
55
|
-
at: string,
|
|
56
|
-
commands: readonly string[],
|
|
57
|
-
within?: number,
|
|
58
|
-
): readonly { readonly command: string; readonly ok: boolean; readonly said: string }[] {
|
|
59
|
-
if (commands.length === 0) return []
|
|
60
|
-
|
|
61
|
-
const out = starts(execPath, [...LOADER, RUNNER, ...new Set(commands)], { at, shows: 'errors', within })
|
|
62
|
-
const lines = out.out.split('\n')
|
|
63
|
-
|
|
64
|
-
const found = new Map<string, { readonly command: string; readonly ok: boolean; readonly said: string }>()
|
|
65
|
-
let held: string[] = []
|
|
66
|
-
for (const line of lines) {
|
|
67
|
-
const mark = line.startsWith(RAN) ? RAN : line.startsWith(REFUSED_BY) ? REFUSED_BY : undefined
|
|
68
|
-
if (mark === undefined) {
|
|
69
|
-
held.push(line)
|
|
70
|
-
continue
|
|
71
|
-
}
|
|
72
|
-
const command = line.slice(mark.length).trim()
|
|
73
|
-
found.set(command, { command, ok: mark === RAN, said: held.join('\n').trim() })
|
|
74
|
-
held = []
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const orphaned = held.join('\n').trim()
|
|
78
|
-
const died =
|
|
79
|
-
out.why !== ''
|
|
80
|
-
? ` The runner itself failed to start or was stopped: ${out.why}.`
|
|
81
|
-
: out.killed !== null
|
|
82
|
-
? ` The runner was killed by ${out.killed} after ${String(within ?? 0)} ms. The last "settle: running" line above names the command it was inside, and everything after that one is unreported rather than refused.`
|
|
83
|
-
: found.size === 0 && !out.ok
|
|
84
|
-
? ` The runner exited ${String(out.code)} having reported nothing, so it died before the first command finished.`
|
|
85
|
-
: ''
|
|
86
|
-
let firstUnreached = true
|
|
87
|
-
return commands.map((one) => {
|
|
88
|
-
const has = found.get(one)
|
|
89
|
-
if (has !== undefined) return has
|
|
90
|
-
const why = `settle: the runner never reported on \`${one}\`, so it was not run and is not settled.${died}`
|
|
91
|
-
const said = firstUnreached && orphaned !== '' ? `${orphaned}\n${why}` : why
|
|
92
|
-
firstUnreached = false
|
|
93
|
-
return { command: one, ok: false, said }
|
|
94
|
-
})
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const WITHIN = 300_000
|
|
98
|
-
|
|
99
|
-
function batch(
|
|
100
|
-
at: string,
|
|
101
|
-
commands: readonly string[],
|
|
102
|
-
): { readonly said: readonly string[]; readonly refused: readonly string[] } {
|
|
103
|
-
const all = batched(at, commands, WITHIN)
|
|
104
|
-
return {
|
|
105
|
-
said: all.map((one) => one.said).filter((one) => one !== ''),
|
|
106
|
-
refused: all.filter((one) => !one.ok).map((one) => one.command),
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function settling(
|
|
111
|
-
at: string,
|
|
112
|
-
all: readonly Row[],
|
|
113
|
-
skip: readonly string[] = [],
|
|
114
|
-
): { readonly ran: readonly string[]; readonly unsettled: readonly string[]; readonly rounds: number } {
|
|
115
|
-
const mine = all
|
|
116
|
-
.filter((one) => word(one.by) !== '' && word(one.runs) !== '' && word(one.by) !== word(one.runs) && !skip.includes(word(one.by)))
|
|
117
|
-
.filter((one, order, every) => every.findIndex((each) => word(each.by) === word(one.by)) === order)
|
|
118
|
-
|
|
119
|
-
const ran: string[] = []
|
|
120
|
-
let round = 0
|
|
121
|
-
let before: readonly string[] | undefined
|
|
122
|
-
|
|
123
|
-
while (round < ROUNDS) {
|
|
124
|
-
round += 1
|
|
125
|
-
|
|
126
|
-
const asking = batch(at, mine.map((one) => word(one.runs))).refused
|
|
127
|
-
const unhappy = mine.filter((one) => asking.includes(word(one.runs)))
|
|
128
|
-
if (unhappy.length === 0) return { ran, unsettled: [], rounds: round }
|
|
129
|
-
|
|
130
|
-
const still = unhappy.map((one) => word(one.runs))
|
|
131
|
-
const seen = before
|
|
132
|
-
if (seen !== undefined && still.length === seen.length && still.every((one) => seen.includes(one))) {
|
|
133
|
-
return { ran, unsettled: still, rounds: round }
|
|
134
|
-
}
|
|
135
|
-
before = still
|
|
136
|
-
|
|
137
|
-
const writing = batch(at, unhappy.map((one) => word(one.by)))
|
|
138
|
-
for (const said of writing.said) ran.push(said)
|
|
139
|
-
const refused = unhappy.filter((one) => writing.refused.includes(word(one.by))).map((one) => word(one.runs))
|
|
140
|
-
|
|
141
|
-
if (refused.length === unhappy.length) return { ran, unsettled: refused, rounds: round }
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return { ran, unsettled: batch(at, mine.map((one) => word(one.runs))).refused, rounds: round }
|
|
145
|
-
}
|
package/dist/core/condition.ts
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { ENVIRONMENT, type Actor } from './actor.ts'
|
|
2
|
-
|
|
3
|
-
export const COMPARE_OPS = ['eq', 'neq', 'lt', 'lte', 'gt', 'gte', 'in', 'has'] as const
|
|
4
|
-
export type CompareOp = (typeof COMPARE_OPS)[number]
|
|
5
|
-
|
|
6
|
-
export const AGGREGATIONS = ['count', 'sum', 'min', 'max', 'avg'] as const
|
|
7
|
-
|
|
8
|
-
export const ACTOR_FACTS = ['id', 'roles', 'entitlements', ...ENVIRONMENT.map(([fact]) => fact)] as const satisfies readonly (keyof Actor)[]
|
|
9
|
-
|
|
10
|
-
export const AGO = /^[1-9][0-9]*[smhd]$/
|
|
11
|
-
|
|
12
|
-
export type Operand =
|
|
13
|
-
| { readonly value: string | number | boolean | null }
|
|
14
|
-
| { readonly field: string }
|
|
15
|
-
| { readonly was: string }
|
|
16
|
-
| { readonly outer: string }
|
|
17
|
-
| { readonly actor: (typeof ACTOR_FACTS)[number] }
|
|
18
|
-
| { readonly now: true }
|
|
19
|
-
| { readonly ago: string }
|
|
20
|
-
| {
|
|
21
|
-
readonly agg: {
|
|
22
|
-
readonly of: string
|
|
23
|
-
readonly how: (typeof AGGREGATIONS)[number]
|
|
24
|
-
readonly field?: string
|
|
25
|
-
readonly where?: Condition
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type Condition =
|
|
30
|
-
| { readonly all: readonly Condition[] }
|
|
31
|
-
| { readonly any: readonly Condition[] }
|
|
32
|
-
| { readonly not: Condition }
|
|
33
|
-
| { readonly is: Operand; readonly op: CompareOp; readonly than: Operand }
|
|
34
|
-
|
|
35
|
-
export function walkOperands(condition: Condition, take: (one: Operand) => void): void {
|
|
36
|
-
if ('all' in condition) {
|
|
37
|
-
for (const one of condition.all) walkOperands(one, take)
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
if ('any' in condition) {
|
|
41
|
-
for (const one of condition.any) walkOperands(one, take)
|
|
42
|
-
return
|
|
43
|
-
}
|
|
44
|
-
if ('not' in condition) {
|
|
45
|
-
walkOperands(condition.not, take)
|
|
46
|
-
return
|
|
47
|
-
}
|
|
48
|
-
for (const side of [condition.is, condition.than]) {
|
|
49
|
-
take(side)
|
|
50
|
-
if ('agg' in side && side.agg.where !== undefined) walkOperands(side.agg.where, take)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export function mentionsActor(condition: Condition): boolean {
|
|
55
|
-
let found = false
|
|
56
|
-
walkOperands(condition, (one) => {
|
|
57
|
-
if ('actor' in one) found = true
|
|
58
|
-
})
|
|
59
|
-
return found
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function agoSeconds(ago: string): number | undefined {
|
|
63
|
-
if (!AGO.test(ago)) return undefined
|
|
64
|
-
const size = Number(ago.slice(0, -1))
|
|
65
|
-
const unit = ago.slice(-1)
|
|
66
|
-
return size * (unit === 's' ? 1 : unit === 'm' ? 60 : unit === 'h' ? 3_600 : 86_400)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function satisfies(condition: Condition, actor: Actor): boolean {
|
|
70
|
-
if ('all' in condition) return condition.all.every((one) => satisfies(one, actor))
|
|
71
|
-
if ('any' in condition) return condition.any.some((one) => satisfies(one, actor))
|
|
72
|
-
if ('not' in condition) return !satisfies(condition.not, actor)
|
|
73
|
-
|
|
74
|
-
const reads = (one: Operand): unknown => {
|
|
75
|
-
if ('value' in one) return one.value
|
|
76
|
-
if ('actor' in one) return actor[one.actor]
|
|
77
|
-
return undefined
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const left = reads(condition.is)
|
|
81
|
-
const right = reads(condition.than)
|
|
82
|
-
if (left === undefined || right === undefined) return false
|
|
83
|
-
|
|
84
|
-
const op = condition.op
|
|
85
|
-
if (op === 'in') return Array.isArray(right) && right.includes(left)
|
|
86
|
-
if (op === 'has') return Array.isArray(left) && left.includes(right)
|
|
87
|
-
if (op === 'eq') return left === right
|
|
88
|
-
if (op === 'neq') return left !== right
|
|
89
|
-
if (typeof left !== typeof right) return false
|
|
90
|
-
if (typeof left !== 'number' && typeof left !== 'string') return false
|
|
91
|
-
const other = right as number | string
|
|
92
|
-
if (op === 'lt') return left < other
|
|
93
|
-
if (op === 'lte') return left <= other
|
|
94
|
-
if (op === 'gt') return left > other
|
|
95
|
-
return left >= other
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
type Asked = {
|
|
99
|
-
readonly id: string
|
|
100
|
-
readonly fields: readonly string[]
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export const CONDITION_ANSWERS = ['is', 'than', 'field', 'value', 'actor', 'ago', 'of', 'how', 'over', 'op', 'when', 'joins'] as const
|
|
104
|
-
|
|
105
|
-
const OPERAND_WORDS = ['field', 'value', 'actor', 'now', 'ago', 'agg'] as const
|
|
106
|
-
|
|
107
|
-
async function sided(
|
|
108
|
-
ask: (key: string, question: string) => Promise<string>,
|
|
109
|
-
say: (line: string) => void,
|
|
110
|
-
subject: Asked,
|
|
111
|
-
all: readonly Asked[],
|
|
112
|
-
which: string,
|
|
113
|
-
): Promise<Operand> {
|
|
114
|
-
for (;;) {
|
|
115
|
-
const kind = await ask(which, ` ${which} — ${OPERAND_WORDS.join(', ')}: `)
|
|
116
|
-
if (kind === 'field') {
|
|
117
|
-
const id = await ask('field', ` which field — ${subject.fields.join(', ')}: `)
|
|
118
|
-
if (!subject.fields.includes(id)) {
|
|
119
|
-
say(` "${subject.id}" declares no \`${id}\`.\n`)
|
|
120
|
-
continue
|
|
121
|
-
}
|
|
122
|
-
return { field: id }
|
|
123
|
-
}
|
|
124
|
-
if (kind === 'value') {
|
|
125
|
-
const raw = await ask('value', ' the value (text, a number, true, false, null): ')
|
|
126
|
-
if (raw === 'null') return { value: null }
|
|
127
|
-
if (raw === 'true' || raw === 'false') return { value: raw === 'true' }
|
|
128
|
-
const asNumber = Number(raw)
|
|
129
|
-
return { value: raw !== '' && Number.isFinite(asNumber) ? asNumber : raw }
|
|
130
|
-
}
|
|
131
|
-
if (kind === 'actor') {
|
|
132
|
-
const fact = await ask('actor', ` which — ${ACTOR_FACTS.join(', ')}: `)
|
|
133
|
-
if (!(ACTOR_FACTS as readonly string[]).includes(fact)) {
|
|
134
|
-
say(` an actor has ${ACTOR_FACTS.join(', ')} and nothing else.\n`)
|
|
135
|
-
continue
|
|
136
|
-
}
|
|
137
|
-
return { actor: fact as (typeof ACTOR_FACTS)[number] }
|
|
138
|
-
}
|
|
139
|
-
if (kind === 'now') return { now: true }
|
|
140
|
-
if (kind === 'ago') {
|
|
141
|
-
const span = await ask('ago', ' how long — e.g. 30d, 12h, 15m, 45s: ')
|
|
142
|
-
if (!AGO.test(span)) {
|
|
143
|
-
say(' a span is a number and one of s, m, h, d.\n')
|
|
144
|
-
continue
|
|
145
|
-
}
|
|
146
|
-
return { ago: span }
|
|
147
|
-
}
|
|
148
|
-
if (kind === 'agg') {
|
|
149
|
-
const of = await ask('of', ` over which entity — ${all.map((one) => one.id).join(', ')}: `)
|
|
150
|
-
const target = all.find((one) => one.id === of)
|
|
151
|
-
if (target === undefined) {
|
|
152
|
-
say(` nothing declares \`${of}\`.\n`)
|
|
153
|
-
continue
|
|
154
|
-
}
|
|
155
|
-
const how = await ask('how', ` how — ${AGGREGATIONS.join(', ')}: `)
|
|
156
|
-
if (!(AGGREGATIONS as readonly string[]).includes(how)) {
|
|
157
|
-
say(` an aggregation is ${AGGREGATIONS.join(', ')}.\n`)
|
|
158
|
-
continue
|
|
159
|
-
}
|
|
160
|
-
if (how === 'count') return { agg: { of, how: 'count' } }
|
|
161
|
-
const over = await ask('over', ` over which field of ${of} — ${target.fields.join(', ')}: `)
|
|
162
|
-
if (!target.fields.includes(over)) {
|
|
163
|
-
say(` "${of}" declares no \`${over}\`.\n`)
|
|
164
|
-
continue
|
|
165
|
-
}
|
|
166
|
-
return { agg: { of, how: how as 'sum' | 'min' | 'max' | 'avg', field: over } }
|
|
167
|
-
}
|
|
168
|
-
say(` there is no side called "${kind}".\n`)
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
async function compared(
|
|
173
|
-
ask: (key: string, question: string) => Promise<string>,
|
|
174
|
-
say: (line: string) => void,
|
|
175
|
-
subject: Asked,
|
|
176
|
-
all: readonly Asked[],
|
|
177
|
-
): Promise<Condition> {
|
|
178
|
-
for (;;) {
|
|
179
|
-
const is = await sided(ask, say, subject, all, 'is')
|
|
180
|
-
const op = await ask('op', ` op — ${COMPARE_OPS.join(', ')}: `)
|
|
181
|
-
if (!(COMPARE_OPS as readonly string[]).includes(op)) {
|
|
182
|
-
say(` \`${op}\` is not a comparison this provider carries.\n`)
|
|
183
|
-
continue
|
|
184
|
-
}
|
|
185
|
-
const than = await sided(ask, say, subject, all, 'than')
|
|
186
|
-
return { is, op: op as (typeof COMPARE_OPS)[number], than }
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export async function conditioned(
|
|
191
|
-
ask: (key: string, question: string) => Promise<string>,
|
|
192
|
-
say: (line: string) => void,
|
|
193
|
-
subject: { readonly id: string; readonly fields: readonly string[] },
|
|
194
|
-
all: readonly { readonly id: string; readonly fields: readonly string[] }[],
|
|
195
|
-
): Promise<Condition | undefined> {
|
|
196
|
-
const held: Condition[] = []
|
|
197
|
-
let joiner: 'all' | 'any' = 'all'
|
|
198
|
-
for (;;) {
|
|
199
|
-
if (held.length === 0) {
|
|
200
|
-
const wanted = await ask('when', ' state a condition — y/n: ')
|
|
201
|
-
if (!wanted.toLowerCase().startsWith('y')) return undefined
|
|
202
|
-
}
|
|
203
|
-
held.push(await compared(ask, say, subject, all))
|
|
204
|
-
const next = await ask('joins', ' and, or, done: ')
|
|
205
|
-
if (next === 'done' || next === '') break
|
|
206
|
-
if (next === 'and') joiner = 'all'
|
|
207
|
-
else if (next === 'or') joiner = 'any'
|
|
208
|
-
else {
|
|
209
|
-
say(' say and, or, or done.\n')
|
|
210
|
-
break
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
if (held.length === 0) return undefined
|
|
214
|
-
const first = held[0] as Condition
|
|
215
|
-
return held.length === 1 ? first : joiner === 'all' ? { all: held } : { any: held }
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
import { quoted } from './rows.ts'
|
|
219
|
-
|
|
220
|
-
export function said(one: unknown): string {
|
|
221
|
-
if (typeof one === 'string') return quoted(one)
|
|
222
|
-
if (one === null) return 'null'
|
|
223
|
-
if (typeof one === 'boolean' || typeof one === 'number') return String(one)
|
|
224
|
-
if (Array.isArray(one)) return `[${one.map((each) => said(each)).join(', ')}]`
|
|
225
|
-
if (typeof one === 'object') return `{ ${Object.entries(one).map(([key, held]) => `${key}: ${said(held)}`).join(', ')} }`
|
|
226
|
-
return String(one)
|
|
227
|
-
}
|