@ontrails/core 0.2.0

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 (86) hide show
  1. package/CHANGELOG.md +849 -0
  2. package/README.md +190 -0
  3. package/package.json +36 -0
  4. package/src/activation-provenance.ts +116 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-derivation.ts +227 -0
  7. package/src/activation-source.ts +93 -0
  8. package/src/blob-ref.ts +90 -0
  9. package/src/branded.ts +135 -0
  10. package/src/collections.ts +99 -0
  11. package/src/compose-batch.ts +69 -0
  12. package/src/compose-schema.ts +36 -0
  13. package/src/context.ts +66 -0
  14. package/src/derive.ts +485 -0
  15. package/src/detours.ts +8 -0
  16. package/src/diagnostics.ts +21 -0
  17. package/src/draft.ts +350 -0
  18. package/src/entity.ts +346 -0
  19. package/src/error-rendering.ts +87 -0
  20. package/src/errors.ts +483 -0
  21. package/src/execute.ts +1577 -0
  22. package/src/fetch.ts +138 -0
  23. package/src/fire.ts +1172 -0
  24. package/src/glob.ts +81 -0
  25. package/src/guards.ts +37 -0
  26. package/src/index.ts +704 -0
  27. package/src/internal/fork-ctx.ts +69 -0
  28. package/src/layer-field-rendering.ts +193 -0
  29. package/src/layer.ts +81 -0
  30. package/src/observe.ts +361 -0
  31. package/src/path-scope.ts +66 -0
  32. package/src/path-security.ts +98 -0
  33. package/src/patterns/bulk.ts +16 -0
  34. package/src/patterns/change.ts +12 -0
  35. package/src/patterns/date-range.ts +12 -0
  36. package/src/patterns/index.ts +8 -0
  37. package/src/patterns/pagination.ts +22 -0
  38. package/src/patterns/progress.ts +13 -0
  39. package/src/patterns/sorting.ts +14 -0
  40. package/src/patterns/status.ts +11 -0
  41. package/src/patterns/timestamps.ts +12 -0
  42. package/src/permits.ts +12 -0
  43. package/src/queue.ts +163 -0
  44. package/src/redaction/index.ts +3 -0
  45. package/src/redaction/patterns.ts +50 -0
  46. package/src/redaction/redactor.ts +178 -0
  47. package/src/resilience.ts +234 -0
  48. package/src/resource-config.ts +804 -0
  49. package/src/resource.ts +194 -0
  50. package/src/result.ts +212 -0
  51. package/src/run.ts +76 -0
  52. package/src/runtime-builtins.ts +69 -0
  53. package/src/schedule-runtime.ts +689 -0
  54. package/src/schedule.ts +326 -0
  55. package/src/serialization.ts +265 -0
  56. package/src/sha256.ts +136 -0
  57. package/src/signal-diagnostics.ts +633 -0
  58. package/src/signal-ref.ts +111 -0
  59. package/src/signal.ts +104 -0
  60. package/src/store/accessor-protocol.ts +56 -0
  61. package/src/store/index.ts +4 -0
  62. package/src/structured-examples.ts +248 -0
  63. package/src/surface-derivation.ts +91 -0
  64. package/src/surface-filter.ts +101 -0
  65. package/src/surface-overlay.ts +694 -0
  66. package/src/surface-versioning.ts +42 -0
  67. package/src/topo.ts +835 -0
  68. package/src/tracing.ts +346 -0
  69. package/src/trail-id-glob.ts +15 -0
  70. package/src/trail.ts +1351 -0
  71. package/src/trails/derive-trail.ts +835 -0
  72. package/src/trails/index.ts +9 -0
  73. package/src/trails/ingest.ts +152 -0
  74. package/src/trails-db.ts +212 -0
  75. package/src/transport-error-map.ts +163 -0
  76. package/src/type-utils.ts +87 -0
  77. package/src/types.ts +300 -0
  78. package/src/validate-established-topo.ts +73 -0
  79. package/src/validate-topo.ts +725 -0
  80. package/src/validation.ts +330 -0
  81. package/src/version-marker.ts +716 -0
  82. package/src/version-resolution.ts +308 -0
  83. package/src/version-runtime.ts +120 -0
  84. package/src/webhook.ts +461 -0
  85. package/src/workspace.ts +244 -0
  86. package/src/zod-wrappers.ts +72 -0
package/src/types.ts ADDED
@@ -0,0 +1,300 @@
1
+ import type { TrailsError } from './errors.js';
2
+ import type { BasePermit } from './permits.js';
3
+ import type { Result } from './result.js';
4
+ import type { Signal } from './signal.js';
5
+ import type { AnyTrail } from './trail.js';
6
+ import type { ComposeInput, TrailOutput } from './type-utils.js';
7
+ import type { ActivationProvenance } from './activation-provenance.js';
8
+ import type { TrailVersionReference } from './version-resolution.js';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Detour
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /** A recovery path that activates when a trail's implementation fails with a matching error. */
15
+ export interface Detour<Input, Output, TErr extends TrailsError = TrailsError> {
16
+ /* oxlint-disable-next-line no-explicit-any -- standard pattern for matching abstract+concrete class constructors */
17
+ readonly on: abstract new (...args: any[]) => TErr;
18
+ readonly maxAttempts?: number | undefined;
19
+ readonly recover: (
20
+ attempt: DetourAttempt<Input, TErr>,
21
+ ctx: TrailContext
22
+ ) => Promise<Result<Output, TrailsError>>;
23
+ }
24
+
25
+ /** Context passed to a detour's recover function on each attempt. */
26
+ export interface DetourAttempt<Input, TErr extends TrailsError = TrailsError> {
27
+ /** 1-indexed attempt number */
28
+ readonly attempt: number;
29
+ /** The matched error */
30
+ readonly error: TErr;
31
+ /** Original trail input */
32
+ readonly input: Input;
33
+ }
34
+
35
+ type ComposeBatchCall<TTarget extends AnyTrail | string = AnyTrail | string> =
36
+ TTarget extends AnyTrail
37
+ ? readonly [trail: TTarget, input: ComposeInput<TTarget>]
38
+ : readonly [id: string, input: unknown];
39
+
40
+ type ComposeBatchResult<TTarget extends AnyTrail | string> =
41
+ TTarget extends AnyTrail
42
+ ? Result<TrailOutput<TTarget>, Error>
43
+ : Result<unknown, Error>;
44
+
45
+ type ComposeBatchResults<TCalls extends readonly ComposeBatchCall[]> = {
46
+ readonly [K in keyof TCalls]: TCalls[K] extends readonly [
47
+ infer TTarget,
48
+ unknown,
49
+ ]
50
+ ? TTarget extends AnyTrail | string
51
+ ? ComposeBatchResult<TTarget>
52
+ : never
53
+ : never;
54
+ };
55
+
56
+ /** Runtime options for batch `ctx.compose([...])` calls. */
57
+ export interface ComposeBatchOptions {
58
+ /**
59
+ * Maximum number of branches to execute concurrently.
60
+ *
61
+ * Omit for unbounded concurrency. `1` is equivalent to sequential execution.
62
+ */
63
+ readonly concurrency?: number | undefined;
64
+ }
65
+
66
+ /** Runtime options for a single `ctx.compose(trail, input, options)` call. */
67
+ export interface ComposeOptions {
68
+ /**
69
+ * Execute a specific live version of the composed trail.
70
+ *
71
+ * Omit to keep composition current by default. Historical revision entries
72
+ * transpose through the current trail; fork entries run their own implementation.
73
+ */
74
+ readonly version?: TrailVersionReference | undefined;
75
+ }
76
+
77
+ /**
78
+ * Trail implementation — sync or async.
79
+ *
80
+ * Authors can return `Result` directly or wrap it in a `Promise`. The framework
81
+ * normalizes with `await` at every call site, so both forms work transparently.
82
+ */
83
+ export type Implementation<I, O, Ctx extends TrailContext = TrailContext> = (
84
+ input: I,
85
+ ctx: Ctx
86
+ ) => Result<O, Error> | Promise<Result<O, Error>>;
87
+
88
+ /**
89
+ * Invoke another trail — used for trail composition.
90
+ *
91
+ * Two call shapes:
92
+ *
93
+ * - **By trail object** (typed): `ctx.compose(showGist, { id })` — the compiler
94
+ * infers `I` and `O` from the trail's schemas, so the result is fully typed.
95
+ * - **By string id** (untyped escape hatch): `ctx.compose('gist.show', { id })`
96
+ * — returns `Result<O, Error>` where `O` defaults to `unknown`.
97
+ * - **By batch**: `ctx.compose([[showGist, { id }], ['audit.log', payload]])`
98
+ * — executes every composing concurrently and resolves once all results are
99
+ * available. Result ordering always matches the input tuple ordering. Pass
100
+ * `{ concurrency: N }` as the second argument to limit how many branches
101
+ * run at once.
102
+ */
103
+ export interface ComposeFn {
104
+ <const TCalls extends readonly ComposeBatchCall[]>(
105
+ calls: TCalls,
106
+ options?: ComposeBatchOptions
107
+ ): Promise<ComposeBatchResults<TCalls>>;
108
+ <T extends AnyTrail>(
109
+ trail: T,
110
+ input: ComposeInput<T>,
111
+ options?: ComposeOptions
112
+ ): Promise<Result<TrailOutput<T>, Error>>;
113
+ <O = unknown>(
114
+ id: string,
115
+ input: unknown,
116
+ options?: ComposeOptions
117
+ ): Promise<Result<O, Error>>;
118
+ }
119
+
120
+ /**
121
+ * Emit a signal — used for signal-driven activation.
122
+ *
123
+ * Fan-out to consumer trails (those with the signal in their `on:` array) is
124
+ * the framework's responsibility. Producers call with a `Signal<T>` value and
125
+ * get best-effort `Promise<void>` semantics: payload validation, missing topo
126
+ * entries, guard suppression, and consumer failures are observable through
127
+ * diagnostics/logging but do not become producer-facing `Result` plumbing.
128
+ * Consumers fan out in parallel, each with its own derived context. Runtime
129
+ * cycle suppression is still signal-id-based against the current fire stack:
130
+ * it prevents re-entrant loops but can over-suppress legitimate diamond
131
+ * re-fires, with a debug breadcrumb and a warn emitted when suppression
132
+ * happens.
133
+ */
134
+ export type FireFn = <T>(signal: Signal<T>, payload: T) => Promise<void>;
135
+
136
+ /** Resolve a resource instance from the current trail context. */
137
+ export type ResourceLookup = <T = unknown>(
138
+ resourceOrId: { readonly id: string } | string
139
+ ) => T;
140
+
141
+ /**
142
+ * Wrap the execution of `fn` in a child trace span.
143
+ *
144
+ * Creates a nested span under the current trail's root trace record, times
145
+ * the callback, records success or failure (including error category), and
146
+ * writes the completed span to the registered sink. Errors thrown by `fn`
147
+ * are recorded on the span and then rethrown — tracing never swallows them.
148
+ */
149
+ export type TraceFn = <T>(
150
+ label: string,
151
+ fn: () => T | Promise<T>
152
+ ) => Promise<T>;
153
+
154
+ /** Callback for reporting progress from long-running trails */
155
+ export type ProgressCallback = (event: ProgressEvent) => void;
156
+
157
+ /** Structured progress event emitted during trail execution */
158
+ export interface ProgressEvent {
159
+ readonly type: 'start' | 'progress' | 'complete' | 'error';
160
+ readonly current?: number | undefined;
161
+ readonly total?: number | undefined;
162
+ readonly message?: string | undefined;
163
+ readonly ts: string;
164
+ }
165
+
166
+ /** Minimal logger interface — implementations can bridge to any logging library */
167
+ export interface Logger {
168
+ readonly name?: string | undefined;
169
+ trace(message: string, data?: Record<string, unknown>): void;
170
+ debug(message: string, data?: Record<string, unknown>): void;
171
+ info(message: string, data?: Record<string, unknown>): void;
172
+ warn(message: string, data?: Record<string, unknown>): void;
173
+ error(message: string, data?: Record<string, unknown>): void;
174
+ fatal(message: string, data?: Record<string, unknown>): void;
175
+ child(context: Record<string, unknown>): Logger;
176
+ }
177
+
178
+ export type LogLevel =
179
+ | 'debug'
180
+ | 'error'
181
+ | 'fatal'
182
+ | 'info'
183
+ | 'silent'
184
+ | 'trace'
185
+ | 'warn';
186
+
187
+ export interface LogRecord {
188
+ readonly category: string;
189
+ readonly level: LogLevel;
190
+ readonly message: string;
191
+ readonly metadata: Record<string, unknown>;
192
+ readonly timestamp: Date;
193
+ }
194
+
195
+ export interface LogSink {
196
+ readonly name: string;
197
+ readonly write: (record: LogRecord) => void;
198
+ readonly flush?: (() => Promise<void>) | undefined;
199
+ }
200
+
201
+ export interface LogFormatter {
202
+ format(record: LogRecord): string;
203
+ }
204
+
205
+ /**
206
+ * Context extension key for the invoking surface name.
207
+ */
208
+ export const SURFACE_KEY = '__trails_surface' as const;
209
+
210
+ /**
211
+ * Context extension key for the layer names attached by the invoking surface.
212
+ */
213
+ export const SURFACE_LAYER_NAMES_KEY = '__trails_surface_layer_names' as const;
214
+
215
+ /**
216
+ * Context extension key carrying per-layer runtime input.
217
+ *
218
+ * Surfaces (CLI, MCP, HTTP) render each typed layer's `input` schema onto
219
+ * their native idioms (flags, tool params, query strings). At execute time
220
+ * the parsed values are partitioned per layer and stored under this key as
221
+ * `Record<layerName, unknown>`. Layers that need runtime input read their
222
+ * own slot via `ctx.extensions?.[LAYER_INPUTS_KEY]?.[layer.name]`.
223
+ *
224
+ * @see TRL-473 for the CLI derivation contract.
225
+ */
226
+ export const LAYER_INPUTS_KEY = '__trails_layer_inputs' as const;
227
+
228
+ /** Runtime context threaded through every trail execution */
229
+ export interface TrailContext {
230
+ readonly activation?: ActivationProvenance | undefined;
231
+ readonly requestId: string;
232
+ readonly abortSignal: AbortSignal;
233
+ readonly compose?: ComposeFn | undefined;
234
+ /**
235
+ * Emit a typed signal. Fans out to every trail with the signal in its
236
+ * `on:` declaration. Bound by the runner that holds the topo (typically
237
+ * `run()`); undefined when a context is constructed without topo access.
238
+ */
239
+ readonly fire?: FireFn | undefined;
240
+ readonly permit?: BasePermit;
241
+ readonly workspaceRoot?: string | undefined;
242
+ readonly logger?: Logger | undefined;
243
+ readonly progress?: ProgressCallback | undefined;
244
+ readonly cwd?: string | undefined;
245
+ readonly env?: Record<string, string | undefined> | undefined;
246
+ readonly extensions?: Readonly<Record<string, unknown>> | undefined;
247
+ readonly resource?: ResourceLookup | undefined;
248
+ /**
249
+ * Whether the current invocation is a dry run.
250
+ *
251
+ * Defaults to `false`. Trails that don't read this field are unaffected.
252
+ * Trails that do read it decide what dry-run means for their domain — for
253
+ * example: preview the change without committing, validate inputs without
254
+ * performing side effects, or return what would happen without actually
255
+ * doing it.
256
+ *
257
+ * The framework only carries the flag from the surface (e.g. CLI
258
+ * `--dry-run`) into the context. It never short-circuits trail execution
259
+ * on its own based on this field.
260
+ *
261
+ * Pair this runtime signal with `TrailSpec.dryRun`, which declares whether a
262
+ * trail supports dry-run semantics for governance, derivation, and surface
263
+ * tooling.
264
+ *
265
+ * @remarks Always defined on contexts produced by `executeTrail` or
266
+ * `createTrailContext` (normalized to `false` when not provided).
267
+ */
268
+ readonly dryRun?: boolean | undefined;
269
+ /**
270
+ * Wrap a callback in a child trace span.
271
+ *
272
+ * Always present on contexts produced by `executeTrail` or
273
+ * `createTrailContext`. Optional on the interface so manually constructed
274
+ * contexts (tests, ad-hoc compositions) don't have to supply one — call
275
+ * sites tolerate `undefined` by falling back to a no-op passthrough.
276
+ */
277
+ readonly trace?: TraceFn | undefined;
278
+ }
279
+
280
+ /** Trail context for implementations that declare trail composition. */
281
+ export interface ComposeTrailContext extends TrailContext {
282
+ readonly compose: ComposeFn;
283
+ }
284
+
285
+ /**
286
+ * Permit requirement declared on a trail spec.
287
+ *
288
+ * A scopes object means the trail requires a permit with those scopes.
289
+ * `'public'` means the trail has explicitly opted out of auth.
290
+ * Omitting the field entirely means the trail hasn't declared an auth posture.
291
+ */
292
+ export type PermitRequirement =
293
+ | { readonly scopes: readonly string[] }
294
+ | 'public';
295
+
296
+ /** Input shape used to seed a runtime TrailContext before resolution. */
297
+ export type TrailContextInit = Omit<TrailContext, 'resource' | 'trace'> & {
298
+ readonly resource?: ResourceLookup | undefined;
299
+ readonly trace?: TraceFn | undefined;
300
+ };
@@ -0,0 +1,73 @@
1
+ import { ValidationError } from './errors.js';
2
+ import { Result } from './result.js';
3
+ import type { Topo } from './topo.js';
4
+ import { validateDraftFreeTopo } from './draft.js';
5
+ import type { TopoDiagnostic } from './validate-topo.js';
6
+ import { validateTopo } from './validate-topo.js';
7
+
8
+ const DERIVATION_BLOCKING_RULES = new Set([
9
+ 'compose-cycle',
10
+ 'compose-exists',
11
+ 'no-self-compose',
12
+ 'activation-source-definition-unique',
13
+ 'activation-source-edge-unique',
14
+ 'activation-source-kind-known',
15
+ 'activation-queue-valid',
16
+ 'activation-schedule-valid',
17
+ 'resource-exists',
18
+ 'signal-fire-exists',
19
+ 'signal-on-exists',
20
+ 'signal-origin-exists',
21
+ ]);
22
+
23
+ const isDerivationBlockingIssue = (issue: TopoDiagnostic): boolean =>
24
+ DERIVATION_BLOCKING_RULES.has(issue.rule) ||
25
+ (issue.rule === 'activation-source-input-compatible' &&
26
+ issue.sourceKind === 'queue');
27
+
28
+ const keepDerivationBlockingIssues = (
29
+ result: ReturnType<typeof validateTopo>
30
+ ) => {
31
+ if (result.isOk()) {
32
+ return result;
33
+ }
34
+
35
+ const issues = (
36
+ result.error.context as { issues?: readonly TopoDiagnostic[] } | undefined
37
+ )?.issues;
38
+ const remainingIssues = issues?.filter(isDerivationBlockingIssue);
39
+
40
+ if (remainingIssues === undefined || remainingIssues.length === 0) {
41
+ return Result.ok();
42
+ }
43
+
44
+ return Result.err(
45
+ new ValidationError(
46
+ `Topo validation failed with ${remainingIssues.length} issue(s)`,
47
+ {
48
+ cause: result.error,
49
+ context: { issues: remainingIssues },
50
+ }
51
+ )
52
+ );
53
+ };
54
+
55
+ /**
56
+ * Validate that a topo is ready for established outputs.
57
+ *
58
+ * Established surfaces still require the authored graph to be structurally
59
+ * valid, and they must also reject any remaining draft state.
60
+ */
61
+ export const validateEstablishedTopo = (topo: Topo) => {
62
+ const structural = keepDerivationBlockingIssues(validateTopo(topo));
63
+ if (structural.isErr()) {
64
+ return structural;
65
+ }
66
+
67
+ const established = validateDraftFreeTopo(topo);
68
+ if (established.isErr()) {
69
+ return established;
70
+ }
71
+
72
+ return Result.ok();
73
+ };