@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.
Files changed (148) hide show
  1. package/CHANGELOG.md +193 -0
  2. package/dist/core/attach.js +21 -0
  3. package/dist/core/descend.d.ts +2 -0
  4. package/dist/core/descend.js +18 -3
  5. package/dist/core/generators/commit.js +55 -3
  6. package/dist/core/generators/dev.d.ts +1 -0
  7. package/dist/core/generators/dev.js +68 -0
  8. package/dist/core/rows.d.ts +5 -0
  9. package/dist/core/rows.js +45 -0
  10. package/dist/core/tree.d.ts +1 -0
  11. package/dist/core/tree.js +7 -1
  12. package/dist/derivation/generators/build.d.ts +3 -1
  13. package/dist/derivation/generators/build.js +103 -22
  14. package/dist/derivation/generators/release.js +74 -26
  15. package/dist/derivation/release.js +3 -3
  16. package/dist/integrations/cli.d.ts +3 -9
  17. package/dist/integrations/cli.js +96 -36
  18. package/dist/integrations/declared.d.ts +8 -0
  19. package/dist/integrations/declared.js +22 -0
  20. package/dist/integrations/pglite.js +13 -3
  21. package/dist/integrations/target.d.ts +4 -1
  22. package/dist/integrations/target.js +20 -6
  23. package/dist/integrity/attach.js +1 -5
  24. package/dist/integrity/generators/publish.js +2 -1
  25. package/dist/integrity/slopnators/sl_stale_path.js +10 -0
  26. package/dist/integrity/slopnators/sl_unlisted_program.js +2 -2
  27. package/dist/operations/access.js +3 -2
  28. package/dist/operations/capabilities.d.ts +2 -0
  29. package/dist/operations/capabilities.js +2 -0
  30. package/dist/operations/generators/capability.js +234 -24
  31. package/dist/operations/generators/compile.d.ts +21 -0
  32. package/dist/operations/generators/compile.js +151 -51
  33. package/dist/operations/generators/permits.d.ts +2 -1
  34. package/dist/operations/generators/permits.js +52 -47
  35. package/dist/operations/generators/seed.d.ts +3 -0
  36. package/dist/operations/generators/seed.js +25 -3
  37. package/dist/operations/nodes.d.ts +2 -2
  38. package/package.json +1 -1
  39. package/dist/core/actor.ts +0 -21
  40. package/dist/core/attach.ts +0 -613
  41. package/dist/core/chain.ts +0 -145
  42. package/dist/core/condition.ts +0 -227
  43. package/dist/core/descend.ts +0 -1173
  44. package/dist/core/errors/catalogue.ts +0 -314
  45. package/dist/core/errors/classes.ts +0 -51
  46. package/dist/core/errors/error.ts +0 -152
  47. package/dist/core/errors/namespaces.ts +0 -19
  48. package/dist/core/finding.ts +0 -37
  49. package/dist/core/generators/commit.ts +0 -157
  50. package/dist/core/init.ts +0 -222
  51. package/dist/core/invoked.ts +0 -52
  52. package/dist/core/round.ts +0 -74
  53. package/dist/core/rows.ts +0 -459
  54. package/dist/core/scope.ts +0 -20
  55. package/dist/core/thread.ts +0 -29
  56. package/dist/core/tree.ts +0 -90
  57. package/dist/core/types.ts +0 -351
  58. package/dist/derivation/attach.ts +0 -57
  59. package/dist/derivation/generators/build.ts +0 -371
  60. package/dist/derivation/generators/check.ts +0 -118
  61. package/dist/derivation/generators/determinism.ts +0 -145
  62. package/dist/derivation/generators/release.ts +0 -236
  63. package/dist/derivation/release.ts +0 -43
  64. package/dist/integrations/attach.ts +0 -187
  65. package/dist/integrations/cli.ts +0 -615
  66. package/dist/integrations/disk.ts +0 -466
  67. package/dist/integrations/generators/read.ts +0 -127
  68. package/dist/integrations/generators/vault.ts +0 -210
  69. package/dist/integrations/https.ts +0 -216
  70. package/dist/integrations/pglite.ts +0 -206
  71. package/dist/integrations/secret.ts +0 -54
  72. package/dist/integrations/target.ts +0 -656
  73. package/dist/integrations/vault.ts +0 -148
  74. package/dist/integrity/attach.ts +0 -150
  75. package/dist/integrity/check-threads.ts +0 -166
  76. package/dist/integrity/constraint.ts +0 -1246
  77. package/dist/integrity/generators/compiler.ts +0 -138
  78. package/dist/integrity/generators/gate.ts +0 -227
  79. package/dist/integrity/generators/issues.ts +0 -160
  80. package/dist/integrity/generators/publish.ts +0 -232
  81. package/dist/integrity/generators/trial.ts +0 -359
  82. package/dist/integrity/lint.ts +0 -214
  83. package/dist/integrity/lsp/bin.ts +0 -44
  84. package/dist/integrity/lsp/diagnostics.ts +0 -92
  85. package/dist/integrity/lsp/findings.ts +0 -63
  86. package/dist/integrity/lsp/lint.ts +0 -124
  87. package/dist/integrity/lsp/protocol.ts +0 -78
  88. package/dist/integrity/lsp/server.ts +0 -224
  89. package/dist/integrity/slopnator.ts +0 -324
  90. package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
  91. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
  92. package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
  93. package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
  94. package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
  95. package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
  96. package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
  97. package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
  98. package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
  99. package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
  100. package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
  101. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
  102. package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
  103. package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
  104. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
  105. package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
  106. package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
  107. package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
  108. package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
  109. package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
  110. package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
  111. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
  112. package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
  113. package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
  114. package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
  115. package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
  116. package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
  117. package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
  118. package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
  119. package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
  120. package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
  121. package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
  122. package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
  123. package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
  124. package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
  125. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
  126. package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
  127. package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
  128. package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
  129. package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
  130. package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
  131. package/dist/integrity/sweep.ts +0 -395
  132. package/dist/knowledge/attach.ts +0 -327
  133. package/dist/knowledge/generators/docsgen.ts +0 -186
  134. package/dist/knowledge/generators/glossary.ts +0 -73
  135. package/dist/knowledge/glossary.ts +0 -6
  136. package/dist/operations/access.ts +0 -131
  137. package/dist/operations/attach.ts +0 -408
  138. package/dist/operations/capabilities.ts +0 -88
  139. package/dist/operations/dispatch.ts +0 -294
  140. package/dist/operations/error-results.ts +0 -31
  141. package/dist/operations/generators/capability.ts +0 -1054
  142. package/dist/operations/generators/compile.ts +0 -701
  143. package/dist/operations/generators/permits.ts +0 -224
  144. package/dist/operations/generators/seed.ts +0 -359
  145. package/dist/operations/generators/trial.ts +0 -666
  146. package/dist/operations/nodes.ts +0 -53
  147. package/dist/operations/operations.ts +0 -91
  148. package/dist/operations/result.ts +0 -28
@@ -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
- }
@@ -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
- }