@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,656 +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(name: string, at: string, there: (path: string) => boolean): string | undefined {
416
- if (NAMED.includes(name)) return name
417
- if (name === RUNTIME) return execPath
418
- if (name === PACKAGER) return managerOf(at, there)
419
- const installed = join(at, 'node_modules', '.bin', name)
420
- return there(installed) ? installed : undefined
421
- }
422
-
423
- export const unlisted = (name: string): string =>
424
- '`' + REACH.cli + name + '` names ' + name + ', and nothing lists a program by that name: it is not under node_modules/.bin, 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 + '`'
425
-
426
- const SHAPED: Readonly<Record<'cli', RegExp>> = {
427
- cli: /^-{0,2}[A-Za-z][A-Za-z0-9-]*$/,
428
- }
429
-
430
- const EVERY_VALUE = new Set(['constructor', '__proto__', 'prototype'])
431
-
432
- export function misnamed(target: string, name: string): string | undefined {
433
- const reach = reachOf(target)
434
- if (reach !== 'cli') return undefined
435
- if (EVERY_VALUE.has(name)) {
436
- return (
437
- `\`${name}\` is a step every JavaScript value has rather than something \`${target}\` answers to, and walking to ` +
438
- 'it leaves what was declared'
439
- )
440
- }
441
- if (!SHAPED[reach].test(name)) {
442
- 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'
443
- }
444
- return undefined
445
- }
446
-
447
- export function misaccepted(
448
- targets: readonly Target[],
449
- entities: readonly string[],
450
- calls: readonly Call[],
451
- ): readonly string[] {
452
- const isEntity = new Set(entities)
453
- const isCall = new Set(calls.map((one) => one.id))
454
- const found: string[] = []
455
- for (const one of targets) {
456
- const holds = holdsOf(one.id)
457
- if (holds === undefined) continue
458
- for (const named of one.takes) {
459
- if (holds.takes === 'entities' && isEntity.has(named)) continue
460
- if (holds.takes === 'calls' && isCall.has(named)) continue
461
-
462
- if (holds.takes === null) {
463
- 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`)
464
- continue
465
- }
466
- found.push(
467
- `\`${one.id}\` is reached over ${String(reachOf(one.id))} and so accepts ${holds.takes}, and its \`takes\` names ` +
468
- `"${named}", which is ${isEntity.has(named) ? 'an entity' : isCall.has(named) ? 'a call' : 'nothing this app declares'}`,
469
- )
470
- }
471
- }
472
- return found
473
- }
474
-
475
- export const placedAt = (targets: readonly Target[], id: string): string => targets.find((one) => one.id === id)?.address ?? ''
476
-
477
- export function placed(one: Target): readonly string[] {
478
- const said = one.address
479
- if (said === undefined) {
480
- 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']
481
- }
482
- let held: URL
483
- try {
484
- held = new URL(said)
485
- } catch {
486
- return [one.id + ' has the address ' + JSON.stringify(said) + ', and that is not an absolute url. Write the whole of it, scheme and host']
487
- }
488
- if ([held.search, held.hash].some((each) => each !== '')) {
489
- 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']
490
- }
491
- const wrong = Object.entries(one.headers ?? {}).filter(([, value]) => typeof value !== 'string')
492
- if (wrong.length > 0) {
493
- 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']
494
- }
495
- return []
496
- }
497
-
498
- const stated = (one: Target, holds: Holds): readonly string[] => {
499
- if (holds.addressed === 'row') return placed(one)
500
- const carried = [...(one.address === undefined ? [] : ['an address']), ...(one.headers === undefined ? [] : ['headers'])]
501
- if (carried.length === 0) return []
502
- return [one.id + ' carries ' + carried.join(' and ') + ', and a ' + String(reachOf(one.id)) + ' is ' + ADDRESS[holds.addressed] + '. Take it off the row']
503
- }
504
-
505
- export function unaddressed(targets: readonly Target[], secrets: readonly Secret[]): readonly string[] {
506
- const held = new Map(secrets.map((one) => [one.id, one]))
507
- const found: string[] = []
508
- for (const one of targets) {
509
- const holds = holdsOf(one.id)
510
- if (holds === undefined) continue
511
- found.push(...stated(one, holds))
512
- const addresses = one.requires
513
- .map((each) => held.get(each))
514
- .filter((each): each is Secret => each !== undefined && each.sends === undefined && each.signs === undefined)
515
- if (holds.addressed !== 'secret') {
516
-
517
- if (addresses.length > 0) {
518
- found.push(
519
- one.id + ' is reached over ' + String(reachOf(one.id)) + ', which is ' + ADDRESS[holds.addressed] + ', and it requires ' +
520
- (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',
521
- )
522
- }
523
- continue
524
- }
525
- if (addresses.length === 0) {
526
- found.push(
527
- `\`${one.id}\` is reached over ${String(reachOf(one.id))} and nothing says where it is. Require one secret with ` +
528
- 'no `sends` — that one is its address. A credential with nowhere to be sent is a value nothing can use',
529
- )
530
- continue
531
- }
532
- if (addresses.length > 1) {
533
- found.push(
534
- `\`${one.id}\` requires ${addresses.map((each) => `\`${each.id}\``).join(' and ')}, and neither says where it goes, ` +
535
- 'so both read as its address. One place is reached at one address',
536
- )
537
- }
538
- }
539
- return found
540
- }
541
-
542
- export function unheld(targets: readonly Target[], secrets: readonly Secret[]): readonly string[] {
543
- const known = new Set([...secrets.map((one) => one.id), ...targets.map((one) => one.id)])
544
- const found: string[] = []
545
- for (const one of targets) {
546
- for (const named of one.requires) {
547
- if (known.has(named)) continue
548
- found.push(
549
- `\`${one.id}\` requires "${named}", and this app declares no secret and no target by that name. A secret is declared before it ` +
550
- 'is required, and a target before it is reached through, so the one place saying who holds a value, or ' +
551
- 'answers for a reach, is never whichever place happened to need it',
552
- )
553
- }
554
- }
555
- return found
556
- }
557
-
558
- export function unanswered(calls: readonly Call[], targets: readonly Target[]): readonly string[] {
559
- const held = new Map(targets.map((one) => [one.id, one]))
560
- const found: string[] = []
561
- for (const one of calls) {
562
- const of = held.get(one.of)
563
- if (of === undefined) {
564
- found.push(`\`${one.id}\` is a call on "${one.of}", and this app declares no target by that name`)
565
- continue
566
- }
567
- const holds = holdsOf(of.id)
568
- if (holds !== undefined && holds.takes !== 'calls') {
569
- found.push(
570
- holds.takes === 'entities'
571
- ? `\`${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`
572
- : `\`${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`,
573
- )
574
- continue
575
- }
576
- if (reachOf(of.id) === 'https' && one.method === undefined) {
577
- 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`)
578
- continue
579
- }
580
- if (reachOf(of.id) !== 'https' && one.method !== undefined) {
581
- 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`)
582
- continue
583
- }
584
- const shaped = misnamed(of.id, one.path)
585
- if (shaped !== undefined) {
586
- found.push(`\`${one.id}\` is a call on \`${of.id}\`, and its path ${shaped}`)
587
- continue
588
- }
589
- if (!of.takes.includes(one.id)) {
590
- found.push(
591
- `\`${one.id}\` is a call on \`${of.id}\` and \`${of.id}\` does not accept it. Add it to that target's \`takes\` — ` +
592
- 'what a target accepts is read off the target, not gathered from whatever pointed at it',
593
- )
594
- }
595
- }
596
- return found
597
- }
598
-
599
- export function unplaced(targets: readonly Target[], integrations: readonly Integration[]): readonly string[] {
600
- const known = new Set(integrations.map((one) => one.id))
601
- const found: string[] = []
602
- for (const one of targets) {
603
- const named = one.of ?? ''
604
- if (named === '') {
605
- found.push(
606
- `\`${one.id}\` names no integration, and every target is one way into something outside this app — a vendor, ` +
607
- 'a service, a program on the machine. Say which one it reaches into: `thread create integration` lists ' +
608
- 'the ones already declared and declares another',
609
- )
610
- continue
611
- }
612
- if (known.has(named)) continue
613
- found.push(
614
- `\`${one.id}\` is part of "${named}", and this app declares no integration by that name. Declare it with ` +
615
- '`thread create integration`, or point this row at one already declared',
616
- )
617
- }
618
- return found
619
- }
620
-
621
- export function refusals(
622
- targets: readonly Target[],
623
- secrets: readonly Secret[],
624
- calls: readonly Call[],
625
- entities: readonly string[],
626
- integrations: readonly Integration[],
627
- ): readonly string[] {
628
- return [
629
- ...unreachable(targets),
630
- ...unplaced(targets, integrations),
631
- ...unheld(targets, secrets),
632
- ...unaddressed(targets, secrets),
633
- ...unanswered(calls, targets),
634
- ...misaccepted(targets, entities, calls),
635
- ]
636
- }
637
-
638
- export function targetOf(entity: string, targets: readonly Target[]): Target | string {
639
- const keeping = targets.filter((one) => holdsOf(one.id)?.takes === 'entities')
640
- const found = keeping.filter((one) => one.takes.includes(entity))
641
- const only = found[0]
642
- if (only === undefined) {
643
- return (
644
- `"${entity}" is kept by no target. Add its id to the \`takes\` of one of ` +
645
- `${keeping.length === 0 ? '(this app declares none that keep things)' : keeping.map((one) => `\`${one.id}\``).join(', ')} — ` +
646
- 'an entity nothing keeps is a table no database builds, and nothing else here would ever say so'
647
- )
648
- }
649
- if (found.length > 1) {
650
- return (
651
- `"${entity}" is kept by ${found.map((one) => `\`${one.id}\``).join(' and ')}. One thing kept in two places is two ` +
652
- 'things that will disagree, and neither declaration says which one is right'
653
- )
654
- }
655
- return only
656
- }