@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,348 @@
1
+ import { ANONYMOUS, ENVIRONMENT } from '../core/actor.ts'
2
+ import { errorDefForResult } from './error-results.ts'
3
+ import { DbConflict } from './errors/errors.ts'
4
+ import { quoted, runtime } from './generators/seed.ts'
5
+ import type { ResultCode } from './result.ts'
6
+ import { SUPPLY, type AuditOutcome } from './seams.ts'
7
+
8
+ export const DISPATCH_AT = 'operations/_dispatch.ts'
9
+
10
+ const identity = (code: ResultCode): { readonly code: string; readonly class: string } => {
11
+ const def = errorDefForResult(code)
12
+ return { code: def.code, class: def.class }
13
+ }
14
+
15
+ const CODES: readonly ResultCode[] = ['denied', 'invalid', 'rate_limited', 'feature_denied', 'quota_exceeded', 'error']
16
+
17
+ const REFUSAL: Readonly<Record<ResultCode, { readonly said: string; readonly outcome: AuditOutcome }>> = {
18
+ denied: { said: 'not allowed', outcome: 'denied' },
19
+ invalid: { said: 'invalid input', outcome: 'failed' },
20
+ rate_limited: { said: 'rate limited', outcome: 'rate_limited' },
21
+ feature_denied: { said: 'not in plan', outcome: 'feature_denied' },
22
+ quota_exceeded: { said: 'quota exceeded', outcome: 'quota_exceeded' },
23
+ error: { said: 'failed', outcome: 'failed' },
24
+ }
25
+
26
+ const printed = (value: unknown): string => JSON.stringify(value, null, 2)
27
+
28
+ const DEFS = Object.fromEntries(CODES.map((code) => [code, identity(code)]))
29
+
30
+ export const dispatched = (
31
+ reached: Readonly<
32
+ Record<
33
+ string,
34
+ {
35
+ readonly reach: string
36
+ readonly address: string | null
37
+ readonly sends: readonly { readonly secret: string; readonly header: string; readonly prefix: string }[]
38
+ readonly calls: Readonly<Record<string, { readonly method: string; readonly path: string }>>
39
+ }
40
+ >
41
+ >,
42
+ ): string => [
43
+ `import type { Actor } from ${quoted(runtime(DISPATCH_AT, '../../core/actor.ts'))}`,
44
+ `import type { Operation } from ${quoted(runtime(DISPATCH_AT, '../operations.ts'))}`,
45
+ `import type { FailureIdentity, Result, ResultCode } from ${quoted(runtime(DISPATCH_AT, '../result.ts'))}`,
46
+ `import type { AuditOutcome, Handed, LogLevel, QuotaHold, Seams } from ${quoted(runtime(DISPATCH_AT, '../seams.ts'))}`,
47
+ '',
48
+ `const ANONYMOUS: Actor = ${printed(ANONYMOUS)}`,
49
+ '',
50
+ `const ENVIRONMENT: readonly string[] = ${printed(ENVIRONMENT.map(([fact]) => fact))}`,
51
+ '',
52
+ `const REFUSAL: Readonly<Record<ResultCode, { readonly said: string; readonly outcome: AuditOutcome }>> = ${printed(REFUSAL)}`,
53
+ '',
54
+ `const DEFS = ${printed(DEFS)} as Readonly<Record<ResultCode, FailureIdentity>>`,
55
+ '',
56
+ `const CONFLICT = ${printed({ code: DbConflict.def.code, class: DbConflict.def.class })} as FailureIdentity`,
57
+ '',
58
+ `const SUPPLY: Readonly<Record<string, string>> = ${printed(SUPPLY)}`,
59
+ '',
60
+ 'type Reached = Readonly<Record<string, {',
61
+ ' readonly reach: string',
62
+ ' readonly address: string | null',
63
+ ' readonly sends: readonly { readonly secret: string; readonly header: string; readonly prefix: string }[]',
64
+ ' readonly calls: Readonly<Record<string, { readonly method: string; readonly path: string }>>',
65
+ '}>>',
66
+ '',
67
+ `const REACHED: Reached = ${printed(reached)}`,
68
+ '',
69
+ "const STOPPED = Symbol.for('thread.operation.stopped')",
70
+ '',
71
+ 'const ok = <O>(value: O): Result<O> => ({ ok: true, value })',
72
+ '',
73
+ 'const fail = (code: ResultCode, error: string, because?: string, identity?: FailureIdentity): Result<never> => ({',
74
+ ' ok: false,',
75
+ ' code,',
76
+ ' error,',
77
+ ' because,',
78
+ ' ...(identity === undefined ? {} : { errorCode: identity.code, errorClass: identity.class }),',
79
+ '}) as Result<never>',
80
+ '',
81
+ "const strings = (value: unknown): value is string[] => Array.isArray(value) && value.every((one) => typeof one === 'string')",
82
+ '',
83
+ 'const person = (value: unknown): Actor | undefined => {',
84
+ " if (typeof value !== 'object' || value === null) return undefined",
85
+ ' const one = value as Record<string, unknown>',
86
+ " const id = one['id']",
87
+ " if (!(id === null || typeof id === 'string')) return undefined",
88
+ " const roles = one['roles']",
89
+ " const entitlements = one['entitlements']",
90
+ ' if (!strings(roles) || !strings(entitlements)) return undefined',
91
+ ' const carried: Record<string, string> = {}',
92
+ ' for (const fact of ENVIRONMENT) {',
93
+ ' const held = one[fact]',
94
+ ' if (held === undefined) continue',
95
+ " if (typeof held !== 'string') return undefined",
96
+ ' carried[fact] = held',
97
+ ' }',
98
+ ' return { ...carried, id, roles, entitlements }',
99
+ '}',
100
+ '',
101
+ 'const named = (thrown: unknown, what: string): boolean => {',
102
+ " if (typeof thrown !== 'object' || thrown === null) return false",
103
+ ' const one = thrown as { readonly name?: unknown; readonly constructor?: { readonly name?: unknown } }',
104
+ ' return one.name === what || one.constructor?.name === what',
105
+ '}',
106
+ '',
107
+ 'const stopped = (thrown: unknown): { readonly value: unknown } | undefined => {',
108
+ " if (typeof thrown !== 'object' || thrown === null) return undefined",
109
+ ' const one = thrown as Record<PropertyKey, unknown>',
110
+ " return one[STOPPED] === true ? { value: one['value'] } : undefined",
111
+ '}',
112
+ '',
113
+ 'const marked = (thrown: unknown): string | undefined => {',
114
+ " if (typeof thrown !== 'object' || thrown === null) return undefined",
115
+ ' const said = (thrown as { readonly node?: unknown }).node',
116
+ " return typeof said === 'string' ? said : undefined",
117
+ '}',
118
+ '',
119
+ 'const platform = (thrown: unknown): string | undefined => {',
120
+ " if (typeof thrown !== 'object' || thrown === null) return undefined",
121
+ ' const said = (thrown as { readonly because?: unknown }).because',
122
+ " return typeof said === 'string' ? said : undefined",
123
+ '}',
124
+ '',
125
+ 'const filled = (path: string, fills: Readonly<Record<string, string | number>> | undefined): string => {',
126
+ ' let held = path',
127
+ ' for (const [name, value] of Object.entries(fills ?? {})) held = held.split(`{${name}}`).join(encodeURIComponent(String(value)))',
128
+ ' const left = /\\{([a-z][a-z0-9_]*)\\}/.exec(held)',
129
+ ' if (left !== null) throw new Error(`the path ${path} leaves ${left[0]} to be filled and nothing filled it`)',
130
+ ' return held',
131
+ '}',
132
+ '',
133
+ 'const bound = (word: string, wired: Reached[string], seams: Seams): Record<string, unknown> | string => {',
134
+ ' const names = Object.keys(wired.calls)',
135
+ ' if (names.length === 0) {',
136
+ ' return `\\`${word}\\` is required and takes no call, so there is nothing to hand a node. Declare one with \\`thread create target call\\``',
137
+ ' }',
138
+ ' if (seams.http === undefined) {',
139
+ ' return `\\`${word}\\` is reached over ${wired.reach} and the seams this dispatch was handed hold no \\`http\\` — wire one, or take \\`${word}\\` off that operation\'s row`',
140
+ ' }',
141
+ ' const wanted = [...(wired.address === null ? [] : [wired.address]), ...wired.sends.map((one) => one.secret)]',
142
+ ' if (wanted.length > 0 && seams.vault === undefined) {',
143
+ ' return `\\`${word}\\` needs ${wanted.join(\', \')} and the seams this dispatch was handed hold no \\`vault\\` to open them. A target is reached with what opens it or not at all`',
144
+ ' }',
145
+ ' const out: Record<string, unknown> = {}',
146
+ ' for (const name of names) {',
147
+ ' const call = wired.calls[name]',
148
+ ' if (call === undefined) continue',
149
+ ' out[name] = async (given?: { fills?: Readonly<Record<string, string | number>>; json?: unknown; body?: Uint8Array; contentType?: string }) => {',
150
+ ' const values = wanted.length === 0 ? {} : await seams.vault!.resolve(wanted)',
151
+ ' const headers: Record<string, string> = {}',
152
+ ' for (const one of wired.sends) headers[one.header] = `${one.prefix}${values[one.secret] ?? \'\'}`',
153
+ ' const body = given?.json === undefined ? given?.body : new TextEncoder().encode(JSON.stringify(given.json))',
154
+ ' return await seams.http!.call({',
155
+ ' target: word,',
156
+ ' address: wired.address === null ? \'\' : (values[wired.address] ?? \'\'),',
157
+ " method: call.method as 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE',",
158
+ ' path: filled(call.path, given?.fills),',
159
+ ' headers,',
160
+ ' ...(body === undefined ? {} : { body, contentType: given?.contentType ?? \'application/json\' }),',
161
+ ' })',
162
+ ' }',
163
+ ' }',
164
+ ' return out',
165
+ '}',
166
+ '',
167
+
168
+ 'const given = (operation: Operation<unknown, string>, actor: Actor, seams: Seams): Record<string, unknown> | string => {',
169
+ ' const held: Record<string, unknown> = { actor }',
170
+ ' for (const word of operation.requires) {',
171
+ ' const key = SUPPLY[word]',
172
+ ' if (key === undefined) {',
173
+ ' const wired = REACHED[word]',
174
+ ' if (wired === undefined) {',
175
+ ' return `\\`${operation.id}\\` requires \\`${word}\\`, and nothing here supplies it — what may be required is ${[...Object.keys(SUPPLY), ...Object.keys(REACHED)].join(\', \')}`',
176
+ ' }',
177
+ ' const reached = bound(word, wired, seams)',
178
+ " if (typeof reached === 'string') return reached",
179
+ ' held[word] = reached',
180
+ ' continue',
181
+ ' }',
182
+ ' const seam = (seams as unknown as Record<string, unknown>)[key]',
183
+ ' if (seam === undefined) {',
184
+ ' return `\\`${operation.id}\\` requires \\`${word}\\`, and the seams this dispatch was handed hold no \\`${key}\\` — wire one, or take \\`${word}\\` off that operation\'s row. An operation does not run short of what it declared`',
185
+ ' }',
186
+ ' held[key] = seam',
187
+ ' }',
188
+ ' return held',
189
+ '}',
190
+ '',
191
+ 'export async function dispatch<I, R extends string = string>(',
192
+ ' operation: Operation<I, R>,',
193
+ ' rawInput: unknown,',
194
+ ' asking: Actor,',
195
+ ' handed: Seams,',
196
+ '): Promise<Result<unknown>> {',
197
+ ' const seams = running(handed)',
198
+ ' const id = operation.id',
199
+ ' const who = person(asking)',
200
+ ' const run = who?.run ?? seams.random.uuid()',
201
+ ' const actor: Actor = { ...(who ?? ANONYMOUS), run }',
202
+ ' const span = seams.telemetry?.start(id, run, { ...actor })',
203
+ '',
204
+ ' const gates = { rate: seams.rateLimiter !== undefined, replay: seams.idempotency !== undefined, quota: seams.quotas !== undefined, telemetry: seams.telemetry !== undefined }',
205
+ '',
206
+ ' let hold: QuotaHold | null = null',
207
+ ' let granted: string | undefined',
208
+ '',
209
+ ' const sank = (what: string) => (thrown: unknown): void => {',
210
+ " seams.log?.write('error', id, actor, {",
211
+ ' [`${what}_failed`]: thrown instanceof Error ? thrown.message : String(thrown),',
212
+ ' })',
213
+ ' }',
214
+ '',
215
+ ' const settled = async (held: Result<unknown>, because?: string): Promise<Result<unknown>> => {',
216
+ " const outcome: AuditOutcome = held.ok ? 'ok' : REFUSAL[held.code].outcome",
217
+ ' const detail = because === undefined ? {} : { because }',
218
+ '',
219
+ ' const reserved = hold === null ? null : operation.estimate',
220
+ ' let consumed: number | null = null',
221
+ '',
222
+ ' if (hold !== null) {',
223
+ ' const settling = held.ok ? hold.commit(operation.estimate) : hold.release()',
224
+ ' consumed = held.ok ? operation.estimate : 0',
225
+ ' hold = null',
226
+ " await settling.catch(sank('quota'))",
227
+ ' }',
228
+ '',
229
+ " seams.log.write(held.ok ? 'info' : 'warn', id, actor, {",
230
+ ' outcome,',
231
+ ' ...detail,',
232
+ " access: { roles: actor.roles, entitlements: actor.entitlements, granted: granted ?? 'not reached' },",
233
+ ' quota: { meter: id, reserved, consumed },',
234
+ ' gates,',
235
+ ' })',
236
+ ' if (held.ok) span?.ok()',
237
+ ' else span?.error(outcome)',
238
+ '',
239
+ ' await seams.audit',
240
+ " .append(actor, { action: id, outcome, subject: { type: 'operation', id }, ...detail })",
241
+ " .catch(sank('audit'))",
242
+ '',
243
+ ' if (!held.ok) {',
244
+ ' const identity: FailureIdentity =',
245
+ ' held.errorCode === undefined || held.errorClass === undefined',
246
+ ' ? DEFS[held.code]',
247
+ ' : { code: held.errorCode, class: held.errorClass }',
248
+ '',
249
+ ' await seams.errors',
250
+ ' .record(actor, { code: identity.code, class: identity.class, action: id, ...detail })',
251
+ " .catch(sank('errors'))",
252
+ ' }',
253
+ '',
254
+ ' return held',
255
+ ' }',
256
+ '',
257
+ ' const refused = (code: ResultCode, because?: string): Promise<Result<unknown>> =>',
258
+ ' settled(fail(code, REFUSAL[code].said, because), because)',
259
+ '',
260
+ ' const recorded = (began: number, ok: boolean, at: string | undefined, because?: string): void => {',
261
+ ' const took = seams.clock.now() - began',
262
+ ' const sections = {',
263
+ ' node: { at: at ?? null },',
264
+ ' audit: { actor, when: began },',
265
+ ' observability: { ms: took, ok, ...(because === undefined ? {} : { because }) },',
266
+ ' }',
267
+ '',
268
+ " seams.log.write(ok ? 'info' : 'warn', id, actor, sections)",
269
+ ' seams.telemetry?.record?.({',
270
+ ' key: id,',
271
+ ' traceId: run,',
272
+ ' spanId: run,',
273
+ ' durationMs: took,',
274
+ ' ok,',
275
+ ' attrs: sections,',
276
+ ' actor,',
277
+ ' ...(at === undefined ? {} : { nodeId: at }),',
278
+ ' workflowId: id,',
279
+ ' ...(because === undefined ? {} : { error: because }),',
280
+ ' })',
281
+ ' }',
282
+ '',
283
+ ' try {',
284
+ " if (who === undefined) return await refused('denied', 'identity_invalid')",
285
+ '',
286
+ " if (seams.rateLimiter !== undefined && !(await seams.rateLimiter.check(actor, id))) return await refused('rate_limited')",
287
+ '',
288
+ ' const replayed = seams.idempotency === undefined ? null : await seams.idempotency.check(actor.replay, id)',
289
+ " if (replayed !== null && replayed !== undefined) return await settled(ok(replayed), 'replayed')",
290
+ '',
291
+ ' const parsed = operation.input.safeParse(rawInput)',
292
+ ' if (!parsed.success) {',
293
+ " return await refused('invalid', parsed.error.issues.map((one) => one.message).join('; '))",
294
+ ' }',
295
+ '',
296
+ ' if (seams.quotas) {',
297
+ ' const reserved = await seams.quotas.reserve(actor, id, operation.estimate)',
298
+ " if (!reserved.ok) return await refused('quota_exceeded', reserved.behavior ?? 'block')",
299
+ ' hold = reserved',
300
+ ' }',
301
+ '',
302
+ ' if (seams.permits === undefined) {',
303
+ " return await refused('denied', 'no permits check is wired, so nothing can permit this operation')",
304
+ ' }',
305
+ '',
306
+ ' const ruled = await seams.permits.check(actor, id, operation.resource(parsed.data))',
307
+ " if (!ruled.allow) return await refused('denied', ruled.because)",
308
+ ' granted = ruled.because',
309
+ '',
310
+ ' const hands = given(operation as Operation<unknown, string>, actor, seams)',
311
+ " if (typeof hands === 'string') return await refused('error', hands)",
312
+ '',
313
+ ' const held: { readonly input: I } & Record<string, unknown> = { input: parsed.data }',
314
+ ' const began = seams.clock.now()',
315
+ ' let value: unknown',
316
+ ' try {',
317
+ ' value = await operation.run(hands as Handed<R>, held)',
318
+ ' } catch (thrown) {',
319
+ ' recorded(began, stopped(thrown) !== undefined, marked(thrown), thrown instanceof Error ? thrown.message : String(thrown))',
320
+ ' throw thrown',
321
+ ' }',
322
+ ' recorded(began, true, undefined)',
323
+ '',
324
+ " if (seams.idempotency !== undefined) await seams.idempotency.store(actor.replay, id, value).catch(sank('idempotency'))",
325
+ '',
326
+ ' return await settled(ok(value))',
327
+ ' } catch (thrown) {',
328
+ ' const early = stopped(thrown)',
329
+ ' if (early !== undefined) {',
330
+ " if (seams.idempotency !== undefined) await seams.idempotency.store(actor.replay, id, early.value).catch(sank('idempotency'))",
331
+ " return await settled(ok(early.value), 'stopped')",
332
+ ' }',
333
+ '',
334
+ ' const message = thrown instanceof Error ? thrown.message : String(thrown)',
335
+ '',
336
+ " if (named(thrown, 'VersionConflictError')) {",
337
+ " return await settled(fail('invalid', message, 'version_conflict', CONFLICT), 'version_conflict')",
338
+ ' }',
339
+ '',
340
+ " const code: ResultCode = named(thrown, 'ValidationError') ? 'invalid' : 'error'",
341
+ ' const because = platform(thrown) ?? message',
342
+ ' return await settled(fail(code, message, because), because)',
343
+ ' } finally {',
344
+ ' span?.end()',
345
+ ' }',
346
+ '}',
347
+ '',
348
+ ].join('\n')
@@ -0,0 +1,31 @@
1
+ import { PlatformError, catalog, type ErrorDef } from './errors/catalog.ts'
2
+ import { AuthDenied, FeatureNotInPlan, InternalInvalidArgument, InternalUnexpected, QuotaExceeded, RateLimitExceeded } from './errors/errors.ts'
3
+
4
+ import type { Result, ResultCode } from './result.ts'
5
+
6
+ const RESULT_ERROR_DEFS: Readonly<Record<ResultCode, ErrorDef>> = {
7
+ invalid: InternalInvalidArgument.def,
8
+ denied: AuthDenied.def,
9
+ rate_limited: RateLimitExceeded.def,
10
+ quota_exceeded: QuotaExceeded.def,
11
+ feature_denied: FeatureNotInPlan.def,
12
+ error: InternalUnexpected.def,
13
+ }
14
+
15
+ export function errorDefForResult(code: ResultCode): ErrorDef {
16
+ return RESULT_ERROR_DEFS[code]
17
+ }
18
+
19
+ export function platformErrorForResult(result: Result<unknown>, run: string): PlatformError {
20
+ if (result.ok) throw new TypeError('cannot render a successful Result as a problem')
21
+
22
+ const def = result.errorCode === undefined ? errorDefForResult(result.code) : catalog.get(result.errorCode)
23
+ const resolved = def ?? InternalUnexpected.def
24
+ const detail = result.errorCode === undefined && resolved.code === InternalUnexpected.def.code ? undefined : result.error
25
+
26
+ return new PlatformError(resolved, {
27
+ ...(detail === undefined ? {} : { detail }),
28
+ ...(result.because === undefined ? {} : { because: result.because }),
29
+ traceId: run,
30
+ })
31
+ }
@@ -0,0 +1,168 @@
1
+
2
+
3
+ import { classDef, type ErrorClass } from './classes.ts'
4
+
5
+ export const NAMESPACES = [
6
+ 'auth',
7
+ 'quota',
8
+ 'rate_limit',
9
+ 'feature',
10
+ 'db',
11
+ 'workflow',
12
+ 'queue',
13
+ 'ai',
14
+ 'search',
15
+ 'comms',
16
+ 'config',
17
+ 'net',
18
+ 'internal',
19
+ ] as const
20
+
21
+ export type Namespace = (typeof NAMESPACES)[number]
22
+
23
+ export type ErrorCode = `${Namespace}.${string}`
24
+
25
+ export interface ErrorDef<C extends ErrorCode = ErrorCode> {
26
+ readonly code: C
27
+ readonly class: ErrorClass
28
+ readonly status: number
29
+
30
+ readonly titleKey: string
31
+ readonly retryable: boolean
32
+ }
33
+
34
+ export interface DefineErrorInput<C extends ErrorCode = ErrorCode> {
35
+ readonly code: C
36
+ readonly class: ErrorClass
37
+
38
+ readonly status?: number
39
+
40
+ readonly retryable?: boolean
41
+
42
+ readonly titleKey?: string
43
+ }
44
+
45
+ export interface PlatformErrorInit {
46
+
47
+ readonly detail?: string
48
+
49
+ readonly instance?: string
50
+
51
+ readonly because?: string
52
+ readonly traceId?: string
53
+
54
+ readonly retryAfter?: number
55
+ readonly cause?: unknown
56
+
57
+ readonly extensions?: Readonly<Record<string, unknown>>
58
+ }
59
+
60
+ export class PlatformError<C extends ErrorCode = ErrorCode> extends Error {
61
+ readonly def: ErrorDef<C>
62
+ readonly detail?: string
63
+ readonly instance?: string
64
+ readonly because?: string
65
+ readonly traceId?: string
66
+ readonly retryAfter?: number
67
+ readonly extensions: Readonly<Record<string, unknown>>
68
+
69
+ constructor(def: ErrorDef<C>, init: PlatformErrorInit = {}) {
70
+
71
+ super(def.code, init.cause === undefined ? undefined : { cause: init.cause })
72
+ this.name = 'PlatformError'
73
+ this.def = def
74
+ this.detail = init.detail
75
+ this.instance = init.instance
76
+ this.because = init.because
77
+ this.traceId = init.traceId
78
+ this.retryAfter = init.retryAfter
79
+ this.extensions = init.extensions ?? {}
80
+ }
81
+
82
+ get code(): C {
83
+ return this.def.code
84
+ }
85
+
86
+ get status(): number {
87
+ return this.def.status
88
+ }
89
+
90
+ get retryable(): boolean {
91
+ return this.def.retryable
92
+ }
93
+ }
94
+
95
+ export function isPlatformError(value: unknown): value is PlatformError {
96
+ return value instanceof PlatformError
97
+ }
98
+
99
+ export type ErrorFactory<C extends ErrorCode = ErrorCode> = ((
100
+ init?: PlatformErrorInit,
101
+ ) => PlatformError<C>) & {
102
+ readonly def: ErrorDef<C>
103
+
104
+ readonly is: (value: unknown) => value is PlatformError<C>
105
+ }
106
+
107
+ export class DuplicateErrorCodeError extends Error {
108
+ constructor(code: string) {
109
+ super(`duplicate error code: ${code}`)
110
+ this.name = 'DuplicateErrorCodeError'
111
+ }
112
+ }
113
+
114
+ export class ErrorCatalog {
115
+ readonly #byCode = new Map<ErrorCode, ErrorDef>()
116
+
117
+ define<const C extends ErrorCode>(input: DefineErrorInput<C>): ErrorFactory<C> {
118
+ if (this.#byCode.has(input.code)) {
119
+
120
+ throw new DuplicateErrorCodeError(input.code)
121
+ }
122
+
123
+ const cls = classDef(input.class)
124
+ const def: ErrorDef<C> = {
125
+ code: input.code,
126
+ class: input.class,
127
+ status: input.status ?? cls.status,
128
+ retryable: input.retryable ?? cls.retryable,
129
+ titleKey: input.titleKey ?? `errors.${input.code}.title`,
130
+ }
131
+ this.#byCode.set(def.code, def)
132
+
133
+ const factory = ((init?: PlatformErrorInit) => new PlatformError(def, init)) as {
134
+ (init?: PlatformErrorInit): PlatformError<C>
135
+ def: ErrorDef<C>
136
+ is: (value: unknown) => value is PlatformError<C>
137
+ }
138
+ factory.def = def
139
+ factory.is = (value: unknown): value is PlatformError<C> =>
140
+ isPlatformError(value) && value.def.code === def.code
141
+
142
+ return factory as ErrorFactory<C>
143
+ }
144
+
145
+ get(code: string): ErrorDef | undefined {
146
+ return this.#byCode.get(code as ErrorCode)
147
+ }
148
+
149
+ has(code: string): boolean {
150
+ return this.#byCode.has(code as ErrorCode)
151
+ }
152
+
153
+ all(): readonly ErrorDef[] {
154
+ return [...this.#byCode.values()].sort((a, b) => a.code.localeCompare(b.code))
155
+ }
156
+
157
+ get size(): number {
158
+ return this.#byCode.size
159
+ }
160
+ }
161
+
162
+ export const catalog = new ErrorCatalog()
163
+
164
+ export function defineError<const C extends ErrorCode>(
165
+ input: DefineErrorInput<C>,
166
+ ): ErrorFactory<C> {
167
+ return catalog.define(input)
168
+ }
@@ -0,0 +1,51 @@
1
+
2
+
3
+ export interface ErrorClassDef {
4
+
5
+ readonly status: number
6
+
7
+ readonly retryable: boolean
8
+ }
9
+
10
+ export const ERROR_CLASSES = {
11
+
12
+ OK: { status: 200, retryable: false },
13
+
14
+ CANCELLED: { status: 499, retryable: false },
15
+
16
+ UNKNOWN: { status: 500, retryable: false },
17
+
18
+ INVALID_ARGUMENT: { status: 400, retryable: false },
19
+
20
+ DEADLINE_EXCEEDED: { status: 504, retryable: true },
21
+
22
+ NOT_FOUND: { status: 404, retryable: false },
23
+
24
+ ALREADY_EXISTS: { status: 409, retryable: false },
25
+
26
+ PERMISSION_DENIED: { status: 403, retryable: false },
27
+
28
+ RESOURCE_EXHAUSTED: { status: 429, retryable: true },
29
+
30
+ FAILED_PRECONDITION: { status: 400, retryable: false },
31
+
32
+ ABORTED: { status: 409, retryable: true },
33
+
34
+ OUT_OF_RANGE: { status: 400, retryable: false },
35
+
36
+ UNIMPLEMENTED: { status: 501, retryable: false },
37
+
38
+ INTERNAL: { status: 500, retryable: false },
39
+
40
+ UNAVAILABLE: { status: 503, retryable: true },
41
+
42
+ UNAUTHENTICATED: { status: 401, retryable: false },
43
+ } as const satisfies Record<string, ErrorClassDef>
44
+
45
+ export type ErrorClass = keyof typeof ERROR_CLASSES
46
+
47
+ export const ERROR_CLASS_NAMES = Object.keys(ERROR_CLASSES) as readonly ErrorClass[]
48
+
49
+ export function classDef(cls: ErrorClass): ErrorClassDef {
50
+ return ERROR_CLASSES[cls]
51
+ }