@ontrails/core 1.0.0-beta.14 → 1.0.0-beta.16
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.
- package/CHANGELOG.md +74 -0
- package/README.md +73 -24
- package/package.json +11 -4
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/branded.ts +1 -1
- package/src/context.ts +28 -4
- package/src/contour.ts +344 -0
- package/src/cross-batch.ts +69 -0
- package/src/cross-schema.ts +36 -0
- package/src/detours.ts +8 -0
- package/src/draft.ts +60 -52
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +245 -48
- package/src/execute.ts +1250 -40
- package/src/fire.ts +1169 -0
- package/src/index.ts +383 -49
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-projection.ts +192 -0
- package/src/layer.ts +81 -0
- package/src/observe.ts +361 -0
- package/src/path-security.ts +1 -1
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +792 -0
- package/src/resource.ts +175 -0
- package/src/result.ts +18 -0
- package/src/run.ts +7 -6
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +324 -0
- package/src/serialization.ts +90 -55
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +87 -0
- package/src/signal.ts +37 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +245 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +176 -0
- package/src/topo.ts +710 -59
- package/src/tracing.ts +345 -0
- package/src/trail.ts +347 -33
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +138 -0
- package/src/{internal/trails-db.ts → trails-db.ts} +50 -31
- package/src/transport-error-map.ts +174 -0
- package/src/type-utils.ts +17 -2
- package/src/types.ts +208 -17
- package/src/validate-established-topo.ts +8 -2
- package/src/validate-topo.ts +244 -13
- package/src/validation.ts +58 -1
- package/src/webhook.ts +276 -0
- package/src/workspace.ts +1 -1
- package/src/zod-wrappers.ts +72 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -10
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -21
- package/dist/context.js.map +0 -1
- package/dist/derive.d.ts +0 -44
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -149
- package/dist/derive.js.map +0 -1
- package/dist/dispatch.d.ts +0 -27
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/dispatch.js +0 -34
- package/dist/dispatch.js.map +0 -1
- package/dist/draft.d.ts +0 -28
- package/dist/draft.d.ts.map +0 -1
- package/dist/draft.js +0 -156
- package/dist/draft.js.map +0 -1
- package/dist/errors.d.ts +0 -83
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -142
- package/dist/errors.js.map +0 -1
- package/dist/event.d.ts +0 -8
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -7
- package/dist/event.js.map +0 -1
- package/dist/execute.d.ts +0 -35
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js +0 -90
- package/dist/execute.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/gate.d.ts +0 -17
- package/dist/gate.d.ts.map +0 -1
- package/dist/gate.js +0 -21
- package/dist/gate.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/index.d.ts +0 -48
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -51
- package/dist/index.js.map +0 -1
- package/dist/internal/topo-saves.d.ts +0 -47
- package/dist/internal/topo-saves.d.ts.map +0 -1
- package/dist/internal/topo-saves.js +0 -310
- package/dist/internal/topo-saves.js.map +0 -1
- package/dist/internal/topo-store-read.d.ts +0 -67
- package/dist/internal/topo-store-read.d.ts.map +0 -1
- package/dist/internal/topo-store-read.js +0 -222
- package/dist/internal/topo-store-read.js.map +0 -1
- package/dist/internal/topo-store.d.ts +0 -12
- package/dist/internal/topo-store.d.ts.map +0 -1
- package/dist/internal/topo-store.js +0 -571
- package/dist/internal/topo-store.js.map +0 -1
- package/dist/internal/trails-db.d.ts +0 -16
- package/dist/internal/trails-db.d.ts.map +0 -1
- package/dist/internal/trails-db.js +0 -118
- package/dist/internal/trails-db.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/layer.d.ts +0 -6
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -4
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/provision-config.d.ts +0 -22
- package/dist/provision-config.d.ts.map +0 -1
- package/dist/provision-config.js +0 -210
- package/dist/provision-config.js.map +0 -1
- package/dist/provision.d.ts +0 -71
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -56
- package/dist/provision.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -156
- package/dist/result.js.map +0 -1
- package/dist/run.d.ts +0 -27
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -34
- package/dist/run.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -153
- package/dist/serialization.js.map +0 -1
- package/dist/service-config.d.ts +0 -22
- package/dist/service-config.d.ts.map +0 -1
- package/dist/service-config.js +0 -210
- package/dist/service-config.js.map +0 -1
- package/dist/service.d.ts +0 -71
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -56
- package/dist/service.js.map +0 -1
- package/dist/signal.d.ts +0 -33
- package/dist/signal.d.ts.map +0 -1
- package/dist/signal.js +0 -17
- package/dist/signal.js.map +0 -1
- package/dist/topo-store.d.ts +0 -48
- package/dist/topo-store.d.ts.map +0 -1
- package/dist/topo-store.js +0 -175
- package/dist/topo-store.js.map +0 -1
- package/dist/topo.d.ts +0 -25
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -112
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -96
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -20
- package/dist/trail.js.map +0 -1
- package/dist/type-utils.d.ts +0 -24
- package/dist/type-utils.d.ts.map +0 -1
- package/dist/type-utils.js +0 -12
- package/dist/type-utils.js.map +0 -1
- package/dist/types.d.ts +0 -71
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/validate-established-topo.d.ts +0 -76
- package/dist/validate-established-topo.d.ts.map +0 -1
- package/dist/validate-established-topo.js +0 -43
- package/dist/validate-established-topo.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -176
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -165
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -77
- package/src/__tests__/derive.test.ts +0 -260
- package/src/__tests__/errors.test.ts +0 -309
- package/src/__tests__/execute.test.ts +0 -525
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/gate.test.ts +0 -125
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/run.test.ts +0 -181
- package/src/__tests__/serialization.test.ts +0 -401
- package/src/__tests__/service-config.test.ts +0 -228
- package/src/__tests__/service.test.ts +0 -197
- package/src/__tests__/signal.test.ts +0 -82
- package/src/__tests__/topo-store-read.test.ts +0 -251
- package/src/__tests__/topo-store.test.ts +0 -469
- package/src/__tests__/topo.test.ts +0 -277
- package/src/__tests__/trail-permit.test.ts +0 -60
- package/src/__tests__/trail.test.ts +0 -261
- package/src/__tests__/trails-db.test.ts +0 -191
- package/src/__tests__/type-utils.test.ts +0 -90
- package/src/__tests__/validate-topo.test.ts +0 -489
- package/src/__tests__/validation.test.ts +0 -336
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/event.ts +0 -15
- package/src/gate.ts +0 -44
- package/src/internal/topo-saves.ts +0 -429
- package/src/internal/topo-store-read.ts +0 -473
- package/src/internal/topo-store.ts +0 -1087
- package/src/provision-config.ts +0 -373
- package/src/provision.ts +0 -148
- package/src/topo-store.ts +0 -301
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/types.ts
CHANGED
|
@@ -1,4 +1,66 @@
|
|
|
1
|
+
import type { TrailsError } from './errors.js';
|
|
2
|
+
import type { BasePermit } from './permits.js';
|
|
1
3
|
import type { Result } from './result.js';
|
|
4
|
+
import type { Signal } from './signal.js';
|
|
5
|
+
import type { AnyTrail } from './trail.js';
|
|
6
|
+
import type { CrossInput, TrailOutput } from './type-utils.js';
|
|
7
|
+
import type { ActivationProvenance } from './activation-provenance.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Detour
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** A recovery path that activates when a trail's blaze fails with a matching error. */
|
|
14
|
+
export interface Detour<Input, Output, TErr extends TrailsError = TrailsError> {
|
|
15
|
+
/* oxlint-disable-next-line no-explicit-any -- standard pattern for matching abstract+concrete class constructors */
|
|
16
|
+
readonly on: abstract new (...args: any[]) => TErr;
|
|
17
|
+
readonly maxAttempts?: number | undefined;
|
|
18
|
+
readonly recover: (
|
|
19
|
+
attempt: DetourAttempt<Input, TErr>,
|
|
20
|
+
ctx: TrailContext
|
|
21
|
+
) => Promise<Result<Output, TrailsError>>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Context passed to a detour's recover function on each attempt. */
|
|
25
|
+
export interface DetourAttempt<Input, TErr extends TrailsError = TrailsError> {
|
|
26
|
+
/** 1-indexed attempt number */
|
|
27
|
+
readonly attempt: number;
|
|
28
|
+
/** The matched error */
|
|
29
|
+
readonly error: TErr;
|
|
30
|
+
/** Original trail input */
|
|
31
|
+
readonly input: Input;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type CrossBatchCall<TTarget extends AnyTrail | string = AnyTrail | string> =
|
|
35
|
+
TTarget extends AnyTrail
|
|
36
|
+
? readonly [trail: TTarget, input: CrossInput<TTarget>]
|
|
37
|
+
: readonly [id: string, input: unknown];
|
|
38
|
+
|
|
39
|
+
type CrossBatchResult<TTarget extends AnyTrail | string> =
|
|
40
|
+
TTarget extends AnyTrail
|
|
41
|
+
? Result<TrailOutput<TTarget>, Error>
|
|
42
|
+
: Result<unknown, Error>;
|
|
43
|
+
|
|
44
|
+
type CrossBatchResults<TCalls extends readonly CrossBatchCall[]> = {
|
|
45
|
+
readonly [K in keyof TCalls]: TCalls[K] extends readonly [
|
|
46
|
+
infer TTarget,
|
|
47
|
+
unknown,
|
|
48
|
+
]
|
|
49
|
+
? TTarget extends AnyTrail | string
|
|
50
|
+
? CrossBatchResult<TTarget>
|
|
51
|
+
: never
|
|
52
|
+
: never;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/** Runtime options for batch `ctx.cross([...])` calls. */
|
|
56
|
+
export interface CrossBatchOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Maximum number of branches to execute concurrently.
|
|
59
|
+
*
|
|
60
|
+
* Omit for unbounded concurrency. `1` is equivalent to sequential execution.
|
|
61
|
+
*/
|
|
62
|
+
readonly concurrency?: number | undefined;
|
|
63
|
+
}
|
|
2
64
|
|
|
3
65
|
/**
|
|
4
66
|
* Trail implementation — sync or async.
|
|
@@ -11,17 +73,67 @@ export type Implementation<I, O> = (
|
|
|
11
73
|
ctx: TrailContext
|
|
12
74
|
) => Result<O, Error> | Promise<Result<O, Error>>;
|
|
13
75
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Invoke another trail — used for trail composition.
|
|
78
|
+
*
|
|
79
|
+
* Two call shapes:
|
|
80
|
+
*
|
|
81
|
+
* - **By trail object** (typed): `ctx.cross(showGist, { id })` — the compiler
|
|
82
|
+
* infers `I` and `O` from the trail's schemas, so the result is fully typed.
|
|
83
|
+
* - **By string id** (untyped escape hatch): `ctx.cross('gist.show', { id })`
|
|
84
|
+
* — returns `Result<O, Error>` where `O` defaults to `unknown`.
|
|
85
|
+
* - **By batch**: `ctx.cross([[showGist, { id }], ['audit.log', payload]])`
|
|
86
|
+
* — executes every crossing concurrently and resolves once all results are
|
|
87
|
+
* available. Result ordering always matches the input tuple ordering. Pass
|
|
88
|
+
* `{ concurrency: N }` as the second argument to limit how many branches
|
|
89
|
+
* run at once.
|
|
90
|
+
*/
|
|
91
|
+
export interface CrossFn {
|
|
92
|
+
<const TCalls extends readonly CrossBatchCall[]>(
|
|
93
|
+
calls: TCalls,
|
|
94
|
+
options?: CrossBatchOptions
|
|
95
|
+
): Promise<CrossBatchResults<TCalls>>;
|
|
96
|
+
<T extends AnyTrail>(
|
|
97
|
+
trail: T,
|
|
98
|
+
input: CrossInput<T>
|
|
99
|
+
): Promise<Result<TrailOutput<T>, Error>>;
|
|
100
|
+
<O = unknown>(id: string, input: unknown): Promise<Result<O, Error>>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Emit a signal — used for signal-driven activation.
|
|
105
|
+
*
|
|
106
|
+
* Fan-out to consumer trails (those with the signal in their `on:` array) is
|
|
107
|
+
* the framework's responsibility. Producers call with a `Signal<T>` value and
|
|
108
|
+
* get best-effort `Promise<void>` semantics: payload validation, missing topo
|
|
109
|
+
* entries, guard suppression, and consumer failures are observable through
|
|
110
|
+
* diagnostics/logging but do not become producer-facing `Result` plumbing.
|
|
111
|
+
* Consumers fan out in parallel, each with its own derived context. Runtime
|
|
112
|
+
* cycle suppression is still signal-id-based against the current fire stack:
|
|
113
|
+
* it prevents re-entrant loops but can over-suppress legitimate diamond
|
|
114
|
+
* re-fires, with a debug breadcrumb and a warn emitted when suppression
|
|
115
|
+
* happens.
|
|
116
|
+
*/
|
|
117
|
+
export type FireFn = <T>(signal: Signal<T>, payload: T) => Promise<void>;
|
|
19
118
|
|
|
20
|
-
/** Resolve a
|
|
21
|
-
export type
|
|
22
|
-
|
|
119
|
+
/** Resolve a resource instance from the current trail context. */
|
|
120
|
+
export type ResourceLookup = <T = unknown>(
|
|
121
|
+
resourceOrId: { readonly id: string } | string
|
|
23
122
|
) => T;
|
|
24
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Wrap the execution of `fn` in a child trace span.
|
|
126
|
+
*
|
|
127
|
+
* Creates a nested span under the current trail's root trace record, times
|
|
128
|
+
* the callback, records success or failure (including error category), and
|
|
129
|
+
* writes the completed span to the registered sink. Errors thrown by `fn`
|
|
130
|
+
* are recorded on the span and then rethrown — tracing never swallows them.
|
|
131
|
+
*/
|
|
132
|
+
export type TraceFn = <T>(
|
|
133
|
+
label: string,
|
|
134
|
+
fn: () => T | Promise<T>
|
|
135
|
+
) => Promise<T>;
|
|
136
|
+
|
|
25
137
|
/** Callback for reporting progress from long-running trails */
|
|
26
138
|
export type ProgressCallback = (event: ProgressEvent) => void;
|
|
27
139
|
|
|
@@ -46,20 +158,68 @@ export interface Logger {
|
|
|
46
158
|
child(context: Record<string, unknown>): Logger;
|
|
47
159
|
}
|
|
48
160
|
|
|
49
|
-
|
|
50
|
-
|
|
161
|
+
export type LogLevel =
|
|
162
|
+
| 'debug'
|
|
163
|
+
| 'error'
|
|
164
|
+
| 'fatal'
|
|
165
|
+
| 'info'
|
|
166
|
+
| 'silent'
|
|
167
|
+
| 'trace'
|
|
168
|
+
| 'warn';
|
|
169
|
+
|
|
170
|
+
export interface LogRecord {
|
|
171
|
+
readonly category: string;
|
|
172
|
+
readonly level: LogLevel;
|
|
173
|
+
readonly message: string;
|
|
174
|
+
readonly metadata: Record<string, unknown>;
|
|
175
|
+
readonly timestamp: Date;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface LogSink {
|
|
179
|
+
readonly name: string;
|
|
180
|
+
readonly write: (record: LogRecord) => void;
|
|
181
|
+
readonly flush?: (() => Promise<void>) | undefined;
|
|
182
|
+
}
|
|
51
183
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
readonly id: string;
|
|
55
|
-
readonly scopes: readonly string[];
|
|
184
|
+
export interface LogFormatter {
|
|
185
|
+
format(record: LogRecord): string;
|
|
56
186
|
}
|
|
57
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Context extension key for the invoking surface name.
|
|
190
|
+
*/
|
|
191
|
+
export const SURFACE_KEY = '__trails_surface' as const;
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Context extension key for the layer names attached by the invoking surface.
|
|
195
|
+
*/
|
|
196
|
+
export const SURFACE_LAYER_NAMES_KEY = '__trails_surface_layer_names' as const;
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Context extension key carrying per-layer runtime input.
|
|
200
|
+
*
|
|
201
|
+
* Surfaces (CLI, MCP, HTTP) project each typed layer's `input` schema onto
|
|
202
|
+
* their native idioms (flags, tool params, query strings). At execute time
|
|
203
|
+
* the parsed values are partitioned per layer and stored under this key as
|
|
204
|
+
* `Record<layerName, unknown>`. Layers that need runtime input read their
|
|
205
|
+
* own slot via `ctx.extensions?.[LAYER_INPUTS_KEY]?.[layer.name]`.
|
|
206
|
+
*
|
|
207
|
+
* @see TRL-473 for the CLI projection contract.
|
|
208
|
+
*/
|
|
209
|
+
export const LAYER_INPUTS_KEY = '__trails_layer_inputs' as const;
|
|
210
|
+
|
|
58
211
|
/** Runtime context threaded through every trail execution */
|
|
59
212
|
export interface TrailContext {
|
|
213
|
+
readonly activation?: ActivationProvenance | undefined;
|
|
60
214
|
readonly requestId: string;
|
|
61
215
|
readonly abortSignal: AbortSignal;
|
|
62
216
|
readonly cross?: CrossFn | undefined;
|
|
217
|
+
/**
|
|
218
|
+
* Emit a typed signal. Fans out to every trail with the signal in its
|
|
219
|
+
* `on:` declaration. Bound by the runner that holds the topo (typically
|
|
220
|
+
* `run()`); undefined when a context is constructed without topo access.
|
|
221
|
+
*/
|
|
222
|
+
readonly fire?: FireFn | undefined;
|
|
63
223
|
readonly permit?: BasePermit;
|
|
64
224
|
readonly workspaceRoot?: string | undefined;
|
|
65
225
|
readonly logger?: Logger | undefined;
|
|
@@ -67,7 +227,37 @@ export interface TrailContext {
|
|
|
67
227
|
readonly cwd?: string | undefined;
|
|
68
228
|
readonly env?: Record<string, string | undefined> | undefined;
|
|
69
229
|
readonly extensions?: Readonly<Record<string, unknown>> | undefined;
|
|
70
|
-
readonly
|
|
230
|
+
readonly resource?: ResourceLookup | undefined;
|
|
231
|
+
/**
|
|
232
|
+
* Whether the current invocation is a dry run.
|
|
233
|
+
*
|
|
234
|
+
* Defaults to `false`. Trails that don't read this field are unaffected.
|
|
235
|
+
* Trails that do read it decide what dry-run means for their domain — for
|
|
236
|
+
* example: preview the change without committing, validate inputs without
|
|
237
|
+
* performing side effects, or return what would happen without actually
|
|
238
|
+
* doing it.
|
|
239
|
+
*
|
|
240
|
+
* The framework only carries the flag from the surface (e.g. CLI
|
|
241
|
+
* `--dry-run`) into the context. It never short-circuits trail execution
|
|
242
|
+
* on its own based on this field.
|
|
243
|
+
*
|
|
244
|
+
* Pair this runtime signal with `TrailSpec.dryRun`, which declares whether a
|
|
245
|
+
* trail supports dry-run semantics for governance, derivation, and surface
|
|
246
|
+
* tooling.
|
|
247
|
+
*
|
|
248
|
+
* @remarks Always defined on contexts produced by `executeTrail` or
|
|
249
|
+
* `createTrailContext` (normalized to `false` when not provided).
|
|
250
|
+
*/
|
|
251
|
+
readonly dryRun?: boolean | undefined;
|
|
252
|
+
/**
|
|
253
|
+
* Wrap a callback in a child trace span.
|
|
254
|
+
*
|
|
255
|
+
* Always present on contexts produced by `executeTrail` or
|
|
256
|
+
* `createTrailContext`. Optional on the interface so manually constructed
|
|
257
|
+
* contexts (tests, ad-hoc compositions) don't have to supply one — call
|
|
258
|
+
* sites tolerate `undefined` by falling back to a no-op passthrough.
|
|
259
|
+
*/
|
|
260
|
+
readonly trace?: TraceFn | undefined;
|
|
71
261
|
}
|
|
72
262
|
|
|
73
263
|
/**
|
|
@@ -82,6 +272,7 @@ export type PermitRequirement =
|
|
|
82
272
|
| 'public';
|
|
83
273
|
|
|
84
274
|
/** Input shape used to seed a runtime TrailContext before resolution. */
|
|
85
|
-
export type TrailContextInit = Omit<TrailContext, '
|
|
86
|
-
readonly
|
|
275
|
+
export type TrailContextInit = Omit<TrailContext, 'resource' | 'trace'> & {
|
|
276
|
+
readonly resource?: ResourceLookup | undefined;
|
|
277
|
+
readonly trace?: TraceFn | undefined;
|
|
87
278
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { ValidationError } from './errors.js';
|
|
2
2
|
import { Result } from './result.js';
|
|
3
3
|
import type { Topo } from './topo.js';
|
|
4
|
-
import {
|
|
4
|
+
import { validateDraftFreeTopo } from './draft.js';
|
|
5
5
|
import type { TopoIssue } from './validate-topo.js';
|
|
6
6
|
import { validateTopo } from './validate-topo.js';
|
|
7
7
|
|
|
@@ -9,7 +9,13 @@ const PROJECTION_BLOCKING_RULES = new Set([
|
|
|
9
9
|
'cross-cycle',
|
|
10
10
|
'cross-exists',
|
|
11
11
|
'no-self-cross',
|
|
12
|
-
'
|
|
12
|
+
'activation-source-definition-unique',
|
|
13
|
+
'activation-source-edge-unique',
|
|
14
|
+
'activation-source-kind-known',
|
|
15
|
+
'activation-schedule-valid',
|
|
16
|
+
'resource-exists',
|
|
17
|
+
'signal-fire-exists',
|
|
18
|
+
'signal-on-exists',
|
|
13
19
|
'signal-origin-exists',
|
|
14
20
|
]);
|
|
15
21
|
|
package/src/validate-topo.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structural validation for a Topo graph.
|
|
3
3
|
*
|
|
4
|
-
* Checks trail crossing references, example input validity,
|
|
5
|
-
* references, and output schema completeness. Returns
|
|
6
|
-
* issues collected into a single ValidationError.
|
|
4
|
+
* Checks trail crossing references, example input validity, signal origin
|
|
5
|
+
* references, activation source kinds, and output schema completeness. Returns
|
|
6
|
+
* a Result with all issues collected into a single ValidationError.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import type { ActivationSchemaIssue } from './activation-source-compatibility.js';
|
|
10
|
+
import { getActivationSourceInputCompatibilityIssues } from './activation-source-compatibility.js';
|
|
11
|
+
import {
|
|
12
|
+
activationSourceDeclarationSignature,
|
|
13
|
+
activationSourceKey,
|
|
14
|
+
} from './activation-source-projection.js';
|
|
15
|
+
import type { AnyContour } from './contour.js';
|
|
16
|
+
import { getContourReferences } from './contour.js';
|
|
9
17
|
import { ValidationError } from './errors.js';
|
|
18
|
+
import type { ActivationEntry } from './activation-source.js';
|
|
19
|
+
import { isKnownActivationSourceKind } from './activation-source.js';
|
|
10
20
|
import { isDraftId } from './draft.js';
|
|
11
|
-
import type { AnySignal } from './
|
|
21
|
+
import type { AnySignal } from './signal.js';
|
|
22
|
+
import { validateScheduleSource } from './schedule.js';
|
|
12
23
|
import { Result } from './result.js';
|
|
13
24
|
import type { Topo } from './topo.js';
|
|
14
25
|
import type { AnyTrail } from './trail.js';
|
|
15
26
|
import { validateInput } from './validation.js';
|
|
27
|
+
import { validateWebhookSource } from './webhook.js';
|
|
16
28
|
|
|
17
29
|
// ---------------------------------------------------------------------------
|
|
18
30
|
// Issue shape
|
|
@@ -22,8 +34,14 @@ export interface TopoIssue {
|
|
|
22
34
|
readonly trailId: string;
|
|
23
35
|
readonly rule: string;
|
|
24
36
|
readonly message: string;
|
|
37
|
+
readonly inputPath?: readonly (string | number)[];
|
|
38
|
+
readonly schemaIssues?: readonly TopoSchemaIssue[];
|
|
39
|
+
readonly sourceId?: string;
|
|
40
|
+
readonly sourceKind?: string;
|
|
25
41
|
}
|
|
26
42
|
|
|
43
|
+
export type TopoSchemaIssue = ActivationSchemaIssue;
|
|
44
|
+
|
|
27
45
|
// ---------------------------------------------------------------------------
|
|
28
46
|
// Validators
|
|
29
47
|
// ---------------------------------------------------------------------------
|
|
@@ -114,21 +132,21 @@ const checkCrosses = (
|
|
|
114
132
|
return issues;
|
|
115
133
|
};
|
|
116
134
|
|
|
117
|
-
const
|
|
135
|
+
const checkResources = (
|
|
118
136
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
119
137
|
topo: Topo
|
|
120
138
|
): TopoIssue[] => {
|
|
121
139
|
const issues: TopoIssue[] = [];
|
|
122
140
|
|
|
123
141
|
for (const [id, trail] of trails) {
|
|
124
|
-
for (const
|
|
142
|
+
for (const declaredResource of trail.resources) {
|
|
125
143
|
if (
|
|
126
|
-
!topo.
|
|
127
|
-
!isDraftId(
|
|
144
|
+
!topo.hasResource(declaredResource.id) &&
|
|
145
|
+
!isDraftId(declaredResource.id)
|
|
128
146
|
) {
|
|
129
147
|
issues.push({
|
|
130
|
-
message: `
|
|
131
|
-
rule: '
|
|
148
|
+
message: `Resource "${declaredResource.id}" is not in the topo`,
|
|
149
|
+
rule: 'resource-exists',
|
|
132
150
|
trailId: id,
|
|
133
151
|
});
|
|
134
152
|
}
|
|
@@ -203,6 +221,214 @@ const checkSignalOrigins = (
|
|
|
203
221
|
return issues;
|
|
204
222
|
};
|
|
205
223
|
|
|
224
|
+
const checkSignalReferences = (
|
|
225
|
+
trails: ReadonlyMap<string, AnyTrail>,
|
|
226
|
+
signals: ReadonlyMap<string, AnySignal>
|
|
227
|
+
): TopoIssue[] => {
|
|
228
|
+
const issues: TopoIssue[] = [];
|
|
229
|
+
|
|
230
|
+
for (const [id, trail] of trails) {
|
|
231
|
+
for (const signalId of trail.fires ?? []) {
|
|
232
|
+
if (!signals.has(signalId) && !isDraftId(signalId)) {
|
|
233
|
+
issues.push({
|
|
234
|
+
message: `Trail fires signal "${signalId}" which is not in the topo`,
|
|
235
|
+
rule: 'signal-fire-exists',
|
|
236
|
+
trailId: id,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
for (const signalId of trail.on ?? []) {
|
|
242
|
+
if (!signals.has(signalId) && !isDraftId(signalId)) {
|
|
243
|
+
issues.push({
|
|
244
|
+
message: `Trail declares on signal "${signalId}" which is not in the topo`,
|
|
245
|
+
rule: 'signal-on-exists',
|
|
246
|
+
trailId: id,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return issues;
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
const checkActivationSources = (
|
|
256
|
+
trails: ReadonlyMap<string, AnyTrail>
|
|
257
|
+
): TopoIssue[] => {
|
|
258
|
+
const issues: TopoIssue[] = [];
|
|
259
|
+
const sourceDeclarations = new Map<
|
|
260
|
+
string,
|
|
261
|
+
{
|
|
262
|
+
readonly signature: string;
|
|
263
|
+
readonly trailId: string;
|
|
264
|
+
}
|
|
265
|
+
>();
|
|
266
|
+
const trailSourceEdges = new Set<string>();
|
|
267
|
+
|
|
268
|
+
for (const [id, trail] of trails) {
|
|
269
|
+
for (const activation of trail.activationSources ?? []) {
|
|
270
|
+
if (!isKnownActivationSourceKind(activation.source.kind)) {
|
|
271
|
+
issues.push({
|
|
272
|
+
message: `Trail declares on source "${activation.source.id}" with unsupported source kind "${activation.source.kind}"`,
|
|
273
|
+
rule: 'activation-source-kind-known',
|
|
274
|
+
trailId: id,
|
|
275
|
+
});
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const sourceKey = activationSourceKey(activation.source);
|
|
280
|
+
const edgeKey = `${id}\0${sourceKey}`;
|
|
281
|
+
if (trailSourceEdges.has(edgeKey)) {
|
|
282
|
+
issues.push({
|
|
283
|
+
message: `Trail declares activation source "${activation.source.id}" (${activation.source.kind}) more than once. Keep one source-to-trail activation edge, or split distinct activation behavior into distinct source ids.`,
|
|
284
|
+
rule: 'activation-source-edge-unique',
|
|
285
|
+
sourceId: activation.source.id,
|
|
286
|
+
sourceKind: activation.source.kind,
|
|
287
|
+
trailId: id,
|
|
288
|
+
});
|
|
289
|
+
} else {
|
|
290
|
+
trailSourceEdges.add(edgeKey);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (!isDraftId(activation.source.id)) {
|
|
294
|
+
const signature = activationSourceDeclarationSignature(
|
|
295
|
+
activation.source
|
|
296
|
+
);
|
|
297
|
+
const previous = sourceDeclarations.get(sourceKey);
|
|
298
|
+
if (previous === undefined) {
|
|
299
|
+
sourceDeclarations.set(sourceKey, { signature, trailId: id });
|
|
300
|
+
} else if (previous.signature !== signature) {
|
|
301
|
+
issues.push({
|
|
302
|
+
message: `Activation source "${activation.source.id}" (${activation.source.kind}) is declared with conflicting source options by trails "${previous.trailId}" and "${id}". Use one canonical source declaration per source id, or give distinct source configurations distinct ids.`,
|
|
303
|
+
rule: 'activation-source-definition-unique',
|
|
304
|
+
sourceId: activation.source.id,
|
|
305
|
+
sourceKind: activation.source.kind,
|
|
306
|
+
trailId: id,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const scheduleIssues = validateScheduleSource(activation.source);
|
|
312
|
+
for (const issue of scheduleIssues) {
|
|
313
|
+
issues.push({
|
|
314
|
+
inputPath: [issue.field],
|
|
315
|
+
message: `Trail declares schedule source "${activation.source.id}" with invalid ${issue.field}: ${issue.message}`,
|
|
316
|
+
rule: 'activation-schedule-valid',
|
|
317
|
+
schemaIssues: [
|
|
318
|
+
{ code: issue.field, message: issue.message, path: [issue.field] },
|
|
319
|
+
],
|
|
320
|
+
sourceId: activation.source.id,
|
|
321
|
+
sourceKind: activation.source.kind,
|
|
322
|
+
trailId: id,
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const webhookIssues = validateWebhookSource(activation.source);
|
|
327
|
+
for (const issue of webhookIssues) {
|
|
328
|
+
issues.push({
|
|
329
|
+
inputPath: [issue.field],
|
|
330
|
+
message: `Trail declares webhook source "${activation.source.id}" with invalid ${issue.field}: ${issue.message}`,
|
|
331
|
+
rule: 'activation-webhook-valid',
|
|
332
|
+
schemaIssues: [
|
|
333
|
+
{ code: issue.field, message: issue.message, path: [issue.field] },
|
|
334
|
+
],
|
|
335
|
+
sourceId: activation.source.id,
|
|
336
|
+
sourceKind: activation.source.kind,
|
|
337
|
+
trailId: id,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return issues;
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const issuePathText = (path: readonly (string | number)[]): string =>
|
|
347
|
+
path.length > 0 ? path.join('.') : '<root>';
|
|
348
|
+
|
|
349
|
+
const createSourceCompatibilityIssue = (
|
|
350
|
+
trailId: string,
|
|
351
|
+
activation: ActivationEntry,
|
|
352
|
+
schemaIssues: readonly TopoSchemaIssue[]
|
|
353
|
+
): TopoIssue => {
|
|
354
|
+
const [firstIssue] = schemaIssues;
|
|
355
|
+
const inputPath = firstIssue?.path ?? Object.freeze([]);
|
|
356
|
+
return {
|
|
357
|
+
inputPath,
|
|
358
|
+
message: `Activation source "${activation.source.id}" (${activation.source.kind}) does not satisfy trail input at ${issuePathText(inputPath)}: ${firstIssue?.message ?? 'source payload is incompatible with trail input'}`,
|
|
359
|
+
rule: 'activation-source-input-compatible',
|
|
360
|
+
schemaIssues,
|
|
361
|
+
sourceId: activation.source.id,
|
|
362
|
+
sourceKind: activation.source.kind,
|
|
363
|
+
trailId,
|
|
364
|
+
};
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
const checkSourcePayloadCompatibility = (
|
|
368
|
+
trail: AnyTrail,
|
|
369
|
+
activation: ActivationEntry,
|
|
370
|
+
signals: ReadonlyMap<string, AnySignal>
|
|
371
|
+
): TopoIssue | undefined => {
|
|
372
|
+
if (
|
|
373
|
+
!isKnownActivationSourceKind(activation.source.kind) ||
|
|
374
|
+
isDraftId(activation.source.id)
|
|
375
|
+
) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const schemaIssues = getActivationSourceInputCompatibilityIssues(
|
|
380
|
+
trail.input,
|
|
381
|
+
activation.source,
|
|
382
|
+
signals
|
|
383
|
+
);
|
|
384
|
+
if (!schemaIssues) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return schemaIssues.length > 0
|
|
389
|
+
? createSourceCompatibilityIssue(trail.id, activation, schemaIssues)
|
|
390
|
+
: undefined;
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const checkActivationSourceInputCompatibility = (
|
|
394
|
+
trails: ReadonlyMap<string, AnyTrail>,
|
|
395
|
+
signals: ReadonlyMap<string, AnySignal>
|
|
396
|
+
): TopoIssue[] => {
|
|
397
|
+
const issues: TopoIssue[] = [];
|
|
398
|
+
|
|
399
|
+
for (const trail of trails.values()) {
|
|
400
|
+
for (const activation of trail.activationSources ?? []) {
|
|
401
|
+
const issue = checkSourcePayloadCompatibility(trail, activation, signals);
|
|
402
|
+
if (issue) {
|
|
403
|
+
issues.push(issue);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return issues;
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const checkContourReferences = (
|
|
412
|
+
contours: ReadonlyMap<string, AnyContour>,
|
|
413
|
+
topo: Topo
|
|
414
|
+
): TopoIssue[] => {
|
|
415
|
+
const issues: TopoIssue[] = [];
|
|
416
|
+
|
|
417
|
+
for (const [name, contourDef] of contours) {
|
|
418
|
+
for (const ref of getContourReferences(contourDef)) {
|
|
419
|
+
if (!topo.hasContour(ref.contour) && !isDraftId(ref.contour)) {
|
|
420
|
+
issues.push({
|
|
421
|
+
message: `Contour "${name}" references "${ref.contour}" which is not in the topo`,
|
|
422
|
+
rule: 'contour-reference-exists',
|
|
423
|
+
trailId: name,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return issues;
|
|
430
|
+
};
|
|
431
|
+
|
|
206
432
|
// ---------------------------------------------------------------------------
|
|
207
433
|
// Public API
|
|
208
434
|
// ---------------------------------------------------------------------------
|
|
@@ -210,16 +436,21 @@ const checkSignalOrigins = (
|
|
|
210
436
|
/**
|
|
211
437
|
* Validate the structural integrity of a Topo graph.
|
|
212
438
|
*
|
|
213
|
-
* Checks crossing references, example inputs,
|
|
214
|
-
* schema presence. Returns `Result.ok()` when no
|
|
439
|
+
* Checks crossing references, example inputs, signal origins, activation
|
|
440
|
+
* source kinds, and output schema presence. Returns `Result.ok()` when no
|
|
441
|
+
* issues are found, or
|
|
215
442
|
* `Result.err(ValidationError)` with all issues in the error context.
|
|
216
443
|
*/
|
|
217
444
|
export const validateTopo = (topo: Topo): Result<void, ValidationError> => {
|
|
218
445
|
const issues = [
|
|
219
446
|
...checkCrosses(topo.trails, topo),
|
|
220
|
-
...
|
|
447
|
+
...checkResources(topo.trails, topo),
|
|
448
|
+
...checkContourReferences(topo.contours, topo),
|
|
221
449
|
...checkExamples(topo.trails),
|
|
222
450
|
...checkSignalOrigins(topo.signals, topo),
|
|
451
|
+
...checkSignalReferences(topo.trails, topo.signals),
|
|
452
|
+
...checkActivationSources(topo.trails),
|
|
453
|
+
...checkActivationSourceInputCompatibility(topo.trails, topo.signals),
|
|
223
454
|
];
|
|
224
455
|
|
|
225
456
|
if (issues.length === 0) {
|
package/src/validation.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { z } from 'zod';
|
|
9
9
|
|
|
10
|
+
import { BLOB_REF_SCHEMA_META_KEY, blobRefJsonSchema } from './blob-ref.js';
|
|
10
11
|
import { ValidationError } from './errors.js';
|
|
11
12
|
import { Result } from './result.js';
|
|
12
13
|
|
|
@@ -31,10 +32,57 @@ type JsonSchemaConverter = (schema: z.ZodType) => JsonSchema;
|
|
|
31
32
|
// ---------------------------------------------------------------------------
|
|
32
33
|
|
|
33
34
|
const isOptionalLike = (s: ZodInternals): boolean => {
|
|
34
|
-
|
|
35
|
+
let current = s;
|
|
36
|
+
const seen = new Set<ZodInternals>();
|
|
37
|
+
while (
|
|
38
|
+
current._zod.def['type'] === 'readonly' &&
|
|
39
|
+
!seen.has(current) &&
|
|
40
|
+
current._zod.def['innerType'] !== undefined
|
|
41
|
+
) {
|
|
42
|
+
seen.add(current);
|
|
43
|
+
current = current._zod.def['innerType'] as ZodInternals;
|
|
44
|
+
}
|
|
45
|
+
const defType = current._zod.def['type'] as string;
|
|
35
46
|
return defType === 'optional' || defType === 'default';
|
|
36
47
|
};
|
|
37
48
|
|
|
49
|
+
const getSchemaMeta = (
|
|
50
|
+
schema: z.ZodType
|
|
51
|
+
): Readonly<Record<string, unknown>> | undefined => {
|
|
52
|
+
const maybeMeta = (schema as unknown as { meta?: () => unknown }).meta;
|
|
53
|
+
if (typeof maybeMeta !== 'function') {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
const meta = maybeMeta.call(schema);
|
|
57
|
+
return typeof meta === 'object' && meta !== null
|
|
58
|
+
? (meta as Readonly<Record<string, unknown>>)
|
|
59
|
+
: undefined;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const getSchemaJsonSchemaOverride = (
|
|
63
|
+
schema: z.ZodType
|
|
64
|
+
): JsonSchema | undefined => {
|
|
65
|
+
const meta = getSchemaMeta(schema);
|
|
66
|
+
if (meta?.[BLOB_REF_SCHEMA_META_KEY] === true) {
|
|
67
|
+
const override: JsonSchema = {
|
|
68
|
+
properties: Object.fromEntries(
|
|
69
|
+
Object.entries(blobRefJsonSchema.properties).map(([key, value]) => [
|
|
70
|
+
key,
|
|
71
|
+
{ ...value },
|
|
72
|
+
])
|
|
73
|
+
),
|
|
74
|
+
required: [...blobRefJsonSchema.required],
|
|
75
|
+
type: blobRefJsonSchema.type,
|
|
76
|
+
};
|
|
77
|
+
const { description } = schema as unknown as ZodInternals;
|
|
78
|
+
if (description) {
|
|
79
|
+
override['description'] = description;
|
|
80
|
+
}
|
|
81
|
+
return override;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
};
|
|
85
|
+
|
|
38
86
|
// ---------------------------------------------------------------------------
|
|
39
87
|
// Issue formatting
|
|
40
88
|
// ---------------------------------------------------------------------------
|
|
@@ -132,6 +180,11 @@ const resolveDefault = (def: Record<string, unknown>): unknown => {
|
|
|
132
180
|
export const zodToJsonSchema: JsonSchemaConverter = (
|
|
133
181
|
schema: z.ZodType
|
|
134
182
|
): JsonSchema => {
|
|
183
|
+
const jsonSchemaOverride = getSchemaJsonSchemaOverride(schema);
|
|
184
|
+
if (jsonSchemaOverride !== undefined) {
|
|
185
|
+
return jsonSchemaOverride;
|
|
186
|
+
}
|
|
187
|
+
|
|
135
188
|
const s = schema as unknown as ZodInternals;
|
|
136
189
|
|
|
137
190
|
const collectObjectFields = (shape: Record<string, ZodInternals>) => {
|
|
@@ -197,6 +250,10 @@ export const zodToJsonSchema: JsonSchemaConverter = (
|
|
|
197
250
|
const inner = value._zod.def['innerType'] as unknown as z.ZodType;
|
|
198
251
|
return zodToJsonSchema(inner);
|
|
199
252
|
},
|
|
253
|
+
readonly: (value) => {
|
|
254
|
+
const inner = value._zod.def['innerType'] as unknown as z.ZodType;
|
|
255
|
+
return zodToJsonSchema(inner);
|
|
256
|
+
},
|
|
200
257
|
string: () => ({ type: 'string' }),
|
|
201
258
|
union: (value) => {
|
|
202
259
|
const options = value._zod.def['options'] as unknown as z.ZodType[];
|