@prestack/thread 0.0.1 → 0.0.2

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 (150) hide show
  1. package/dist/core/actor.ts +20 -0
  2. package/dist/core/asking.d.ts +0 -1
  3. package/dist/core/asking.js +0 -1
  4. package/dist/core/asking.ts +1342 -0
  5. package/dist/core/attach.js +2 -2
  6. package/dist/core/attach.ts +585 -0
  7. package/dist/core/chain.js +8 -1
  8. package/dist/core/chain.ts +148 -0
  9. package/dist/core/cli.js +97 -18
  10. package/dist/core/cli.ts +2004 -0
  11. package/dist/core/condition.ts +227 -0
  12. package/dist/core/descend.d.ts +1 -0
  13. package/dist/core/descend.js +36 -0
  14. package/dist/core/descend.ts +1001 -0
  15. package/dist/core/finding.ts +38 -0
  16. package/dist/core/generators/meta.js +6 -0
  17. package/dist/core/generators/meta.ts +1006 -0
  18. package/dist/core/init.ts +170 -0
  19. package/dist/core/invoked.ts +47 -0
  20. package/dist/core/round.ts +57 -0
  21. package/dist/core/rows.ts +369 -0
  22. package/dist/core/scope.ts +18 -0
  23. package/dist/core/thread.ts +29 -0
  24. package/dist/core/tree.ts +67 -0
  25. package/dist/core/types.ts +350 -0
  26. package/dist/derivation/attach.js +1 -1
  27. package/dist/derivation/attach.ts +68 -0
  28. package/dist/derivation/generators/build.ts +232 -0
  29. package/dist/derivation/generators/check.ts +118 -0
  30. package/dist/derivation/generators/derivation.ts +347 -0
  31. package/dist/derivation/generators/determinism.ts +145 -0
  32. package/dist/derivation/generators/release.js +19 -9
  33. package/dist/derivation/generators/release.ts +213 -0
  34. package/dist/derivation/makers/release.ts +135 -0
  35. package/dist/derivation/release.ts +23 -0
  36. package/dist/integrations/attach.js +2 -2
  37. package/dist/integrations/attach.ts +207 -0
  38. package/dist/integrations/generators/read.ts +127 -0
  39. package/dist/integrations/generators/secret.ts +289 -0
  40. package/dist/integrations/generators/target.js +16 -6
  41. package/dist/integrations/generators/target.ts +944 -0
  42. package/dist/integrations/generators/vault.js +46 -14
  43. package/dist/integrations/generators/vault.ts +212 -0
  44. package/dist/integrations/{sdk.js → module.js} +4 -4
  45. package/dist/integrations/module.ts +189 -0
  46. package/dist/integrations/secret.ts +50 -0
  47. package/dist/integrations/target.d.ts +3 -1
  48. package/dist/integrations/target.js +14 -11
  49. package/dist/integrations/target.ts +340 -0
  50. package/dist/integrations/vault.d.ts +2 -0
  51. package/dist/integrations/vault.js +4 -2
  52. package/dist/integrations/vault.ts +136 -0
  53. package/dist/integrity/attach.ts +234 -0
  54. package/dist/integrity/check-threads.js +2 -26
  55. package/dist/integrity/check-threads.ts +166 -0
  56. package/dist/integrity/constraint.ts +1202 -0
  57. package/dist/integrity/generators/compiler.ts +161 -0
  58. package/dist/integrity/generators/gate.ts +201 -0
  59. package/dist/integrity/generators/issues.ts +160 -0
  60. package/dist/integrity/generators/publish.js +14 -5
  61. package/dist/integrity/generators/publish.ts +247 -0
  62. package/dist/integrity/generators/rule.ts +271 -0
  63. package/dist/integrity/generators/seal.ts +117 -0
  64. package/dist/integrity/generators/trial.ts +320 -0
  65. package/dist/integrity/lint.ts +227 -0
  66. package/dist/integrity/lsp/bin.ts +42 -0
  67. package/dist/integrity/lsp/diagnostics.ts +92 -0
  68. package/dist/integrity/lsp/findings.ts +63 -0
  69. package/dist/integrity/lsp/lint.ts +140 -0
  70. package/dist/integrity/lsp/protocol.ts +78 -0
  71. package/dist/integrity/lsp/server.ts +224 -0
  72. package/dist/integrity/sealed.js +2 -2
  73. package/dist/integrity/sealed.ts +56 -0
  74. package/dist/integrity/slopnator.ts +350 -0
  75. package/dist/integrity/slopnators/sl_broken_lineage.ts +36 -0
  76. package/dist/integrity/slopnators/sl_declared_elsewhere.ts +39 -0
  77. package/dist/integrity/slopnators/sl_derived_drift.ts +98 -0
  78. package/dist/integrity/slopnators/sl_hardcoded_style.ts +55 -0
  79. package/dist/integrity/slopnators/sl_inert_island.ts +91 -0
  80. package/dist/integrity/slopnators/sl_inert_slopnator.ts +93 -0
  81. package/dist/integrity/slopnators/sl_missing_row.ts +182 -0
  82. package/dist/integrity/slopnators/sl_no_barrel.ts +48 -0
  83. package/dist/integrity/slopnators/sl_no_child_process.ts +74 -0
  84. package/dist/integrity/slopnators/sl_no_comment.ts +43 -0
  85. package/dist/integrity/slopnators/sl_no_dispatch_bypass.ts +328 -0
  86. package/dist/integrity/slopnators/sl_no_import_cycle.ts +50 -0
  87. package/dist/integrity/slopnators/sl_no_such_command.ts +64 -0
  88. package/dist/integrity/slopnators/sl_only_takes_a_literal.ts +63 -0
  89. package/dist/integrity/slopnators/sl_secret_literal.ts +100 -0
  90. package/dist/integrity/slopnators/sl_stale_path.ts +94 -0
  91. package/dist/integrity/slopnators/sl_twice_meant.ts +31 -0
  92. package/dist/integrity/slopnators/sl_twice_parented.ts +37 -0
  93. package/dist/integrity/slopnators/sl_undeclared_crossing.ts +87 -0
  94. package/dist/integrity/slopnators/sl_undeclared_destination.ts +65 -0
  95. package/dist/integrity/slopnators/sl_undeclared_element.ts +44 -0
  96. package/dist/integrity/slopnators/sl_undeclared_node.ts +69 -0
  97. package/dist/integrity/slopnators/sl_undeclared_operation.ts +147 -0
  98. package/dist/integrity/slopnators/sl_undeclared_variant.ts +148 -0
  99. package/dist/integrity/slopnators/sl_undescribed.ts +65 -0
  100. package/dist/integrity/slopnators/sl_ungoverned_island.ts +61 -0
  101. package/dist/integrity/slopnators/sl_unkept_rows.ts +111 -0
  102. package/dist/integrity/slopnators/sl_unnamed_island.ts +41 -0
  103. package/dist/integrity/slopnators/sl_unpaired_member.ts +131 -0
  104. package/dist/integrity/slopnators/sl_unpaired_slopnator.ts +58 -0
  105. package/dist/integrity/slopnators/sl_unprefixed_member.ts +136 -0
  106. package/dist/integrity/slopnators/sl_unseamed_reach.ts +79 -0
  107. package/dist/integrity/slopnators/sl_unthreaded.ts +419 -0
  108. package/dist/integrity/slopnators/sl_untranslated_key.ts +164 -0
  109. package/dist/integrity/slopnators/sl_unversioned.ts +36 -0
  110. package/dist/integrity/sweep.ts +307 -0
  111. package/dist/knowledge/attach.ts +350 -0
  112. package/dist/knowledge/generators/docsgen.ts +174 -0
  113. package/dist/knowledge/generators/glossary.ts +73 -0
  114. package/dist/knowledge/generators/skills.ts +263 -0
  115. package/dist/knowledge/glossary.ts +6 -0
  116. package/dist/operations/access.ts +88 -0
  117. package/dist/operations/attach.ts +475 -0
  118. package/dist/operations/capabilities.ts +65 -0
  119. package/dist/operations/cost.ts +58 -0
  120. package/dist/operations/dispatch.ts +348 -0
  121. package/dist/operations/error-results.ts +31 -0
  122. package/dist/operations/errors/catalog.ts +168 -0
  123. package/dist/operations/errors/classes.ts +51 -0
  124. package/dist/operations/errors/errors.ts +314 -0
  125. package/dist/operations/generators/capability.js +29 -19
  126. package/dist/operations/generators/capability.ts +704 -0
  127. package/dist/operations/generators/compile.js +20 -1
  128. package/dist/operations/generators/compile.ts +643 -0
  129. package/dist/operations/generators/grant.ts +336 -0
  130. package/dist/operations/generators/operation.ts +972 -0
  131. package/dist/operations/generators/permits.ts +223 -0
  132. package/dist/operations/generators/seed.ts +347 -0
  133. package/dist/operations/generators/trial.js +4 -1
  134. package/dist/operations/generators/trial.ts +529 -0
  135. package/dist/operations/http.ts +164 -0
  136. package/dist/operations/makers/capability.ts +166 -0
  137. package/dist/operations/makers/trial.ts +520 -0
  138. package/dist/operations/memory.ts +470 -0
  139. package/dist/operations/metering.ts +328 -0
  140. package/dist/operations/nodes.ts +54 -0
  141. package/dist/operations/operations.ts +32 -0
  142. package/dist/operations/queue-policy.ts +10 -0
  143. package/dist/operations/result.ts +28 -0
  144. package/dist/operations/seams.d.ts +4 -4
  145. package/dist/operations/seams.ts +364 -0
  146. package/dist/operations/store.ts +29 -0
  147. package/package.json +8 -1
  148. package/dist/integrations/generators/sdk.d.ts +0 -1
  149. package/dist/integrations/generators/sdk.js +0 -148
  150. /package/dist/integrations/{sdk.d.ts → module.d.ts} +0 -0
@@ -0,0 +1,704 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'
2
+ import { dirname, resolve } from 'node:path'
3
+ import { argv, stdout } from 'node:process'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ import { marked, wordsIn } from '../../core/asking.ts'
7
+ import { ROOT, TREE, descend } from '../../core/descend.ts'
8
+ import { exit, invoked } from '../../core/invoked.ts'
9
+ import { named as packageOf } from '../../core/scope.ts'
10
+ import { PREFIXES } from '../../core/thread.ts'
11
+
12
+ import type { Secret } from '../../integrations/secret.ts'
13
+ import { servesBrowser, type Target } from '../../integrations/target.ts'
14
+
15
+ import {
16
+ CAPABILITIES_AT,
17
+ CLIENT_AT,
18
+ DECLARED_SEAMS_AT,
19
+ MCP_AT,
20
+ OPENAPI_AT,
21
+ DISPATCHING_AT,
22
+ SEAMS_AT,
23
+ routeAt,
24
+ SURFACES,
25
+ switched,
26
+ type Capability,
27
+ type Surface,
28
+ } from '../capabilities.ts'
29
+ import { ANONYMOUS, ENVIRONMENT } from '../../core/actor.ts'
30
+ import { PERMITS_AT } from '../access.ts'
31
+ import { SAID as HANDED, supplies } from '../seams.ts'
32
+ import { beside, operations, quoted, runtime, types } from './seed.ts'
33
+
34
+ interface Written {
35
+ readonly at: string
36
+ readonly text: string
37
+ }
38
+
39
+ const compare = (a: string, b: string): number => (a < b ? -1 : a > b ? 1 : 0)
40
+
41
+ function stable(value: unknown): unknown {
42
+ if (Array.isArray(value)) return value.map(stable)
43
+ if (typeof value === 'object' && value !== null) {
44
+ const found: Record<string, unknown> = {}
45
+ for (const key of Object.keys(value as Record<string, unknown>).sort(compare)) {
46
+ found[key] = stable((value as Record<string, unknown>)[key])
47
+ }
48
+ return found
49
+ }
50
+ return value === undefined ? null : value
51
+ }
52
+
53
+ const printed = (value: unknown): string => `${JSON.stringify(stable(value), null, 2)}\n`
54
+
55
+ function verbOf(id: string): 'GET' | 'POST' | undefined {
56
+ if (id.startsWith(PREFIXES.view)) return 'GET'
57
+ if (id.startsWith(PREFIXES.workflow) || id.startsWith(PREFIXES.agent)) return 'POST'
58
+ return undefined
59
+ }
60
+
61
+ async function schemaOf(at: string): Promise<unknown | string> {
62
+ const from = resolve(ROOT, at)
63
+ if (!existsSync(from)) return `${at} is not there, so nothing states what this operation takes`
64
+
65
+ const held = (await import(pathToFileURL(from).href)) as { readonly input?: unknown }
66
+ const input = held.input
67
+ if (input === undefined) return `${at} exports no \`input\`, so nothing states what this operation takes`
68
+
69
+ const zod = (await import('zod')) as { readonly toJSONSchema?: (one: unknown) => unknown }
70
+ if (zod.toJSONSchema === undefined) {
71
+ return 'this app\'s zod states no `toJSONSchema`, so an input schema cannot be written from it'
72
+ }
73
+ try {
74
+ return zod.toJSONSchema(input)
75
+ } catch (thrown) {
76
+ return `${at} states an input that cannot be written as a schema: ${thrown instanceof Error ? thrown.message : String(thrown)}`
77
+ }
78
+ }
79
+
80
+ async function sheet<T>(file: string, list: string): Promise<readonly T[]> {
81
+ const at = resolve(ROOT, `_threads/${file}`)
82
+ if (!existsSync(at)) return []
83
+ try {
84
+ const held = (await import(pathToFileURL(at).href)) as Record<string, readonly T[] | undefined>
85
+ return held[list] ?? []
86
+ } catch {
87
+ return []
88
+ }
89
+ }
90
+
91
+ function browsing(requires: readonly string[], targets: readonly Target[], secrets: readonly Secret[]): boolean {
92
+ for (const word of requires) {
93
+ const target = targets.find((one) => one.id === word)
94
+ if (target === undefined) continue
95
+ if (servesBrowser(target, secrets) !== true) return false
96
+ }
97
+ return true
98
+ }
99
+
100
+ function locally(requires: readonly string[]): boolean {
101
+ for (const word of requires) {
102
+ if (!supplies(word)) return false
103
+ if (word === 'sm_vault') return false
104
+ }
105
+ return true
106
+ }
107
+
108
+ export async function derive(): Promise<{
109
+ readonly found: readonly Capability[]
110
+ readonly refusals: readonly string[]
111
+ }> {
112
+ const pass = descend()
113
+ const known = types(pass)
114
+ const refusals: string[] = []
115
+ const found: Capability[] = []
116
+
117
+ const targets = await sheet<Target>('targets.ts', 'TARGETS')
118
+ const secrets = await sheet<Secret>('secrets.ts', 'SECRETS')
119
+
120
+ const at = resolve(ROOT, CAPABILITIES_AT)
121
+ const already = switched(existsSync(at) ? readFileSync(at, 'utf8') : undefined)
122
+
123
+ for (const [id, one] of [...operations(pass, known)].sort((a, b) => compare(a[0], b[0]))) {
124
+ const stated = pass.stated.get(id)
125
+ const describes = stated?.describes
126
+ if (describes === undefined) {
127
+ refusals.push(`\`${id}\` states no \`describes\`, and a caller told nothing about an operation cannot choose it`)
128
+ continue
129
+ }
130
+
131
+ const method = verbOf(id)
132
+ if (method === undefined) {
133
+ refusals.push(
134
+ `\`${id}\` wears none of the prefixes an operation may wear (${Object.values(PREFIXES).join(', ')}), so there is no verb to give it`,
135
+ )
136
+ continue
137
+ }
138
+
139
+ const takes = one.takes[0]?.at
140
+ if (takes === undefined) {
141
+ refusals.push(`\`${id}\` names nothing it takes, so nothing states its input`)
142
+ continue
143
+ }
144
+
145
+ const schema = await schemaOf(takes)
146
+ if (typeof schema === 'string') {
147
+ refusals.push(`\`${id}\`: ${schema}`)
148
+ continue
149
+ }
150
+
151
+ const live = already.get(id) ?? [...SURFACES]
152
+
153
+ found.push({
154
+ operation: id,
155
+ describes,
156
+ requires: [...one.requires].sort(compare),
157
+ nodes: one.nodes.map((each) => each.id ?? '').filter((each) => each !== ''),
158
+ input: schema,
159
+ live: [...live].sort(compare) as readonly Surface[],
160
+ browsable: browsing(one.requires, targets, secrets),
161
+ local: browsing(one.requires, targets, secrets) && locally(one.requires),
162
+ mcp: { name: id, description: describes, inputSchema: schema },
163
+ cli: { invocation: `${id} --input '<json>'`, describes },
164
+ api: { method, path: `/operations/${id}` },
165
+ })
166
+ }
167
+
168
+ return { found, refusals }
169
+ }
170
+
171
+ function named(): { readonly title: string; readonly version: string } {
172
+ try {
173
+ const held = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')) as {
174
+ readonly name?: unknown
175
+ readonly version?: unknown
176
+ }
177
+ return {
178
+ title: typeof held.name === 'string' ? held.name : 'this app',
179
+ version: typeof held.version === 'string' ? held.version : '0.0.0',
180
+ }
181
+ } catch {
182
+ return { title: 'this app', version: '0.0.0' }
183
+ }
184
+ }
185
+
186
+ function openapi(found: readonly Capability[]): unknown {
187
+ const paths: Record<string, unknown> = {}
188
+ for (const one of found) {
189
+ if (!one.live.includes('api')) continue
190
+ const method = one.api.method.toLowerCase()
191
+ const responses = {
192
+ '200': {
193
+ description: 'the operation ran',
194
+ content: { 'application/json': { schema: { type: 'object', properties: { ok: { const: true }, value: {} }, required: ['ok'] } } },
195
+ },
196
+ '400': {
197
+ description: 'dispatch refused it — the code says which gate',
198
+ content: {
199
+ 'application/json': {
200
+ schema: {
201
+ type: 'object',
202
+ properties: {
203
+ ok: { const: false },
204
+ code: { enum: ['denied', 'invalid', 'rate_limited', 'feature_denied', 'quota_exceeded', 'error'] },
205
+ error: { type: 'string' },
206
+ because: { type: 'string' },
207
+ },
208
+ required: ['ok', 'code', 'error'],
209
+ },
210
+ },
211
+ },
212
+ },
213
+ }
214
+ paths[one.api.path] = {
215
+ [method]: {
216
+ operationId: one.operation,
217
+ summary: one.describes,
218
+ ...(one.api.method === 'GET'
219
+ ? { parameters: [{ name: 'input', in: 'query', required: true, content: { 'application/json': { schema: one.input } } }] }
220
+ : { requestBody: { required: true, content: { 'application/json': { schema: one.input } } } }),
221
+ responses,
222
+ },
223
+ }
224
+ }
225
+ const who = named()
226
+ return {
227
+ openapi: '3.1.0',
228
+ info: { title: who.title, version: who.version, description: 'Every operation this app declares that is live on its API surface.' },
229
+ paths,
230
+ }
231
+ }
232
+
233
+ const mcp = (found: readonly Capability[]): unknown => ({
234
+ tools: found.filter((one) => one.live.includes('mcp')).map((one) => one.mcp),
235
+ })
236
+
237
+ const compiled = (operation: string): boolean => existsSync(resolve(ROOT, `operations/${operation}.ts`))
238
+
239
+ function client(found: readonly Capability[]): string {
240
+ const rows = found
241
+ .filter(held)
242
+ .map((one) =>
243
+ [
244
+ ' {',
245
+ ` operation: ${JSON.stringify(one.operation)},`,
246
+ ` describes: ${JSON.stringify(one.describes)},`,
247
+ ` method: ${JSON.stringify(one.api.method)},`,
248
+ ` path: ${JSON.stringify(one.api.path)},`,
249
+ ' },',
250
+ ].join('\n'),
251
+ )
252
+
253
+ const near = found.filter((one) => one.local && compiled(one.operation))
254
+
255
+ const bound = near.flatMap((one) => [
256
+ `export async function ${one.operation}Locally(input: unknown, actor: Actor, seams: Seams): Promise<Result<unknown>> {`,
257
+ ` const held = await dispatch(${one.operation}, input, actor, seams)`,
258
+ ' return held',
259
+ '}',
260
+ '',
261
+ ])
262
+
263
+ return [
264
+ ...(near.length === 0
265
+ ? []
266
+ : [
267
+ `import type { Actor } from '${runtime(CLIENT_AT, '../../core/actor.ts')}'`,
268
+ `import type { Result } from '${runtime(CLIENT_AT, '../result.ts')}'`,
269
+ `import type { Seams } from '${runtime(CLIENT_AT, '../seams.ts')}'`,
270
+ '',
271
+ "import { dispatch } from './_dispatch.ts'",
272
+ ...near.map((one) => `import { operation as ${one.operation} } from './${one.operation}.ts'`),
273
+ '',
274
+ ]),
275
+ 'export interface Reachable {',
276
+ ' readonly operation: string',
277
+ ' readonly describes: string',
278
+ " readonly method: 'GET' | 'POST'",
279
+ ' readonly path: string',
280
+ '}',
281
+ '',
282
+ `export const CLIENT: readonly Reachable[] = [${rows.length === 0 ? '' : `\n${rows.join('\n')}\n`}]`,
283
+ '',
284
+ 'export const reachable = (operation: string): Reachable | undefined =>',
285
+ ' CLIENT.find((one) => one.operation === operation)',
286
+ '',
287
+ ...bound,
288
+ '',
289
+ ].join('\n')
290
+ }
291
+
292
+ function route(one: Capability): string {
293
+ const at = routeAt(one.operation)
294
+ return [
295
+ "import { existsSync } from 'node:fs'",
296
+ "import { join } from 'node:path'",
297
+ '',
298
+ "import { dispatch } from '../../../operations/_dispatch.ts'",
299
+ `import { actor } from '${beside(at, DECLARED_SEAMS_AT)}'`,
300
+ `import { SEAMS } from '${beside(at, SEAMS_AT)}'`,
301
+ `import { operation } from '../../../operations/${one.operation}.ts'`,
302
+ '',
303
+ 'const STATUS: Record<string, number> = {',
304
+ ' denied: 403,',
305
+ ' invalid: 400,',
306
+ ' rate_limited: 429,',
307
+ ' feature_denied: 403,',
308
+ ' quota_exceeded: 429,',
309
+ ' error: 500,',
310
+ '}',
311
+ '',
312
+ "const PASSED = existsSync(join(process.cwd(), '.thread', 'gate.json'))",
313
+ '',
314
+ `export async function ${one.api.method}(request: Request): Promise<Response> {`,
315
+ ' if (!PASSED) {',
316
+ ' return Response.json(',
317
+ " { ok: false, code: 'error', error: 'failed', because: 'this tree has no passing gate, so nothing in it may answer a request. Run `thread gate` — a gate that has never run is not a gate that passed' },",
318
+ ' { status: 500 },',
319
+ ' )',
320
+ ' }',
321
+ ...(one.api.method === 'GET'
322
+ ? [' const input = Object.fromEntries(new URL(request.url).searchParams)']
323
+ : [' const input = await request.json().catch(() => undefined)']),
324
+ ' const held = await dispatch(operation, input, await actor(request), SEAMS)',
325
+ ' return Response.json(held, { status: held.ok ? 200 : (STATUS[held.code] ?? 500) })',
326
+ '}',
327
+ '',
328
+ ].join('\n')
329
+ }
330
+
331
+ const SEAM = /export\s+const\s+store\s*:/
332
+
333
+ const STORES_AT = 'src/stores'
334
+
335
+ function kept(): readonly string[] {
336
+ const at = resolve(ROOT, STORES_AT)
337
+ if (!existsSync(at)) return []
338
+ return readdirSync(at)
339
+ .filter((one) => one.endsWith('.ts'))
340
+ .filter((one) => {
341
+ try {
342
+ return SEAM.test(readFileSync(resolve(at, one), 'utf8'))
343
+ } catch {
344
+ return false
345
+ }
346
+ })
347
+ .sort()
348
+ .map((one) => `${STORES_AT}/${one}`)
349
+ }
350
+
351
+ const ENTITIES_AT = `${TREE}entities.ts`
352
+
353
+ const OPERATIONS_AT = `${TREE}operations.ts`
354
+
355
+ const VAULT_AT = `src/vault.${'ts'}`
356
+
357
+ function settled(keeping: boolean, stores: readonly string[], ruled: boolean, vaulted: boolean): string {
358
+ const named = stores.map((one, at) => ({ from: beside(DECLARED_SEAMS_AT, one), as: `s${String(at)}` }))
359
+ const only = named[0]
360
+ return [
361
+ `import type { Actor, Environment } from '${runtime(DECLARED_SEAMS_AT, '../../core/actor.ts')}'`,
362
+ `import type { Seams } from '${runtime(DECLARED_SEAMS_AT, '../seams.ts')}'`,
363
+ ...(keeping ? [`import { ENTITIES } from '${beside(DECLARED_SEAMS_AT, ENTITIES_AT)}'`] : []),
364
+ ...(ruled ? [`import { permits } from '${beside(DECLARED_SEAMS_AT, PERMITS_AT)}'`] : []),
365
+ ...(vaulted ? [`import { vault as opening } from '${beside(DECLARED_SEAMS_AT, VAULT_AT)}'`] : []),
366
+ ...(stores.length === 1 && only !== undefined ? [`import { store as kept } from '${only.from}'`] : []),
367
+ ...(stores.length > 1 ? named.map((one) => `import { keeps as ${one.as}Keeps, store as ${one.as} } from '${one.from}'`) : []),
368
+ '',
369
+ ...(keeping
370
+ ? [
371
+ 'const made = {', " log: { write(level: string, action: string, who: Actor, data: Record<string, unknown>) { console.log(JSON.stringify({ level, action, who, data })) } },", " audit: { async append(who: Actor, record: unknown) { console.log(JSON.stringify({ audit: record, who })) } },", " errors: { async record(who: Actor, failure: unknown) { console.error(JSON.stringify({ failure, who })) } },", '}',
372
+ ]
373
+ : ['const made = {', " log: { write(level: string, action: string, who: Actor, data: Record<string, unknown>) { console.log(JSON.stringify({ level, action, who, data })) } },", " audit: { async append(who: Actor, record: unknown) { console.log(JSON.stringify({ audit: record, who })) } },", " errors: { async record(who: Actor, failure: unknown) { console.error(JSON.stringify({ failure, who })) } },", '}']),
374
+ '',
375
+ ...(stores.length > 1
376
+ ? [
377
+ "const WHERE = new Map<string, Seams['store']>([",
378
+ ...named.map((one) => ` ...${one.as}Keeps.map((entity) => [entity, ${one.as}] as const),`),
379
+ '])',
380
+ '',
381
+ 'function at(entity: string) {',
382
+ ' const found = WHERE.get(entity)',
383
+ " if (found === undefined) throw new Error(entity + ' is not kept in any target this app declares')",
384
+ ' return found',
385
+ '}',
386
+ '',
387
+ "const store: Seams['store'] = {",
388
+ ' insert: (actor, entity, row) => at(entity).insert(actor, entity, row),',
389
+ ' get: (actor, entity, key, fields) => at(entity).get(actor, entity, key, fields),',
390
+ ' list: (actor, entity, opts) => at(entity).list(actor, entity, opts),',
391
+ ' update: (actor, entity, key, patch, expectedVersion) => at(entity).update(actor, entity, key, patch, expectedVersion),',
392
+ ' remove: (actor, entity, key) => at(entity).remove(actor, entity, key),',
393
+ '}',
394
+ '',
395
+ ]
396
+ : []),
397
+ `const ANONYMOUS: Actor = ${JSON.stringify(ANONYMOUS)}`,
398
+ '',
399
+ "const http: NonNullable<Seams['http']> = {",
400
+ ' async call(request) {',
401
+ ' const base = new URL(request.address)',
402
+ " if (base.protocol !== 'https:') throw new Error(request.target + ' is reached over ' + base.protocol + ' and an outward call goes over https only')",
403
+ ' const stem = base.pathname.length === 1 ? String() : base.pathname',
404
+ ' const url = new URL(base.origin + stem + request.path)',
405
+ " if (url.origin !== base.origin) throw new Error(request.path + ' resolves to ' + url.origin + ', which is not where ' + request.target + ' is')",
406
+ ' const headers: Record<string, string> = { ...request.headers }',
407
+ " if (request.contentType !== undefined) headers['content-type'] = request.contentType",
408
+ ' const carried = new Uint8Array(request.body === undefined ? 0 : request.body.byteLength)',
409
+ ' if (request.body !== undefined) carried.set(request.body)',
410
+ ' const began = Date.now()',
411
+ ' const sent = JSON.stringify(headers).length + (request.body === undefined ? 0 : request.body.byteLength)',
412
+ ' const answered = await fetch(url.toString(), { method: request.method, headers, ...(request.body === undefined ? {} : { body: carried }), signal: AbortSignal.timeout(10000) })',
413
+ ' const body = new Uint8Array(await answered.arrayBuffer())',
414
+ ' return { status: answered.status, headers: Object.fromEntries(answered.headers.entries()), body, bytesSent: sent, bytesReceived: body.byteLength, durationMs: Date.now() - began }',
415
+ ' },',
416
+ '}',
417
+ '',
418
+ 'const shared = globalThis as { __threadSeams?: Seams }',
419
+ '',
420
+ 'export const DECLARED: Seams = (shared.__threadSeams ??= {',
421
+ ' ...made,',
422
+ ...(stores.length === 1 ? [' store: kept,'] : []),
423
+ ...(stores.length > 1 ? [' store,'] : []),
424
+ ' clock: { now: () => Date.now() },',
425
+ ' random: { next: Math.random, uuid: () => crypto.randomUUID() },',
426
+ ' http,',
427
+ ...(ruled ? [' permits: { check: (actor, action, resource) => Promise.resolve(permits(actor, action, resource)) },'] : []),
428
+ ...(vaulted ? [' vault: { resolve: opening },'] : []),
429
+ '})',
430
+ '',
431
+ 'function environment(request: Request): Environment {',
432
+ ...ENVIRONMENT.map(([fact, header]) => ` const ${fact} = request.headers.get(${quoted(header)})`),
433
+ ' return {',
434
+ ...ENVIRONMENT.map(([fact]) => ` ...(${fact} === null ? {} : { ${fact} }),`),
435
+ ' }',
436
+ '}',
437
+ '',
438
+ ...(keeping
439
+ ? [
440
+ 'const worn = (held: unknown): readonly string[] =>',
441
+ " Array.isArray(held) ? held.filter((one) => typeof one === 'string') : typeof held === 'string' ? [held] : []",
442
+ '',
443
+ 'function addressed(request: Request, from: string): string | null {',
444
+ " if (from.startsWith('cookie:')) {",
445
+ " const jar = request.headers.get('cookie') ?? ''",
446
+ " const want = `${from.slice('cookie:'.length).trim()}=`",
447
+ " const found = jar.split(';').map((one) => one.trim()).find((one) => one.startsWith(want))",
448
+ ' return found === undefined ? null : decodeURIComponent(found.slice(want.length))',
449
+ ' }',
450
+ ' const held = request.headers.get(from)',
451
+ ' if (held === null) return null',
452
+ " const bare = held.startsWith('Bearer ') ? held.slice('Bearer '.length) : held",
453
+ " return bare.trim() === '' ? null : bare.trim()",
454
+ '}',
455
+ '',
456
+ 'export async function actor(request: Request): Promise<Actor> {',
457
+ ' const anyone: Actor = { ...ANONYMOUS, ...environment(request) }',
458
+ ' const who = ENTITIES.find((one) => one.actor !== undefined)',
459
+ ' if (who?.actor === undefined) return anyone',
460
+ '',
461
+ " const address = (who.must ?? []).flatMap((one) => ('unique' in one ? one.unique : []))[0]",
462
+ ' if (address === undefined) return anyone',
463
+ '',
464
+ ' const held = addressed(request, who.actor.from)',
465
+ ' if (held === null) return anyone',
466
+ '',
467
+ ' const token = who.actor.verifies',
468
+ '',
469
+ ' const row =',
470
+ ' token === undefined',
471
+ ' ? await DECLARED.store.get(anyone, who.id, held, [',
472
+ ' address,',
473
+ ' who.actor.roles,',
474
+ ' ...(who.actor.entitlements === undefined ? [] : [who.actor.entitlements]),',
475
+ ' ])',
476
+ ' : ((await DECLARED.store.list(anyone, who.id, { where: { [token]: held }, limit: 1 }))[0] ?? null)',
477
+ ' if (row === null) return anyone',
478
+ '',
479
+ ' return {',
480
+ ' ...anyone,',
481
+ ' id: String(row[address] ?? held),',
482
+ ' roles: worn(row[who.actor.roles]),',
483
+ ' entitlements: who.actor.entitlements === undefined ? [] : worn(row[who.actor.entitlements]),',
484
+ ' }',
485
+ '}',
486
+ '',
487
+ ]
488
+ : [
489
+ 'export const actor = (request: Request): Promise<Actor> => Promise.resolve({ ...ANONYMOUS, ...environment(request) })',
490
+ '',
491
+ ]),
492
+ ].join('\n')
493
+ }
494
+
495
+ const seeded = (): string =>
496
+ [
497
+ `import { DECLARED } from '${beside(SEAMS_AT, DECLARED_SEAMS_AT)}'`,
498
+ `import type { Seams } from '${runtime(SEAMS_AT, '../seams.ts')}'`,
499
+ '',
500
+ 'export const SEAMS: Seams = {',
501
+ ' ...DECLARED,',
502
+ '}',
503
+ '',
504
+ ].join('\n')
505
+
506
+ const width = Math.max(...Object.keys(HANDED).map((one) => one.length))
507
+
508
+ const handing = (stores: readonly string[], keeping: boolean, vaulted: boolean): string =>
509
+ [
510
+ `capabilities: seeded ${SEAMS_AT} — what this app hands its operations, and yours from here.`,
511
+ ` It hands on everything ${DECLARED_SEAMS_AT} settles from your declarations: rows go to ` +
512
+ (stores.length === 0 ? 'memory until a target keeps entities' : stores.join(', ')) +
513
+ `, who is asking is ${keeping ? 'read off the entity that names the caller' : 'nobody until an entity names the caller'}, ` +
514
+ `calls leave through http${vaulted ? ' with credentials opened by the vault' : ''}.`,
515
+ ' Replace a seam, or add one, by naming it in SEAMS. Every seam an operation can be handed:',
516
+ ...Object.entries(HANDED).map(([one, says]) => ` ${one.padEnd(width)} ${says}`),
517
+ '',
518
+ ].join('\n')
519
+
520
+ const served = (one: Capability): boolean => one.live.includes('api') && compiled(one.operation)
521
+
522
+ const held = (one: Capability): boolean => one.browsable && one.live.includes('api')
523
+
524
+ function dispatching(found: readonly Capability[]): string {
525
+
526
+ const reachable = found.filter(served)
527
+ return [
528
+ '/* GENERATED by thread — do not edit. It is derived from what this app declares. */',
529
+ '',
530
+ `declare module '${packageOf('ui')}/ui/checked' {`,
531
+ ...(reachable.length === 0
532
+ ? [' interface Dispatching {}']
533
+ : [' interface Dispatching {', ...reachable.map((one) => ` readonly ${one.operation}: true`), ' }']),
534
+ '}',
535
+ '',
536
+ 'interface Reaches {',
537
+ ' readonly operation: string',
538
+ " readonly method: 'GET' | 'POST'",
539
+ ' readonly path: string',
540
+ '}',
541
+ '',
542
+ `const REACHES: readonly Reaches[] = [${
543
+ reachable.length === 0
544
+ ? ''
545
+ : `\n${reachable
546
+ .map(
547
+ (one) =>
548
+ ` { operation: ${JSON.stringify(one.operation)}, method: ${JSON.stringify(one.api?.method)}, path: ${JSON.stringify(one.api?.path)} },`,
549
+ )
550
+ .join('\n')}\n`
551
+ }]`,
552
+ '',
553
+ 'export async function dispatchTo(operation: string, input?: Readonly<Record<string, unknown>>): Promise<unknown> {',
554
+ ' const one = REACHES.find((each) => each.operation === operation)',
555
+ ' if (one === undefined) {',
556
+ ' throw new Error(',
557
+ " `thread: nothing declares an operation called ${operation}, so there is nowhere to send this. ` +",
558
+ " `This app reaches: ${REACHES.map((each) => each.operation).join(', ') || '(nothing)'}`,",
559
+ ' )',
560
+ ' }',
561
+ ' const sent = await fetch(one.path, {',
562
+ ' method: one.method,',
563
+ " headers: { 'content-type': 'application/json' },",
564
+ " ...(one.method === 'GET' ? {} : { body: JSON.stringify(input ?? {}) }),",
565
+ ' })',
566
+ ' return await sent.json()',
567
+ '}',
568
+ '',
569
+ ].join('\n')
570
+ }
571
+
572
+ const installed = (): boolean => existsSync(resolve(ROOT, '_threads', 'ui.ts'))
573
+
574
+ const files = (found: readonly Capability[]): readonly Written[] => [
575
+ { at: CAPABILITIES_AT, text: printed(found) },
576
+ { at: OPENAPI_AT, text: printed(openapi(found)) },
577
+ { at: MCP_AT, text: printed(mcp(found)) },
578
+ { at: CLIENT_AT, text: client(found) },
579
+
580
+ ...(installed() ? [{ at: DISPATCHING_AT, text: dispatching(found) }] : []),
581
+ ...found.filter(served).map((one) => ({ at: routeAt(one.operation), text: route(one) })),
582
+ ]
583
+
584
+ if (invoked(import.meta.url)) {
585
+ const { found, refusals } = await derive()
586
+ for (const one of refusals) console.error(`capabilities: ${one}`)
587
+
588
+ const mark = (one: Capability): string =>
589
+ marked([one.operation, one.describes, one.requires.join(','), one.nodes.join(','), one.live.join(',')])
590
+
591
+ const words = wordsIn(['read'])
592
+ const verb = words[0]
593
+
594
+ if (verb === 'read') {
595
+ const which = words[1]
596
+ const one = found.find((each) => mark(each) === which)
597
+ if (one === undefined) {
598
+ console.error(
599
+ `capabilities: \`${which ?? '(nothing)'}\` is not a mark this tree issued. Run \`thread capabilities\` to see them — each is printed in brackets beside the capability it belongs to.`,
600
+ )
601
+ exit(1)
602
+ }
603
+ stdout.write(`${one.operation} — ${one.describes}\n`)
604
+ stdout.write(`requires ${one.requires.length === 0 ? 'nothing' : one.requires.join(', ')}\n`)
605
+ stdout.write(`nodes: ${one.nodes.length === 0 ? '(none)' : one.nodes.join(', ')}\n`)
606
+ stdout.write(
607
+ `live: ${SURFACES.map((each) => `${one.live.includes(each) ? '[on] ' : '[off]'} ${each}`).join(' ')}\n`,
608
+ )
609
+ stdout.write(
610
+ `${one.browsable ? 'may be called from a page' : 'server only — it reaches a target that needs a server secret'}\n`,
611
+ )
612
+ stdout.write(`${one.local ? 'runs where it is called, with nothing sent' : 'reaches a target over the network'}\n`)
613
+ stdout.write(`api: ${one.api.method} ${one.api.path}\n`)
614
+ stdout.write(`cli: ${one.cli.invocation}\n`)
615
+ stdout.write(`mcp: ${one.mcp.name} — ${one.mcp.description}\n`)
616
+ stdout.write('input:\n')
617
+ for (const line of printed(one.input).trimEnd().split('\n')) stdout.write(` ${line}\n`)
618
+ exit(0)
619
+ }
620
+
621
+ const checking = argv.includes('--check')
622
+ let adrift = 0
623
+
624
+ const keeping = existsSync(resolve(ROOT, ENTITIES_AT))
625
+
626
+ if (existsSync(resolve(ROOT, OPERATIONS_AT))) {
627
+ const stores = kept()
628
+ const vaulted = existsSync(resolve(ROOT, VAULT_AT))
629
+ const text = settled(keeping, stores, existsSync(resolve(ROOT, PERMITS_AT)), vaulted)
630
+ const at = resolve(ROOT, DECLARED_SEAMS_AT)
631
+ if ((existsSync(at) ? readFileSync(at, 'utf8') : '') !== text) {
632
+ if (checking) {
633
+ adrift += 1
634
+ console.error(
635
+ `capabilities: ${DECLARED_SEAMS_AT} is not what the declarations settle. Run \`thread capabilities\` and commit the result.`,
636
+ )
637
+ } else {
638
+ mkdirSync(dirname(at), { recursive: true })
639
+ writeFileSync(at, text, 'utf8')
640
+ console.log(`capabilities: wrote ${DECLARED_SEAMS_AT} — the seams your targets, entities, grants and secrets settle.`)
641
+ }
642
+ }
643
+
644
+ const seed = resolve(ROOT, SEAMS_AT)
645
+ if (!existsSync(seed)) {
646
+ if (checking) {
647
+ adrift += 1
648
+ console.error(
649
+ `capabilities: ${SEAMS_AT} is not there, and it is what hands every operation its seams. Run \`thread capabilities\` — it seeds the file once, and from then on it is yours.`,
650
+ )
651
+ } else {
652
+ mkdirSync(dirname(seed), { recursive: true })
653
+ writeFileSync(seed, seeded(), 'utf8')
654
+ stdout.write(handing(stores, keeping, vaulted))
655
+ }
656
+ }
657
+ }
658
+
659
+ if (found.length === 0 && refusals.length === 0) {
660
+ console.log('capabilities: this app declares no operation, so nothing is reachable through one yet.')
661
+ exit(adrift > 0 ? 1 : 0)
662
+ }
663
+
664
+ const written = files(found)
665
+
666
+ if (checking) {
667
+ const drifted = written.filter((one) => {
668
+ const path = resolve(ROOT, one.at)
669
+ return (existsSync(path) ? readFileSync(path, 'utf8') : '') !== one.text
670
+ })
671
+ for (const one of drifted) {
672
+ console.error(
673
+ `capabilities: ${one.at} is not what the operations derive. Run \`thread capabilities\` and commit the result.`,
674
+ )
675
+ }
676
+ const off = found.filter((one) => one.live.length < SURFACES.length)
677
+ console.log(
678
+ `capabilities: ${String(found.length)} operation(s) reachable, ${String(off.length)} with a surface switched off, ` +
679
+ `${String(drifted.length + adrift)} finding(s).`,
680
+ )
681
+ exit(drifted.length + adrift > 0 || refusals.length > 0 ? 1 : 0)
682
+ }
683
+
684
+ if (refusals.length > 0) {
685
+ console.error(
686
+ `capabilities: ${String(refusals.length)} refusal(s) above, so nothing was written`,
687
+ )
688
+ exit(1)
689
+ }
690
+
691
+ for (const one of written) {
692
+ const path = resolve(ROOT, one.at)
693
+ mkdirSync(dirname(path), { recursive: true })
694
+ writeFileSync(path, one.text)
695
+ }
696
+
697
+ const shipped = found.filter(held).length
698
+ stdout.write(
699
+ `capabilities: wrote ${written.map((one) => one.at).join(', ')} — ${String(found.length)} operation(s), ` +
700
+ `${String(shipped)} of them in the client bundle.\n`,
701
+ )
702
+ exit(0)
703
+ }
704
+