@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/README.md ADDED
@@ -0,0 +1,190 @@
1
+ # @ontrails/core
2
+
3
+ The foundation. Define trails, compose them into topos, return typed Results, and let the framework derive everything else.
4
+
5
+ ## Usage
6
+
7
+ ```typescript
8
+ import { trail, topo, Result } from '@ontrails/core';
9
+ import { z } from 'zod';
10
+
11
+ const greet = trail('greet', {
12
+ input: z.object({ name: z.string().describe('Who to greet') }),
13
+ output: z.object({ message: z.string() }),
14
+ intent: 'read',
15
+ examples: [
16
+ { name: 'Hello', input: { name: 'World' }, expected: { message: 'Hello, World!' } },
17
+ ],
18
+ implementation: (input) => Result.ok({ message: `Hello, ${input.name}!` }),
19
+ });
20
+
21
+ const graph = topo('myapp', { greet });
22
+ ```
23
+
24
+ Trails compose other trails through `composes` and `ctx.compose()`:
25
+
26
+ ```typescript
27
+ const onboard = trail('entity.onboard', {
28
+ composes: ['entity.add', 'entity.relate'],
29
+ input: z.object({ name: z.string(), type: z.string() }),
30
+ implementation: async (input, ctx) => {
31
+ const added = await ctx.compose('entity.add', input);
32
+ if (added.isErr()) return added;
33
+ return Result.ok({ entity: added.value });
34
+ },
35
+ });
36
+ ```
37
+
38
+ ## API
39
+
40
+ ### Trail primitives
41
+
42
+ | Export | What it does |
43
+ | --- | --- |
44
+ | `trail(id, spec)` | Define a unit of work with typed input and `Result` output; use `composes` for composition |
45
+ | `signal(id, spec)` | Define a server-originated notification with a typed data schema |
46
+ | `resource(id, spec)` | Define an infrastructure dependency with `create`, `dispose`, and optional `mock` |
47
+ | `drainResources(resources, ctx, configValues?)` | Evict and dispose cached resource singletons for surface/test shutdown |
48
+ | `blobRefSchema` / `createBlobRef(...)` | Declare and create binary output references with a shared descriptor contract |
49
+ | `topo(name, ...modules, options?)` | Collect trail modules into a queryable topology with optional `observe:` sinks |
50
+ | `deriveTrail(entity, operation, spec)` | Derive CRUD-shaped trail contracts from a entity on the `@ontrails/core/trails` subpath |
51
+ | `validateTopo(topo)` | Structural validation: compose targets exist, no cycles, examples parse, output schemas present |
52
+
53
+ ### Execution
54
+
55
+ | Export | What it does |
56
+ | --- | --- |
57
+ | `executeTrail(trail, rawInput, options?)` | Centralized execution pipeline: validates input, builds context, composes layers, runs the trail. Never throws -- exceptions become `Result.err(InternalError)`. |
58
+ | `run(topo, id, input, options?)` | Headless trail execution by ID. Looks up the trail in the topo, then delegates to `executeTrail`. Returns `Result.err(NotFoundError)` if the ID is not registered. |
59
+
60
+ ```typescript
61
+ // executeTrail — surfaces use this directly
62
+ const surfaceResult = await executeTrail(greet, { name: 'Alice' });
63
+
64
+ // run — headless execution by trail ID
65
+ const runResult = await run(graph, 'greet', { name: 'Alice' });
66
+ if (runResult.isOk()) console.log(runResult.value);
67
+ ```
68
+
69
+ ### Topo accessors
70
+
71
+ Beyond the `trail(id, spec)` builder, `Topo` exposes these accessors:
72
+
73
+ | Accessor | What it returns |
74
+ | --- | --- |
75
+ | `topo.ids()` | `string[]` of all registered trail IDs |
76
+ | `topo.count` | Number of registered trails |
77
+ | `topo.get(id)` | The `Trail` with that ID, or `undefined` |
78
+ | `topo.has(id)` | Whether a trail ID is registered |
79
+ | `topo.list()` | All registered trails as an array |
80
+
81
+ ### Type utilities
82
+
83
+ | Export | What it does |
84
+ | --- | --- |
85
+ | `TrailInput<T>` | Extract the input type from a `Trail` |
86
+ | `TrailOutput<T>` | Extract the output type from a `Trail` |
87
+ | `TrailResult<T>` | `Result<TrailOutput<T>, Error>` -- the Result type for a trail's output |
88
+ | `inputOf(trail)` | Get the input Zod schema from a trail instance |
89
+ | `outputOf(trail)` | Get the output Zod schema (or `undefined`) from a trail instance |
90
+
91
+ ### Execution option types
92
+
93
+ | Type | What it describes |
94
+ | --- | --- |
95
+ | `ExecuteTrailOptions` | Options for `executeTrail`: `ctx`, `abortSignal`, `layers`, `createContext` |
96
+ | `RunOptions` | Same shape as `ExecuteTrailOptions`; forwarded by `run` |
97
+
98
+ ### Result
99
+
100
+ ```typescript
101
+ Result.ok(value); // Success
102
+ Result.err(error); // Failure
103
+ Result.combine(results); // Result<T>[] → Result<T[]>
104
+ Result.fromJson(json); // Parse JSON string into Result
105
+ Result.toJson(value); // Serialize to JSON string as Result
106
+ Result.fromFetch(response); // Convert fetch Response to Result
107
+
108
+ result.isOk(); // Type guard
109
+ result.isErr(); // Type guard
110
+ result.map(fn); // Transform success
111
+ result.flatMap(fn); // Chain Result-returning functions
112
+ result.match({ ok, err }); // Pattern match
113
+ result.unwrapOr(fallback); // Value or fallback
114
+ ```
115
+
116
+ ### Error taxonomy
117
+
118
+ The current taxonomy is generated from the `errorClasses` owner registry and category code maps in `@ontrails/core`.
119
+
120
+ <!-- error-taxonomy:start -->
121
+ <!-- GENERATED: run `bun run error-taxonomy:sync`; check with `bun run error-taxonomy:check`. Variant: category. -->
122
+
123
+ | Category | CLI Exit | HTTP | JSON-RPC | Retryable | Fixed Classes |
124
+ | --- | --- | --- | --- | --- | --- |
125
+ | `validation` | 1 | 400 | -32602 | No | `ValidationError`, `AmbiguousError` |
126
+ | `not_found` | 2 | 404 | -32601 | No | `NotFoundError`, `VersionNotSupportedError` |
127
+ | `conflict` | 3 | 409 | -32603 | No | `AlreadyExistsError`, `ConflictError` |
128
+ | `permission` | 4 | 403 | -32600 | No | `PermissionError`, `PermitError` |
129
+ | `timeout` | 5 | 504 | -32603 | Yes | `TimeoutError` |
130
+ | `rate_limit` | 6 | 429 | -32603 | Yes | `RateLimitError` |
131
+ | `network` | 7 | 502 | -32603 | Yes | `NetworkError` |
132
+ | `shift` | 10 | 503 | -32603 | Yes | `WorkspaceShiftError` |
133
+ | `internal` | 8 | 500 | -32603 | No | `AssertionError`, `InternalError`, `DerivationError`, `RecoverableCompletionError` |
134
+ | `auth` | 9 | 401 | -32600 | No | `AuthError` |
135
+ | `cancelled` | 130 | 499 | -32603 | No | `CancelledError` |
136
+
137
+ Dynamic classes:
138
+
139
+ - `RetryExhaustedError` inherits category and surface codes from its wrapped `TrailsError`; retryable is always No.
140
+ <!-- error-taxonomy:end -->
141
+
142
+ Public surfaces share one redacted rendering contract before applying surface codes. Sensitive substrings are removed from non-internal `TrailsError` messages. Internal-category `TrailsError` instances and unknown native errors remain opaque: HTTP, MCP, and library boundaries use `Internal server error`, while CLI uses the transport-neutral `Internal error`. Diagnostics and serialized payloads keep their useful structure while redacting messages, context, and stack strings.
143
+
144
+ The developer returns `Result.err(new NotFoundError(...))`. The framework maps it to the right code on every surface.
145
+
146
+ ### Other exports
147
+
148
+ - **Schema derivation** -- `deriveFields(schema)` extracts faithfully
149
+ representable field metadata from Zod for prompts and forms
150
+ - **Validation** -- `validateInput`, `formatZodIssues`, `zodToJsonSchema`
151
+ - **Resilience** -- `retry`, `withTimeout`, `shouldRetry`, `deriveBackoffDelay`
152
+ - **Serialization** -- `serializeError`, `deserializeError`
153
+ - **Branded types** -- `uuid`, `email`, `nonEmptyString`, `positiveInt`
154
+ - **Execution layers** -- low-level pipeline wrappers via `composeLayers`
155
+ - **Guards and collections** -- `isDefined`, `chunk`, `dedupe`, `groupBy`, `sortBy`
156
+ - **Patterns** (`@ontrails/core/patterns`) -- reusable Zod schemas for pagination, bulk ops, timestamps, sorting
157
+ - **Trail factories** (`@ontrails/core/trails`) -- derive CRUD-shaped trail contracts from entities without re-authoring IDs, schemas, examples, or intents
158
+ - **Redaction** (`@ontrails/core/redaction`) -- strip sensitive data before logging
159
+
160
+ ### Public helper boundaries
161
+
162
+ The root package also exposes a few low-level contracts that other framework packages build on:
163
+
164
+ - **Intrinsic tracing** -- `TraceRecord`, `TraceSink`, `TraceContext`, and the sink registry helpers are the core-owned execution record shape shared by `@ontrails/observability` and adapters.
165
+ - **Trails DB** -- `deriveTrailsDbPath`, `deriveTrailsStateDir`, `deriveTrailsStateHome`, `deriveTrailsProjectKey`, `deriveTrailsDir`, `ensureSubsystemSchema`, `openReadTrailsDb`, and `openWriteTrailsDb` are the generic database primitive used by framework subsystems.
166
+ - **Surface rendering helpers** -- safe error rendering, layer field rendering, compose-batch validation, late-bound signal references, and Zod default-wrapper stripping are stable root exports for first-party surfaces, store helpers, and tests.
167
+
168
+ See the [API Reference](../../docs/api-reference.md) for the full list.
169
+
170
+ ## Migration: topo-store moved to `@ontrails/topography`
171
+
172
+ Per [ADR-0042](../../docs/adr/0042-core-topography-boundary-doctrine.md), the topo-store public API previously exported from `@ontrails/core` now lives in `@ontrails/topography`. Generic `trails-db` helpers (`openReadTrailsDb`, `openWriteTrailsDb`, `ensureSubsystemSchema`, `deriveTrailsDbPath`, `deriveTrailsStateDir`, `deriveTrailsStateHome`, `deriveTrailsProjectKey`, `deriveTrailsDir`) stay in core because tracing and other subsystems share them.
173
+
174
+ Update consumer imports:
175
+
176
+ ```diff
177
+ - import { topoStore, createTopoStore, createMockTopoStore, createTopoSnapshot, listTopoSnapshots, pinTopoSnapshot, unpinTopoSnapshot, createStoredTopoSnapshot, getStoredTopoExport, countTopoSnapshots, countPinnedSnapshots, countPrunableSnapshots, pruneUnpinnedSnapshots } from '@ontrails/core';
178
+ + import { topoStore, createTopoStore, createMockTopoStore, createTopoSnapshot, listTopoSnapshots, pinTopoSnapshot, unpinTopoSnapshot } from '@ontrails/topography';
179
+ + import { createStoredTopoSnapshot, getStoredTopoExport, countTopoSnapshots, countPinnedSnapshots, countPrunableSnapshots, pruneUnpinnedSnapshots } from '@ontrails/topography/backend-support';
180
+ ```
181
+
182
+ Types `ReadOnlyTopoStore`, `MockTopoStoreSeed`, `TopoSnapshot`, `TopoStoreRef`, `TopoStoreExportRecord`, `TopoStoreResourceRecord`, `TopoStoreTrailRecord`, `TopoStoreTrailDetailRecord`, `CreateTopoSnapshotInput`, and `ListTopoSnapshotsOptions` move to `@ontrails/topography`. `StoredTopoExport` moves to `@ontrails/topography/backend-support`.
183
+
184
+ ## Installation
185
+
186
+ These commands target stable `0.2.0`. Run them after that version is published to npm.
187
+
188
+ ```bash
189
+ bun add --exact @ontrails/core@0.2.0 zod
190
+ ```
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@ontrails/core",
3
+ "version": "0.2.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/outfitter-dev/trails.git",
7
+ "directory": "packages/core"
8
+ },
9
+ "files": [
10
+ "src/**/*.ts",
11
+ "!src/**/__tests__/**",
12
+ "!src/**/*.test.ts",
13
+ "!src/**/*.test-d.ts",
14
+ "README.md",
15
+ "CHANGELOG.md"
16
+ ],
17
+ "type": "module",
18
+ "exports": {
19
+ ".": "./src/index.ts",
20
+ "./patterns": "./src/patterns/index.ts",
21
+ "./redaction": "./src/redaction/index.ts",
22
+ "./store": "./src/store/index.ts",
23
+ "./trails": "./src/trails/index.ts",
24
+ "./package.json": "./package.json"
25
+ },
26
+ "scripts": {
27
+ "build": "tsc -b",
28
+ "test": "bun test",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "oxlint ./src",
31
+ "clean": "rm -rf dist *.tsbuildinfo"
32
+ },
33
+ "peerDependencies": {
34
+ "zod": "^4.3.5"
35
+ }
36
+ }
@@ -0,0 +1,116 @@
1
+ import type {
2
+ ActivationSourceKind,
3
+ ActivationSourceMeta,
4
+ } from './activation-source.js';
5
+
6
+ export const ACTIVATION_PROVENANCE_KEY =
7
+ '__trails_activation_provenance' as const;
8
+
9
+ export interface ActivationProvenanceSource {
10
+ readonly cron?: string | undefined;
11
+ readonly id: string;
12
+ readonly kind: ActivationSourceKind;
13
+ readonly meta?: ActivationSourceMeta | undefined;
14
+ readonly producerTrailId?: string | undefined;
15
+ readonly queue?: string | undefined;
16
+ readonly timezone?: string | undefined;
17
+ }
18
+
19
+ export interface ActivationProvenance {
20
+ readonly fireId: string;
21
+ readonly parentFireId?: string | undefined;
22
+ readonly rootFireId: string;
23
+ readonly source: ActivationProvenanceSource;
24
+ }
25
+
26
+ export interface ActivationProvenanceCarrier {
27
+ readonly activation?: ActivationProvenance | undefined;
28
+ readonly extensions?: Readonly<Record<string, unknown>> | undefined;
29
+ }
30
+
31
+ const isObjectRecord = (value: unknown): value is Record<string, unknown> =>
32
+ typeof value === 'object' && value !== null && !Array.isArray(value);
33
+
34
+ const optionalString = (value: unknown): boolean =>
35
+ value === undefined || typeof value === 'string';
36
+
37
+ const isActivationProvenanceSource = (
38
+ value: unknown
39
+ ): value is ActivationProvenanceSource =>
40
+ isObjectRecord(value) &&
41
+ typeof value['id'] === 'string' &&
42
+ typeof value['kind'] === 'string' &&
43
+ optionalString(value['cron']) &&
44
+ optionalString(value['producerTrailId']) &&
45
+ optionalString(value['queue']) &&
46
+ optionalString(value['timezone']) &&
47
+ (value['meta'] === undefined || isObjectRecord(value['meta']));
48
+
49
+ const isActivationProvenance = (
50
+ value: unknown
51
+ ): value is ActivationProvenance =>
52
+ isObjectRecord(value) &&
53
+ typeof value['fireId'] === 'string' &&
54
+ optionalString(value['parentFireId']) &&
55
+ typeof value['rootFireId'] === 'string' &&
56
+ isActivationProvenanceSource(value['source']);
57
+
58
+ export const getActivationProvenance = (
59
+ ctx: ActivationProvenanceCarrier | undefined
60
+ ): ActivationProvenance | undefined => {
61
+ if (isActivationProvenance(ctx?.activation)) {
62
+ return ctx.activation;
63
+ }
64
+ const fromExtensions = ctx?.extensions?.[ACTIVATION_PROVENANCE_KEY];
65
+ return isActivationProvenance(fromExtensions) ? fromExtensions : undefined;
66
+ };
67
+
68
+ export const withActivationProvenance = <
69
+ TCtx extends {
70
+ readonly extensions?: Readonly<Record<string, unknown>> | undefined;
71
+ },
72
+ >(
73
+ ctx: TCtx,
74
+ activation: ActivationProvenance
75
+ ): TCtx & { readonly activation: ActivationProvenance } => ({
76
+ ...ctx,
77
+ activation,
78
+ extensions: {
79
+ ...ctx.extensions,
80
+ [ACTIVATION_PROVENANCE_KEY]: activation,
81
+ },
82
+ });
83
+
84
+ export const buildActivationProvenanceTraceAttrs = (
85
+ activation: ActivationProvenance | undefined
86
+ ): Readonly<Record<string, unknown>> => {
87
+ if (activation === undefined) {
88
+ return {};
89
+ }
90
+
91
+ const attrs: Record<string, unknown> = {
92
+ 'trails.activation.fire_id': activation.fireId,
93
+ 'trails.activation.root_fire_id': activation.rootFireId,
94
+ 'trails.activation.source.id': activation.source.id,
95
+ 'trails.activation.source.kind': activation.source.kind,
96
+ };
97
+
98
+ if (activation.parentFireId !== undefined) {
99
+ attrs['trails.activation.parent_fire_id'] = activation.parentFireId;
100
+ }
101
+ if (activation.source.producerTrailId !== undefined) {
102
+ attrs['trails.activation.source.producer_trail.id'] =
103
+ activation.source.producerTrailId;
104
+ }
105
+ if (activation.source.queue !== undefined) {
106
+ attrs['trails.activation.source.queue'] = activation.source.queue;
107
+ }
108
+ if (activation.source.cron !== undefined) {
109
+ attrs['trails.activation.source.cron'] = activation.source.cron;
110
+ }
111
+ if (activation.source.timezone !== undefined) {
112
+ attrs['trails.activation.source.timezone'] = activation.source.timezone;
113
+ }
114
+
115
+ return attrs;
116
+ };