@prestack/thread 0.1.21 → 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 +109 -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 +68 -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.js +69 -10
- 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 +234 -24
- package/dist/operations/generators/compile.js +150 -50
- package/dist/operations/generators/seed.d.ts +3 -0
- package/dist/operations/generators/seed.js +25 -3
- package/dist/operations/nodes.d.ts +2 -2
- package/package.json +1 -1
- package/dist/core/actor.ts +0 -21
- package/dist/core/attach.ts +0 -613
- package/dist/core/chain.ts +0 -145
- package/dist/core/condition.ts +0 -227
- package/dist/core/descend.ts +0 -1185
- package/dist/core/errors/catalogue.ts +0 -314
- package/dist/core/errors/classes.ts +0 -51
- package/dist/core/errors/error.ts +0 -152
- package/dist/core/errors/namespaces.ts +0 -19
- package/dist/core/finding.ts +0 -37
- package/dist/core/generators/commit.ts +0 -157
- package/dist/core/init.ts +0 -222
- package/dist/core/invoked.ts +0 -52
- package/dist/core/round.ts +0 -74
- package/dist/core/rows.ts +0 -506
- package/dist/core/scope.ts +0 -20
- package/dist/core/thread.ts +0 -29
- package/dist/core/tree.ts +0 -90
- package/dist/core/types.ts +0 -351
- package/dist/derivation/attach.ts +0 -57
- package/dist/derivation/generators/build.ts +0 -371
- package/dist/derivation/generators/check.ts +0 -118
- package/dist/derivation/generators/determinism.ts +0 -145
- package/dist/derivation/generators/release.ts +0 -236
- package/dist/derivation/release.ts +0 -43
- package/dist/integrations/attach.ts +0 -187
- package/dist/integrations/cli.ts +0 -606
- package/dist/integrations/declared.ts +0 -34
- package/dist/integrations/disk.ts +0 -466
- package/dist/integrations/generators/read.ts +0 -127
- package/dist/integrations/generators/vault.ts +0 -210
- package/dist/integrations/https.ts +0 -216
- package/dist/integrations/pglite.ts +0 -206
- package/dist/integrations/secret.ts +0 -54
- package/dist/integrations/target.ts +0 -672
- package/dist/integrations/vault.ts +0 -148
- package/dist/integrity/attach.ts +0 -150
- package/dist/integrity/check-threads.ts +0 -166
- package/dist/integrity/constraint.ts +0 -1246
- package/dist/integrity/generators/compiler.ts +0 -138
- package/dist/integrity/generators/gate.ts +0 -227
- package/dist/integrity/generators/issues.ts +0 -160
- package/dist/integrity/generators/publish.ts +0 -233
- package/dist/integrity/generators/trial.ts +0 -359
- package/dist/integrity/lint.ts +0 -214
- package/dist/integrity/lsp/bin.ts +0 -44
- package/dist/integrity/lsp/diagnostics.ts +0 -92
- package/dist/integrity/lsp/findings.ts +0 -63
- package/dist/integrity/lsp/lint.ts +0 -124
- package/dist/integrity/lsp/protocol.ts +0 -78
- package/dist/integrity/lsp/server.ts +0 -224
- package/dist/integrity/slopnator.ts +0 -324
- package/dist/integrity/slopnators/sl_broken_lineage.ts +0 -38
- package/dist/integrity/slopnators/sl_declared_elsewhere.ts +0 -60
- package/dist/integrity/slopnators/sl_derived_drift.ts +0 -101
- package/dist/integrity/slopnators/sl_door_in_a_node.ts +0 -250
- package/dist/integrity/slopnators/sl_hardcoded_style.ts +0 -55
- package/dist/integrity/slopnators/sl_inert_island.ts +0 -84
- package/dist/integrity/slopnators/sl_inert_slopnator.ts +0 -93
- package/dist/integrity/slopnators/sl_missing_row.ts +0 -140
- package/dist/integrity/slopnators/sl_no_barrel.ts +0 -48
- package/dist/integrity/slopnators/sl_no_child_process.ts +0 -100
- package/dist/integrity/slopnators/sl_no_comment.ts +0 -43
- package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +0 -276
- package/dist/integrity/slopnators/sl_no_import_cycle.ts +0 -66
- package/dist/integrity/slopnators/sl_no_such_command.ts +0 -81
- package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +0 -63
- package/dist/integrity/slopnators/sl_outside_an_operation.ts +0 -25
- package/dist/integrity/slopnators/sl_screen_holds_only_declared.ts +0 -70
- package/dist/integrity/slopnators/sl_secret_literal.ts +0 -68
- package/dist/integrity/slopnators/sl_stale_path.ts +0 -178
- package/dist/integrity/slopnators/sl_twice_meant.ts +0 -57
- package/dist/integrity/slopnators/sl_twice_parented.ts +0 -82
- package/dist/integrity/slopnators/sl_undeclared_crossing.ts +0 -481
- package/dist/integrity/slopnators/sl_undeclared_destination.ts +0 -113
- package/dist/integrity/slopnators/sl_undeclared_element.ts +0 -44
- package/dist/integrity/slopnators/sl_undeclared_list.ts +0 -358
- package/dist/integrity/slopnators/sl_undeclared_node.ts +0 -75
- package/dist/integrity/slopnators/sl_undeclared_operation.ts +0 -257
- package/dist/integrity/slopnators/sl_undeclared_variant.ts +0 -152
- package/dist/integrity/slopnators/sl_undeclared_webhook.ts +0 -108
- package/dist/integrity/slopnators/sl_underived_route.ts +0 -72
- package/dist/integrity/slopnators/sl_undescribed.ts +0 -65
- package/dist/integrity/slopnators/sl_ungoverned_island.ts +0 -60
- package/dist/integrity/slopnators/sl_unlisted_program.ts +0 -46
- package/dist/integrity/slopnators/sl_unnamed_island.ts +0 -41
- package/dist/integrity/slopnators/sl_unpaired_member.ts +0 -139
- package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +0 -71
- package/dist/integrity/slopnators/sl_unprefixed_member.ts +0 -110
- package/dist/integrity/slopnators/sl_unthreaded.ts +0 -421
- package/dist/integrity/slopnators/sl_untranslated_key.ts +0 -140
- package/dist/integrity/slopnators/sl_unvaulted_secret.ts +0 -132
- package/dist/integrity/slopnators/sl_unversioned.ts +0 -52
- package/dist/integrity/sweep.ts +0 -395
- package/dist/knowledge/attach.ts +0 -327
- package/dist/knowledge/generators/docsgen.ts +0 -186
- package/dist/knowledge/generators/glossary.ts +0 -73
- package/dist/knowledge/glossary.ts +0 -6
- package/dist/operations/access.ts +0 -131
- package/dist/operations/attach.ts +0 -408
- package/dist/operations/capabilities.ts +0 -88
- package/dist/operations/dispatch.ts +0 -294
- package/dist/operations/error-results.ts +0 -31
- package/dist/operations/generators/capability.ts +0 -1054
- package/dist/operations/generators/compile.ts +0 -701
- package/dist/operations/generators/permits.ts +0 -229
- package/dist/operations/generators/seed.ts +0 -359
- package/dist/operations/generators/trial.ts +0 -666
- package/dist/operations/nodes.ts +0 -53
- package/dist/operations/operations.ts +0 -91
- package/dist/operations/result.ts +0 -28
|
@@ -1,672 +0,0 @@
|
|
|
1
|
-
import { join } from 'node:path'
|
|
2
|
-
import { execPath } from 'node:process'
|
|
3
|
-
|
|
4
|
-
import type { Member } from '../core/thread.ts'
|
|
5
|
-
import type { Actor } from '../core/actor.ts'
|
|
6
|
-
|
|
7
|
-
import { SECRECY, secrecyOf, type Secret } from './secret.ts'
|
|
8
|
-
|
|
9
|
-
export const REACH = {
|
|
10
|
-
postgres: 'pg_',
|
|
11
|
-
pglite: 'pl_',
|
|
12
|
-
browser: 'zu_',
|
|
13
|
-
https: 'ht_',
|
|
14
|
-
cli: 'cli_',
|
|
15
|
-
disk: 'dsk_',
|
|
16
|
-
sdk: 'sdk_',
|
|
17
|
-
} as const satisfies Readonly<Record<string, string>>
|
|
18
|
-
|
|
19
|
-
export type Reach = keyof typeof REACH
|
|
20
|
-
|
|
21
|
-
export const ADDRESS = {
|
|
22
|
-
row: 'reached at the address written on its row, which anybody may read',
|
|
23
|
-
secret: 'reached at a secret, because its address is a connection string and a connection string carries a password',
|
|
24
|
-
nothing: 'addressed by nothing',
|
|
25
|
-
} as const satisfies Readonly<Record<string, string>>
|
|
26
|
-
|
|
27
|
-
export type Addressed = keyof typeof ADDRESS
|
|
28
|
-
|
|
29
|
-
export interface Holds {
|
|
30
|
-
|
|
31
|
-
readonly takes: 'entities' | 'calls' | null
|
|
32
|
-
|
|
33
|
-
readonly from: 'server' | 'browser' | 'either'
|
|
34
|
-
|
|
35
|
-
readonly addressed: Addressed
|
|
36
|
-
|
|
37
|
-
readonly migrations: boolean
|
|
38
|
-
|
|
39
|
-
readonly references: boolean
|
|
40
|
-
|
|
41
|
-
readonly sql: boolean
|
|
42
|
-
|
|
43
|
-
readonly driver: string | null
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export const HOLDS: Readonly<Record<Reach, Holds>> = {
|
|
47
|
-
postgres: { takes: 'entities', from: 'server', addressed: 'secret', migrations: true, references: true, sql: true, driver: 'postgres' },
|
|
48
|
-
pglite: { takes: 'entities', from: 'either', addressed: 'nothing', migrations: true, references: true, sql: true, driver: '@electric-sql/pglite' },
|
|
49
|
-
browser: { takes: 'entities', from: 'browser', addressed: 'nothing', migrations: false, references: false, sql: false, driver: 'zustand' },
|
|
50
|
-
https: { takes: 'calls', from: 'either', addressed: 'row', migrations: false, references: false, sql: false, driver: null },
|
|
51
|
-
cli: { takes: 'calls', from: 'server', addressed: 'nothing', migrations: false, references: false, sql: false, driver: null },
|
|
52
|
-
disk: { takes: null, from: 'server', addressed: 'nothing', migrations: false, references: false, sql: false, driver: null },
|
|
53
|
-
sdk: { takes: 'calls', from: 'server', addressed: 'nothing', migrations: false, references: false, sql: false, driver: null },
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export const SAID: Readonly<Record<Reach, string>> = {
|
|
57
|
-
postgres:
|
|
58
|
-
'A postgres this app connects to over the network. It keeps entities and holds everything a declaration can state about their shape — tables, references and invariants — so what is kept here is held to that shape by the database itself rather than by whatever remembered to check. Who may touch a row is not asked of it: that is answered by the app, against the caller who actually arrived. It is reached from a server only: a connection to it from a page is a database somebody else can open. This is where a thing goes when losing it would matter.',
|
|
59
|
-
pglite:
|
|
60
|
-
'A postgres running inside the process that opened it. It keeps entities and holds tables, references and invariants, exactly as the networked one does — what differs is who can reach it, not what it can be told to hold. It is addressed by nothing: its files sit under .thread beside the tree, in a folder named after the target, so nobody types a path and a fresh clone starts it empty. What belongs here is what the caller is already entitled to see: a local copy, a working set, an offline cache.',
|
|
61
|
-
browser:
|
|
62
|
-
'A store in the browser, held in memory the viewer owns. It keeps entities and it is addressed by nothing, because it is already where the code runs. It holds no migration, because a page load builds it from nothing every time. What belongs here is what the interface needs to be responsive and can afford to lose.',
|
|
63
|
-
https:
|
|
64
|
-
'Something this app calls over https — a vendor, a service, another app. It accepts calls rather than entities: each is declared with the verb and the path it takes, so a node names the call and never writes a url. Where it is goes on its row as address, and a header every call carries goes on its row as headers, because both are facts anybody may read; a credential it sends is a secret, and requiring one is what keeps the target out of a browser.',
|
|
65
|
-
cli:
|
|
66
|
-
'A program this app runs as a command. git is the one that made this necessary. It is not installed, because it is not a dependency: it is on the machine or it is not, and a run finds out. What it accepts is the words it is asked with, declared beside it as calls with thread create target call: a subcommand where the program has them, and the option that selects what it does where it has none, each spelled the way the program spells it. A word that is not declared never runs. The program is the one the target is named after, so declaring one runs nothing and reads nothing off the machine. An operation that requires it is handed one function per declared word, and that is the only way this app ever runs it.',
|
|
67
|
-
disk:
|
|
68
|
-
'The disk this app runs on. It answers a fixed set of orders — is this there, read it, write it, list what is beside it, how big it is, remove it, and what it really is — and those orders are the same for every disk, because they are what a filesystem does rather than what any one of them chose to offer. None of them runs anything: a disk holds files and a program that installs packages is not a file order, which is why installing what a tree depends on happens where somebody typed the command and is not something a disk can be asked for. It is reached from a server only: a page has no filesystem. A trial stands one of these up out of its own scenario, so a rule can be measured against files that exist for the length of that run and nothing is written where you work.',
|
|
69
|
-
sdk:
|
|
70
|
-
'A package this app calls inside its own process — a vendor\'s SDK, fetched from npm, a git link or a tarball. It accepts calls rather than entities: the file this target declares exports `calls`, each call names the function there it runs as its path, and that file is the only place the package is imported, so an operation names the call and never the package. The integration it is part of names what to fetch in needs, and declaring the integration fetches it. It runs where the app runs, so it is addressed by nothing and reached from a server only.',
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const on = (host: string, names: readonly string[]): readonly string[] => names.map((one) => host + '.' + one)
|
|
74
|
-
|
|
75
|
-
const driverOfReach = (one: Reach): readonly string[] => {
|
|
76
|
-
const driver = HOLDS[one].driver
|
|
77
|
-
return driver === null ? [] : ['import ' + driver]
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export const OPENS: Readonly<Record<Reach, readonly string[]>> = {
|
|
81
|
-
postgres: driverOfReach('postgres'),
|
|
82
|
-
pglite: driverOfReach('pglite'),
|
|
83
|
-
browser: driverOfReach('browser'),
|
|
84
|
-
https: [
|
|
85
|
-
'fetch',
|
|
86
|
-
'XMLHttpRequest',
|
|
87
|
-
'WebSocket',
|
|
88
|
-
'EventSource',
|
|
89
|
-
...on('window', ['fetch']),
|
|
90
|
-
...on('navigator', ['sendBeacon']),
|
|
91
|
-
'import node:http',
|
|
92
|
-
'import node:https',
|
|
93
|
-
'import node:http2',
|
|
94
|
-
'import node:net',
|
|
95
|
-
'import node:tls',
|
|
96
|
-
'import node:dgram',
|
|
97
|
-
'import node:dns',
|
|
98
|
-
],
|
|
99
|
-
cli: ['import node:child_process', 'import node:worker_threads', 'import node:cluster'],
|
|
100
|
-
disk: ['import node:fs', 'import node:fs/promises'],
|
|
101
|
-
sdk: [],
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export type Kind = 'file' | 'folder' | 'other' | 'none'
|
|
105
|
-
|
|
106
|
-
export interface Listed {
|
|
107
|
-
readonly name: string
|
|
108
|
-
|
|
109
|
-
readonly kind: Exclude<Kind, 'none'>
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface Measured {
|
|
113
|
-
readonly size: number
|
|
114
|
-
|
|
115
|
-
readonly modified: number
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export interface Answering {
|
|
119
|
-
readonly kind: (at: string) => Kind
|
|
120
|
-
readonly read: (at: string) => string
|
|
121
|
-
readonly write: (at: string, said: string, mode?: number) => void
|
|
122
|
-
readonly list: (at: string) => readonly Listed[]
|
|
123
|
-
readonly stat: (at: string) => Measured
|
|
124
|
-
readonly remove: (at: string) => void
|
|
125
|
-
readonly real: (at: string) => string
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
export interface Attached extends Answering {
|
|
129
|
-
|
|
130
|
-
readonly fetch: (at: string, words?: readonly string[]) => string
|
|
131
|
-
|
|
132
|
-
readonly run: (at: string, words: readonly string[]) => Ran
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export const ORDERS = {
|
|
136
|
-
kind: 'kind',
|
|
137
|
-
read: 'read',
|
|
138
|
-
write: 'written',
|
|
139
|
-
list: 'list',
|
|
140
|
-
stat: 'stat',
|
|
141
|
-
remove: 'removed',
|
|
142
|
-
real: 'real',
|
|
143
|
-
fetch: 'fetched',
|
|
144
|
-
run: 'ran',
|
|
145
|
-
} as const
|
|
146
|
-
|
|
147
|
-
export interface Order {
|
|
148
|
-
readonly order: keyof typeof ORDERS
|
|
149
|
-
readonly at: string
|
|
150
|
-
readonly said?: string
|
|
151
|
-
readonly mode?: number
|
|
152
|
-
readonly words?: readonly string[]
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface Answer {
|
|
156
|
-
readonly answered: string
|
|
157
|
-
readonly found?: unknown
|
|
158
|
-
readonly said?: string
|
|
159
|
-
readonly at?: string
|
|
160
|
-
readonly because?: string
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export interface Recorded {
|
|
164
|
-
readonly order: Order
|
|
165
|
-
readonly answered: Answer
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export interface How {
|
|
169
|
-
readonly at?: string
|
|
170
|
-
readonly hears?: string
|
|
171
|
-
readonly of?: string
|
|
172
|
-
readonly answered?: readonly Answer[]
|
|
173
|
-
readonly recording?: readonly Recorded[]
|
|
174
|
-
readonly answering?: number
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export interface Ran {
|
|
178
|
-
|
|
179
|
-
readonly there: boolean
|
|
180
|
-
|
|
181
|
-
readonly ok: boolean
|
|
182
|
-
|
|
183
|
-
readonly code: number
|
|
184
|
-
|
|
185
|
-
readonly out: string
|
|
186
|
-
|
|
187
|
-
readonly err: string
|
|
188
|
-
|
|
189
|
-
readonly killed: string | null
|
|
190
|
-
|
|
191
|
-
readonly why: string
|
|
192
|
-
|
|
193
|
-
readonly of?: string
|
|
194
|
-
|
|
195
|
-
readonly asks?: readonly Order[]
|
|
196
|
-
|
|
197
|
-
readonly lost?: string
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
export type Running<C extends string = string> = Readonly<
|
|
201
|
-
Record<
|
|
202
|
-
C,
|
|
203
|
-
(
|
|
204
|
-
args: readonly string[],
|
|
205
|
-
shows?: boolean | 'errors' | 'tee',
|
|
206
|
-
within?: number,
|
|
207
|
-
how?: How,
|
|
208
|
-
) => Promise<Ran>
|
|
209
|
-
>
|
|
210
|
-
>
|
|
211
|
-
|
|
212
|
-
export type Calling<C extends string = string> = { readonly [K in C]: (given?: unknown) => Promise<unknown> }
|
|
213
|
-
|
|
214
|
-
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
215
|
-
|
|
216
|
-
export interface HttpRequest {
|
|
217
|
-
readonly target: string
|
|
218
|
-
|
|
219
|
-
readonly address: string
|
|
220
|
-
readonly method: HttpMethod
|
|
221
|
-
|
|
222
|
-
readonly path: string
|
|
223
|
-
|
|
224
|
-
readonly headers?: Readonly<Record<string, string>>
|
|
225
|
-
|
|
226
|
-
readonly body?: Uint8Array
|
|
227
|
-
readonly contentType?: string
|
|
228
|
-
|
|
229
|
-
readonly idempotencyKey?: string
|
|
230
|
-
|
|
231
|
-
readonly told?: (said: unknown) => void
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export interface HttpResponse {
|
|
235
|
-
readonly status: number
|
|
236
|
-
readonly headers: Readonly<Record<string, string>>
|
|
237
|
-
readonly body: Uint8Array
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export interface Http {
|
|
241
|
-
call(request: HttpRequest, within?: number): Promise<HttpResponse>
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export type Row = Record<string, unknown>
|
|
245
|
-
|
|
246
|
-
export type PropertyValue = string | number | boolean | null | readonly (string | number | boolean)[]
|
|
247
|
-
|
|
248
|
-
export function isRow(v: unknown): v is Row {
|
|
249
|
-
return typeof v === 'object' && v !== null && typeof (v as { id?: unknown }).id === 'string'
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export type ReadField = `fld_${string}` | `rel_${string}` | (string & {})
|
|
253
|
-
|
|
254
|
-
export type FieldList = readonly [ReadField, ...ReadField[]]
|
|
255
|
-
|
|
256
|
-
export interface Store {
|
|
257
|
-
insert(actor: Actor, entity: string, row: Row): Promise<Row>
|
|
258
|
-
|
|
259
|
-
get(actor: Actor, entity: string, key: string | number, fields: FieldList): Promise<Row | null>
|
|
260
|
-
|
|
261
|
-
list(
|
|
262
|
-
actor: Actor,
|
|
263
|
-
entity: string,
|
|
264
|
-
opts?: { limit?: number; where?: Readonly<Record<string, string | number | boolean | null>> },
|
|
265
|
-
): Promise<Row[]>
|
|
266
|
-
|
|
267
|
-
update(
|
|
268
|
-
actor: Actor,
|
|
269
|
-
entity: string,
|
|
270
|
-
key: string | number,
|
|
271
|
-
patch: Record<string, unknown>,
|
|
272
|
-
expectedVersion?: number,
|
|
273
|
-
): Promise<Row>
|
|
274
|
-
|
|
275
|
-
remove(actor: Actor, entity: string, key: string | number): Promise<void>
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export interface Transactional {
|
|
279
|
-
transaction<T>(actor: Actor, fn: () => Promise<T>): Promise<T>
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
export function isTransactional(store: Store): store is Store & Transactional {
|
|
283
|
-
return typeof (store as Partial<Transactional>).transaction === 'function'
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
export interface Integration extends Member {
|
|
287
|
-
|
|
288
|
-
readonly needs?: Readonly<Record<string, string>>
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
export interface Target extends Member {
|
|
292
|
-
|
|
293
|
-
readonly of: string
|
|
294
|
-
|
|
295
|
-
readonly takes: readonly string[]
|
|
296
|
-
|
|
297
|
-
readonly requires: readonly string[]
|
|
298
|
-
|
|
299
|
-
readonly address?: string
|
|
300
|
-
|
|
301
|
-
readonly headers?: Readonly<Record<string, string>>
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
export interface Webhook extends Member {
|
|
305
|
-
|
|
306
|
-
readonly of: string
|
|
307
|
-
|
|
308
|
-
readonly path: string
|
|
309
|
-
|
|
310
|
-
readonly arrives?: string
|
|
311
|
-
|
|
312
|
-
readonly invokes: string
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
export interface Call extends Member {
|
|
316
|
-
|
|
317
|
-
readonly of: string
|
|
318
|
-
|
|
319
|
-
readonly method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
|
|
320
|
-
|
|
321
|
-
readonly path: string
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export const CALL_PREFIX = 'cl_'
|
|
325
|
-
|
|
326
|
-
const FILLED = /\{([a-z][a-z0-9_]*)\}/g
|
|
327
|
-
|
|
328
|
-
export const fills = (one: Call): readonly string[] => [...one.path.matchAll(FILLED)].map((each) => each[1] ?? '')
|
|
329
|
-
|
|
330
|
-
export function reachOf(id: string): Reach | undefined {
|
|
331
|
-
return (Object.keys(REACH) as Reach[]).find((one) => id.startsWith(REACH[one]))
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export const standsHere = (one: Target, targets: readonly Target[]): boolean =>
|
|
335
|
-
!one.requires.some((each) => targets.some((two) => two.id === each))
|
|
336
|
-
|
|
337
|
-
export function holdsOf(id: string): Holds | undefined {
|
|
338
|
-
const what = reachOf(id)
|
|
339
|
-
return what === undefined ? undefined : HOLDS[what]
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
export function requirable(targets: readonly Target[], calls: readonly Call[]): readonly Target[] {
|
|
343
|
-
const declared = new Set(calls.map((one) => one.id))
|
|
344
|
-
return targets.filter((one) => holdsOf(one.id)?.takes !== 'calls' || one.takes.some((each) => declared.has(each)))
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
export function driverOf(id: string): string | null {
|
|
348
|
-
return holdsOf(id)?.driver ?? null
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
export function addressOf(one: Target, secrets: readonly Secret[]): Secret | undefined {
|
|
352
|
-
|
|
353
|
-
if (holdsOf(one.id)?.addressed !== 'secret') return undefined
|
|
354
|
-
const held = new Map(secrets.map((each) => [each.id, each]))
|
|
355
|
-
return one.requires.map((each) => held.get(each)).find((each) => each !== undefined && each.sends === undefined && each.signs === undefined)
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
export function credentialsOf(one: Target, secrets: readonly Secret[]): readonly Secret[] {
|
|
359
|
-
const held = new Map(secrets.map((each) => [each.id, each]))
|
|
360
|
-
return one.requires
|
|
361
|
-
.map((each) => held.get(each))
|
|
362
|
-
.filter((each): each is Secret => each !== undefined && (each.sends !== undefined || each.signs !== undefined))
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
export function servesBrowser(one: Target, secrets: readonly Secret[]): true | string {
|
|
366
|
-
const holds = holdsOf(one.id)
|
|
367
|
-
if (holds === undefined) {
|
|
368
|
-
return `\`${one.id}\` wears none of the prefixes a target is reached by, so nothing can tell where it may be reached from`
|
|
369
|
-
}
|
|
370
|
-
if (holds.from === 'server') {
|
|
371
|
-
return (
|
|
372
|
-
`\`${one.id}\` is reached over ${String(reachOf(one.id))}, and that reach is server-only whatever it is ` +
|
|
373
|
-
'authenticated with — the refusal is about the reach itself and not about any credential'
|
|
374
|
-
)
|
|
375
|
-
}
|
|
376
|
-
const held = new Map(secrets.map((each) => [each.id, each]))
|
|
377
|
-
for (const named of one.requires) {
|
|
378
|
-
const secret = held.get(named)
|
|
379
|
-
if (secret === undefined) continue
|
|
380
|
-
if (secrecyOf(secret.id) === 'server') {
|
|
381
|
-
return (
|
|
382
|
-
`\`${one.id}\` requires \`${secret.id}\`, which wears \`${SECRECY.server}\` — a value only a server may hold. ` +
|
|
383
|
-
'Everything a browser bundle reaches is in the bundle, so this would ship the credential to whoever opens the page'
|
|
384
|
-
)
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
return true
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
export const unreachable = (targets: readonly Target[]): readonly string[] =>
|
|
391
|
-
targets
|
|
392
|
-
.filter((one) => reachOf(one.id) === undefined)
|
|
393
|
-
.map(
|
|
394
|
-
(one) =>
|
|
395
|
-
`\`${one.id}\` wears none of the prefixes a target is reached by (${Object.values(REACH).join(', ')}), so ` +
|
|
396
|
-
'nothing can tell how to reach it — and how a thing is reached decides what may be declared for it',
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
export const LOCKFILES: Readonly<Record<string, string>> = {
|
|
400
|
-
'pnpm-lock.yaml': 'pnpm',
|
|
401
|
-
'yarn.lock': 'yarn',
|
|
402
|
-
'bun.lockb': 'bun',
|
|
403
|
-
'package-lock.json': 'npm',
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
export const PACKAGER = 'packager'
|
|
407
|
-
|
|
408
|
-
const NAMED: readonly string[] = ['git', 'thread']
|
|
409
|
-
|
|
410
|
-
const RUNTIME = 'node'
|
|
411
|
-
|
|
412
|
-
export const managerOf = (at: string, there: (path: string) => boolean): string =>
|
|
413
|
-
Object.entries(LOCKFILES).find(([lock]) => there(join(at, lock)))?.[1] ?? 'npm'
|
|
414
|
-
|
|
415
|
-
export function programOf(
|
|
416
|
-
name: string,
|
|
417
|
-
at: string,
|
|
418
|
-
there: (path: string) => boolean,
|
|
419
|
-
read: (path: string) => string,
|
|
420
|
-
): readonly string[] | undefined {
|
|
421
|
-
if (NAMED.includes(name)) return [name]
|
|
422
|
-
if (name === RUNTIME) return [execPath]
|
|
423
|
-
if (name === PACKAGER) return [managerOf(at, there)]
|
|
424
|
-
const installed = join(at, 'node_modules', '.bin', name)
|
|
425
|
-
if (there(installed)) return [installed]
|
|
426
|
-
const shipped = join(at, 'node_modules', name)
|
|
427
|
-
const manifest = join(shipped, 'package.json')
|
|
428
|
-
if (!there(manifest)) return undefined
|
|
429
|
-
let bin: unknown
|
|
430
|
-
try {
|
|
431
|
-
bin = (JSON.parse(read(manifest)) as { readonly bin?: unknown }).bin
|
|
432
|
-
} catch {
|
|
433
|
-
return undefined
|
|
434
|
-
}
|
|
435
|
-
const entry = typeof bin === 'string' ? bin : typeof bin === 'object' && bin !== null ? (bin as Readonly<Record<string, unknown>>)[name] : undefined
|
|
436
|
-
return typeof entry === 'string' ? [execPath, join(shipped, entry)] : undefined
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
export const unlisted = (name: string): string =>
|
|
440
|
-
'`' + REACH.cli + name + '` names ' + name + ', and nothing lists a program by that name: it is not linked under node_modules/.bin, and no package installed here under that name names it as its program, so this tree did not install it; it is not git, node or thread; and it is not `' + PACKAGER + '`, the word for whichever package manager the lockfile picks. A program the tree did not install is whatever the machine happens to have, and a target on that is a shell by another name. Add the package that provides it and declare the target again, or ask for it through a declared call on `' + REACH.cli + PACKAGER + '`'
|
|
441
|
-
|
|
442
|
-
const SHAPED: Readonly<Record<'cli', RegExp>> = {
|
|
443
|
-
cli: /^-{0,2}[A-Za-z][A-Za-z0-9-]*$/,
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
const EVERY_VALUE = new Set(['constructor', '__proto__', 'prototype'])
|
|
447
|
-
|
|
448
|
-
export function misnamed(target: string, name: string): string | undefined {
|
|
449
|
-
const reach = reachOf(target)
|
|
450
|
-
if (reach !== 'cli') return undefined
|
|
451
|
-
if (EVERY_VALUE.has(name)) {
|
|
452
|
-
return (
|
|
453
|
-
`\`${name}\` is a step every JavaScript value has rather than something \`${target}\` answers to, and walking to ` +
|
|
454
|
-
'it leaves what was declared'
|
|
455
|
-
)
|
|
456
|
-
}
|
|
457
|
-
if (!SHAPED[reach].test(name)) {
|
|
458
|
-
return name + ' is not one word as the program spells it. Give a subcommand, e.g. commit or rev-parse, or, where the program has none, the option that selects what it does, e.g. -p'
|
|
459
|
-
}
|
|
460
|
-
return undefined
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
export function misaccepted(
|
|
464
|
-
targets: readonly Target[],
|
|
465
|
-
entities: readonly string[],
|
|
466
|
-
calls: readonly Call[],
|
|
467
|
-
): readonly string[] {
|
|
468
|
-
const isEntity = new Set(entities)
|
|
469
|
-
const isCall = new Set(calls.map((one) => one.id))
|
|
470
|
-
const found: string[] = []
|
|
471
|
-
for (const one of targets) {
|
|
472
|
-
const holds = holdsOf(one.id)
|
|
473
|
-
if (holds === undefined) continue
|
|
474
|
-
for (const named of one.takes) {
|
|
475
|
-
if (holds.takes === 'entities' && isEntity.has(named)) continue
|
|
476
|
-
if (holds.takes === 'calls' && isCall.has(named)) continue
|
|
477
|
-
|
|
478
|
-
if (holds.takes === null) {
|
|
479
|
-
found.push(`\`${one.id}\` takes "${named}", and a ${String(reachOf(one.id))} takes nothing: it answers the orders every ${String(reachOf(one.id))} answers, stated once on the target itself`)
|
|
480
|
-
continue
|
|
481
|
-
}
|
|
482
|
-
found.push(
|
|
483
|
-
`\`${one.id}\` is reached over ${String(reachOf(one.id))} and so accepts ${holds.takes}, and its \`takes\` names ` +
|
|
484
|
-
`"${named}", which is ${isEntity.has(named) ? 'an entity' : isCall.has(named) ? 'a call' : 'nothing this app declares'}`,
|
|
485
|
-
)
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
return found
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
export const placedAt = (targets: readonly Target[], id: string): string => targets.find((one) => one.id === id)?.address ?? ''
|
|
492
|
-
|
|
493
|
-
export function placed(one: Target): readonly string[] {
|
|
494
|
-
const said = one.address
|
|
495
|
-
if (said === undefined) {
|
|
496
|
-
return [one.id + ' is reached over https and its row says nothing about where. Give it an address: the absolute url every call on it is written from, scheme and host and nothing after the path']
|
|
497
|
-
}
|
|
498
|
-
let held: URL
|
|
499
|
-
try {
|
|
500
|
-
held = new URL(said)
|
|
501
|
-
} catch {
|
|
502
|
-
return [one.id + ' has the address ' + JSON.stringify(said) + ', and that is not an absolute url. Write the whole of it, scheme and host']
|
|
503
|
-
}
|
|
504
|
-
if ([held.search, held.hash].some((each) => each !== '')) {
|
|
505
|
-
return [one.id + ' has the address ' + said + ', which carries a query or a fragment. Those belong to a call, so an address ends at its path']
|
|
506
|
-
}
|
|
507
|
-
const wrong = Object.entries(one.headers ?? {}).filter(([, value]) => typeof value !== 'string')
|
|
508
|
-
if (wrong.length > 0) {
|
|
509
|
-
return [one.id + ' carries the header ' + String(wrong[0]?.[0] ?? '') + ' with a value that is not text. A header every call carries is written as text, exactly as it goes on the wire']
|
|
510
|
-
}
|
|
511
|
-
return []
|
|
512
|
-
}
|
|
513
|
-
|
|
514
|
-
const stated = (one: Target, holds: Holds): readonly string[] => {
|
|
515
|
-
if (holds.addressed === 'row') return placed(one)
|
|
516
|
-
const carried = [...(one.address === undefined ? [] : ['an address']), ...(one.headers === undefined ? [] : ['headers'])]
|
|
517
|
-
if (carried.length === 0) return []
|
|
518
|
-
return [one.id + ' carries ' + carried.join(' and ') + ', and a ' + String(reachOf(one.id)) + ' is ' + ADDRESS[holds.addressed] + '. Take it off the row']
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
export function unaddressed(targets: readonly Target[], secrets: readonly Secret[]): readonly string[] {
|
|
522
|
-
const held = new Map(secrets.map((one) => [one.id, one]))
|
|
523
|
-
const found: string[] = []
|
|
524
|
-
for (const one of targets) {
|
|
525
|
-
const holds = holdsOf(one.id)
|
|
526
|
-
if (holds === undefined) continue
|
|
527
|
-
found.push(...stated(one, holds))
|
|
528
|
-
const addresses = one.requires
|
|
529
|
-
.map((each) => held.get(each))
|
|
530
|
-
.filter((each): each is Secret => each !== undefined && each.sends === undefined && each.signs === undefined)
|
|
531
|
-
if (holds.addressed !== 'secret') {
|
|
532
|
-
|
|
533
|
-
if (addresses.length > 0) {
|
|
534
|
-
found.push(
|
|
535
|
-
one.id + ' is reached over ' + String(reachOf(one.id)) + ', which is ' + ADDRESS[holds.addressed] + ', and it requires ' +
|
|
536
|
-
(addresses[0]?.id ?? '') + ', a secret with nowhere to be sent. A secret is a credential: give it the header it goes on with sends, or take it off this row',
|
|
537
|
-
)
|
|
538
|
-
}
|
|
539
|
-
continue
|
|
540
|
-
}
|
|
541
|
-
if (addresses.length === 0) {
|
|
542
|
-
found.push(
|
|
543
|
-
`\`${one.id}\` is reached over ${String(reachOf(one.id))} and nothing says where it is. Require one secret with ` +
|
|
544
|
-
'no `sends` — that one is its address. A credential with nowhere to be sent is a value nothing can use',
|
|
545
|
-
)
|
|
546
|
-
continue
|
|
547
|
-
}
|
|
548
|
-
if (addresses.length > 1) {
|
|
549
|
-
found.push(
|
|
550
|
-
`\`${one.id}\` requires ${addresses.map((each) => `\`${each.id}\``).join(' and ')}, and neither says where it goes, ` +
|
|
551
|
-
'so both read as its address. One place is reached at one address',
|
|
552
|
-
)
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
return found
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
export function unheld(targets: readonly Target[], secrets: readonly Secret[]): readonly string[] {
|
|
559
|
-
const known = new Set([...secrets.map((one) => one.id), ...targets.map((one) => one.id)])
|
|
560
|
-
const found: string[] = []
|
|
561
|
-
for (const one of targets) {
|
|
562
|
-
for (const named of one.requires) {
|
|
563
|
-
if (known.has(named)) continue
|
|
564
|
-
found.push(
|
|
565
|
-
`\`${one.id}\` requires "${named}", and this app declares no secret and no target by that name. A secret is declared before it ` +
|
|
566
|
-
'is required, and a target before it is reached through, so the one place saying who holds a value, or ' +
|
|
567
|
-
'answers for a reach, is never whichever place happened to need it',
|
|
568
|
-
)
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
return found
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
export function unanswered(calls: readonly Call[], targets: readonly Target[]): readonly string[] {
|
|
575
|
-
const held = new Map(targets.map((one) => [one.id, one]))
|
|
576
|
-
const found: string[] = []
|
|
577
|
-
for (const one of calls) {
|
|
578
|
-
const of = held.get(one.of)
|
|
579
|
-
if (of === undefined) {
|
|
580
|
-
found.push(`\`${one.id}\` is a call on "${one.of}", and this app declares no target by that name`)
|
|
581
|
-
continue
|
|
582
|
-
}
|
|
583
|
-
const holds = holdsOf(of.id)
|
|
584
|
-
if (holds !== undefined && holds.takes !== 'calls') {
|
|
585
|
-
found.push(
|
|
586
|
-
holds.takes === 'entities'
|
|
587
|
-
? `\`${one.id}\` is a call on \`${of.id}\`, which is reached over ${String(reachOf(of.id))} and keeps entities. A place you keep rows in is not a place you issue verbs at`
|
|
588
|
-
: `\`${one.id}\` is a call on \`${of.id}\`, which is a ${String(reachOf(of.id))} and answers the orders every ${String(reachOf(of.id))} answers, so nothing is declared on it. A node is handed those orders by requiring it`,
|
|
589
|
-
)
|
|
590
|
-
continue
|
|
591
|
-
}
|
|
592
|
-
if (reachOf(of.id) === 'https' && one.method === undefined) {
|
|
593
|
-
found.push(`\`${one.id}\` is a call over https and states no method. A call over https says its verb: GET, POST, PUT, PATCH or DELETE`)
|
|
594
|
-
continue
|
|
595
|
-
}
|
|
596
|
-
if (reachOf(of.id) !== 'https' && one.method !== undefined) {
|
|
597
|
-
found.push(`\`${one.id}\` states the method ${one.method}, and \`${of.id}\` is a ${String(reachOf(of.id))}. A subcommand or a method has no http verb, so take \`method\` off that row`)
|
|
598
|
-
continue
|
|
599
|
-
}
|
|
600
|
-
const shaped = misnamed(of.id, one.path)
|
|
601
|
-
if (shaped !== undefined) {
|
|
602
|
-
found.push(`\`${one.id}\` is a call on \`${of.id}\`, and its path ${shaped}`)
|
|
603
|
-
continue
|
|
604
|
-
}
|
|
605
|
-
if (!of.takes.includes(one.id)) {
|
|
606
|
-
found.push(
|
|
607
|
-
`\`${one.id}\` is a call on \`${of.id}\` and \`${of.id}\` does not accept it. Add it to that target's \`takes\` — ` +
|
|
608
|
-
'what a target accepts is read off the target, not gathered from whatever pointed at it',
|
|
609
|
-
)
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
return found
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
export function unplaced(targets: readonly Target[], integrations: readonly Integration[]): readonly string[] {
|
|
616
|
-
const known = new Set(integrations.map((one) => one.id))
|
|
617
|
-
const found: string[] = []
|
|
618
|
-
for (const one of targets) {
|
|
619
|
-
const named = one.of ?? ''
|
|
620
|
-
if (named === '') {
|
|
621
|
-
found.push(
|
|
622
|
-
`\`${one.id}\` names no integration, and every target is one way into something outside this app — a vendor, ` +
|
|
623
|
-
'a service, a program on the machine. Say which one it reaches into: `thread create integration` lists ' +
|
|
624
|
-
'the ones already declared and declares another',
|
|
625
|
-
)
|
|
626
|
-
continue
|
|
627
|
-
}
|
|
628
|
-
if (known.has(named)) continue
|
|
629
|
-
found.push(
|
|
630
|
-
`\`${one.id}\` is part of "${named}", and this app declares no integration by that name. Declare it with ` +
|
|
631
|
-
'`thread create integration`, or point this row at one already declared',
|
|
632
|
-
)
|
|
633
|
-
}
|
|
634
|
-
return found
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
export function refusals(
|
|
638
|
-
targets: readonly Target[],
|
|
639
|
-
secrets: readonly Secret[],
|
|
640
|
-
calls: readonly Call[],
|
|
641
|
-
entities: readonly string[],
|
|
642
|
-
integrations: readonly Integration[],
|
|
643
|
-
): readonly string[] {
|
|
644
|
-
return [
|
|
645
|
-
...unreachable(targets),
|
|
646
|
-
...unplaced(targets, integrations),
|
|
647
|
-
...unheld(targets, secrets),
|
|
648
|
-
...unaddressed(targets, secrets),
|
|
649
|
-
...unanswered(calls, targets),
|
|
650
|
-
...misaccepted(targets, entities, calls),
|
|
651
|
-
]
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
export function targetOf(entity: string, targets: readonly Target[]): Target | string {
|
|
655
|
-
const keeping = targets.filter((one) => holdsOf(one.id)?.takes === 'entities')
|
|
656
|
-
const found = keeping.filter((one) => one.takes.includes(entity))
|
|
657
|
-
const only = found[0]
|
|
658
|
-
if (only === undefined) {
|
|
659
|
-
return (
|
|
660
|
-
`"${entity}" is kept by no target. Add its id to the \`takes\` of one of ` +
|
|
661
|
-
`${keeping.length === 0 ? '(this app declares none that keep things)' : keeping.map((one) => `\`${one.id}\``).join(', ')} — ` +
|
|
662
|
-
'an entity nothing keeps is a table no database builds, and nothing else here would ever say so'
|
|
663
|
-
)
|
|
664
|
-
}
|
|
665
|
-
if (found.length > 1) {
|
|
666
|
-
return (
|
|
667
|
-
`"${entity}" is kept by ${found.map((one) => `\`${one.id}\``).join(' and ')}. One thing kept in two places is two ` +
|
|
668
|
-
'things that will disagree, and neither declaration says which one is right'
|
|
669
|
-
)
|
|
670
|
-
}
|
|
671
|
-
return only
|
|
672
|
-
}
|