@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.
- package/CHANGELOG.md +849 -0
- package/README.md +190 -0
- package/package.json +36 -0
- package/src/activation-provenance.ts +116 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-derivation.ts +227 -0
- package/src/activation-source.ts +93 -0
- package/src/blob-ref.ts +90 -0
- package/src/branded.ts +135 -0
- package/src/collections.ts +99 -0
- package/src/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +66 -0
- package/src/derive.ts +485 -0
- package/src/detours.ts +8 -0
- package/src/diagnostics.ts +21 -0
- package/src/draft.ts +350 -0
- package/src/entity.ts +346 -0
- package/src/error-rendering.ts +87 -0
- package/src/errors.ts +483 -0
- package/src/execute.ts +1577 -0
- package/src/fetch.ts +138 -0
- package/src/fire.ts +1172 -0
- package/src/glob.ts +81 -0
- package/src/guards.ts +37 -0
- package/src/index.ts +704 -0
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-field-rendering.ts +193 -0
- package/src/layer.ts +81 -0
- package/src/observe.ts +361 -0
- package/src/path-scope.ts +66 -0
- package/src/path-security.ts +98 -0
- package/src/patterns/bulk.ts +16 -0
- package/src/patterns/change.ts +12 -0
- package/src/patterns/date-range.ts +12 -0
- package/src/patterns/index.ts +8 -0
- package/src/patterns/pagination.ts +22 -0
- package/src/patterns/progress.ts +13 -0
- package/src/patterns/sorting.ts +14 -0
- package/src/patterns/status.ts +11 -0
- package/src/patterns/timestamps.ts +12 -0
- package/src/permits.ts +12 -0
- package/src/queue.ts +163 -0
- package/src/redaction/index.ts +3 -0
- package/src/redaction/patterns.ts +50 -0
- package/src/redaction/redactor.ts +178 -0
- package/src/resilience.ts +234 -0
- package/src/resource-config.ts +804 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +212 -0
- package/src/run.ts +76 -0
- package/src/runtime-builtins.ts +69 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +265 -0
- package/src/sha256.ts +136 -0
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +111 -0
- package/src/signal.ts +104 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/structured-examples.ts +248 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +101 -0
- package/src/surface-overlay.ts +694 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +835 -0
- package/src/tracing.ts +346 -0
- package/src/trail-id-glob.ts +15 -0
- package/src/trail.ts +1351 -0
- package/src/trails/derive-trail.ts +835 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +152 -0
- package/src/trails-db.ts +212 -0
- package/src/transport-error-map.ts +163 -0
- package/src/type-utils.ts +87 -0
- package/src/types.ts +300 -0
- package/src/validate-established-topo.ts +73 -0
- package/src/validate-topo.ts +725 -0
- package/src/validation.ts +330 -0
- package/src/version-marker.ts +716 -0
- package/src/version-resolution.ts +308 -0
- package/src/version-runtime.ts +120 -0
- package/src/webhook.ts +461 -0
- package/src/workspace.ts +244 -0
- package/src/zod-wrappers.ts +72 -0
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { AnySignal, Signal } from './signal.js';
|
|
2
|
+
|
|
3
|
+
export interface LateBoundSignalRef {
|
|
4
|
+
readonly kind: 'store-derived';
|
|
5
|
+
readonly token: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface LateBoundSignalMarker {
|
|
9
|
+
readonly displayId: string;
|
|
10
|
+
readonly token: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LATE_BOUND_SIGNAL_REF = Symbol('trails.late-bound-signal-ref');
|
|
14
|
+
const LATE_BOUND_SIGNAL_BOUND = Symbol('trails.late-bound-signal-bound');
|
|
15
|
+
const LATE_BOUND_SIGNAL_MARKER_PREFIX = '@@trails:late-bound-signal-ref:';
|
|
16
|
+
|
|
17
|
+
const defineLateBoundSignalRef = <T extends object>(
|
|
18
|
+
value: T,
|
|
19
|
+
ref: LateBoundSignalRef
|
|
20
|
+
): T => {
|
|
21
|
+
Object.defineProperty(value, LATE_BOUND_SIGNAL_REF, {
|
|
22
|
+
configurable: false,
|
|
23
|
+
enumerable: false,
|
|
24
|
+
value: ref,
|
|
25
|
+
writable: false,
|
|
26
|
+
});
|
|
27
|
+
return value;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const defineLateBoundSignalBound = <T extends object>(value: T): T => {
|
|
31
|
+
Object.defineProperty(value, LATE_BOUND_SIGNAL_BOUND, {
|
|
32
|
+
configurable: false,
|
|
33
|
+
enumerable: false,
|
|
34
|
+
value: true,
|
|
35
|
+
writable: false,
|
|
36
|
+
});
|
|
37
|
+
return value;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const isBoundLateBoundSignal = (
|
|
41
|
+
signal: Pick<AnySignal, 'id'> | undefined
|
|
42
|
+
): boolean =>
|
|
43
|
+
signal !== undefined &&
|
|
44
|
+
(signal as Record<PropertyKey, unknown>)[LATE_BOUND_SIGNAL_BOUND] === true;
|
|
45
|
+
|
|
46
|
+
export const getLateBoundSignalRef = (
|
|
47
|
+
signal: Pick<AnySignal, 'id'> | undefined
|
|
48
|
+
): LateBoundSignalRef | undefined =>
|
|
49
|
+
signal === undefined
|
|
50
|
+
? undefined
|
|
51
|
+
: ((signal as Record<PropertyKey, unknown>)[LATE_BOUND_SIGNAL_REF] as
|
|
52
|
+
| LateBoundSignalRef
|
|
53
|
+
| undefined);
|
|
54
|
+
|
|
55
|
+
export const attachLateBoundSignalRef = <T>(
|
|
56
|
+
signal: Signal<T>,
|
|
57
|
+
ref: LateBoundSignalRef
|
|
58
|
+
): Signal<T> =>
|
|
59
|
+
Object.freeze(
|
|
60
|
+
defineLateBoundSignalRef(
|
|
61
|
+
{
|
|
62
|
+
...signal,
|
|
63
|
+
},
|
|
64
|
+
ref
|
|
65
|
+
)
|
|
66
|
+
) as Signal<T>;
|
|
67
|
+
|
|
68
|
+
export const cloneSignalWithId = <T>(
|
|
69
|
+
signal: Signal<T>,
|
|
70
|
+
id: string
|
|
71
|
+
): Signal<T> => {
|
|
72
|
+
const clone = {
|
|
73
|
+
...signal,
|
|
74
|
+
id,
|
|
75
|
+
};
|
|
76
|
+
const ref = getLateBoundSignalRef(signal);
|
|
77
|
+
return Object.freeze(
|
|
78
|
+
ref
|
|
79
|
+
? defineLateBoundSignalBound(defineLateBoundSignalRef(clone, ref))
|
|
80
|
+
: clone
|
|
81
|
+
) as Signal<T>;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export const createLateBoundSignalMarker = (
|
|
85
|
+
ref: LateBoundSignalRef,
|
|
86
|
+
displayId: string
|
|
87
|
+
): string =>
|
|
88
|
+
`${LATE_BOUND_SIGNAL_MARKER_PREFIX}${encodeURIComponent(ref.token)}:${displayId}`;
|
|
89
|
+
|
|
90
|
+
export const parseLateBoundSignalMarker = (
|
|
91
|
+
value: string
|
|
92
|
+
): LateBoundSignalMarker | null => {
|
|
93
|
+
if (!value.startsWith(LATE_BOUND_SIGNAL_MARKER_PREFIX)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const remainder = value.slice(LATE_BOUND_SIGNAL_MARKER_PREFIX.length);
|
|
98
|
+
const separator = remainder.indexOf(':');
|
|
99
|
+
if (separator <= 0 || separator === remainder.length - 1) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const token = decodeURIComponent(remainder.slice(0, separator));
|
|
105
|
+
return token.length === 0
|
|
106
|
+
? null
|
|
107
|
+
: { displayId: remainder.slice(separator + 1), token };
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
};
|
package/src/signal.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Signal — a named payload schema with optional provenance meta.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
const formatExampleIssues = (issues: readonly z.core.$ZodIssue[]): string =>
|
|
8
|
+
issues
|
|
9
|
+
.map((issue) => {
|
|
10
|
+
const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
|
|
11
|
+
return `${path}: ${issue.message}`;
|
|
12
|
+
})
|
|
13
|
+
.join('; ');
|
|
14
|
+
|
|
15
|
+
const assertSignalExamples = <T>(
|
|
16
|
+
id: string,
|
|
17
|
+
payload: z.ZodType<T>,
|
|
18
|
+
examples: readonly T[]
|
|
19
|
+
): void => {
|
|
20
|
+
for (const [index, example] of examples.entries()) {
|
|
21
|
+
const parsed = payload.safeParse(example);
|
|
22
|
+
if (!parsed.success) {
|
|
23
|
+
throw new TypeError(
|
|
24
|
+
`signal("${id}") example ${index} is invalid: ${formatExampleIssues(parsed.error.issues)}`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Spec (input to the factory)
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export interface SignalSpec<T> {
|
|
35
|
+
readonly payload: z.ZodType<T>;
|
|
36
|
+
readonly description?: string | undefined;
|
|
37
|
+
/** Example payloads validated against the signal payload schema. */
|
|
38
|
+
readonly examples?: readonly T[] | undefined;
|
|
39
|
+
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
40
|
+
/** Trail IDs that produce this signal (e.g. the trails it originates from). */
|
|
41
|
+
readonly from?: readonly string[] | undefined;
|
|
42
|
+
/** Reserved for future signal-specific design; trail versioning is trail-only. */
|
|
43
|
+
readonly version?: never;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Shape (output of the factory)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
export interface Signal<T> {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly kind: 'signal';
|
|
53
|
+
readonly payload: z.ZodType<T>;
|
|
54
|
+
readonly description?: string | undefined;
|
|
55
|
+
/** Example payloads validated against the signal payload schema. */
|
|
56
|
+
readonly examples?: readonly T[] | undefined;
|
|
57
|
+
readonly meta?: Readonly<Record<string, unknown>> | undefined;
|
|
58
|
+
/** Trail IDs that produce this signal (e.g. the trails it originates from). */
|
|
59
|
+
readonly from?: readonly string[] | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// Factory
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a signal definition.
|
|
68
|
+
*
|
|
69
|
+
* A signal is a named payload schema describing something that happened.
|
|
70
|
+
* Returns a frozen object with `kind: "signal"` and all spec fields.
|
|
71
|
+
*/
|
|
72
|
+
export function signal<T>(id: string, spec: SignalSpec<T>): Signal<T>;
|
|
73
|
+
export function signal<T>(
|
|
74
|
+
spec: SignalSpec<T> & { readonly id: string }
|
|
75
|
+
): Signal<T>;
|
|
76
|
+
export function signal<T>(
|
|
77
|
+
idOrSpec: string | (SignalSpec<T> & { readonly id: string }),
|
|
78
|
+
maybeSpec?: SignalSpec<T>
|
|
79
|
+
): Signal<T> {
|
|
80
|
+
const resolvedId = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
|
|
81
|
+
// oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
|
|
82
|
+
const resolvedSpec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
|
|
83
|
+
if (resolvedSpec.examples !== undefined) {
|
|
84
|
+
assertSignalExamples(
|
|
85
|
+
resolvedId,
|
|
86
|
+
resolvedSpec.payload,
|
|
87
|
+
resolvedSpec.examples
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
description: resolvedSpec.description,
|
|
92
|
+
examples: resolvedSpec.examples
|
|
93
|
+
? Object.freeze([...resolvedSpec.examples])
|
|
94
|
+
: undefined,
|
|
95
|
+
from: resolvedSpec.from ? Object.freeze([...resolvedSpec.from]) : undefined,
|
|
96
|
+
id: resolvedId,
|
|
97
|
+
kind: 'signal' as const,
|
|
98
|
+
meta: resolvedSpec.meta,
|
|
99
|
+
payload: resolvedSpec.payload,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Existential type for heterogeneous signal collections */
|
|
104
|
+
export type AnySignal = Signal<unknown>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural accessor protocol used by `deriveTrail()` to synthesize default
|
|
3
|
+
* implementations for standard CRUD operations without depending on `@ontrails/store`.
|
|
4
|
+
*
|
|
5
|
+
* @remarks
|
|
6
|
+
* This type is intentionally minimal and structural. `@ontrails/store`'s
|
|
7
|
+
* `StoreAccessor` / `StoreTableAccessor` interfaces satisfy this protocol
|
|
8
|
+
* through a compile-time extends check (see `packages/store/src/types.ts`).
|
|
9
|
+
* Keeping the protocol in core avoids a core → store dependency while still
|
|
10
|
+
* allowing the derivation helper to call accessors by convention.
|
|
11
|
+
*
|
|
12
|
+
* Required methods (`get`, `list`, `upsert`, `remove`) match the
|
|
13
|
+
* backend-agnostic write contract every bound store must expose. Optional
|
|
14
|
+
* methods (`insert`, `update`) are declared by tabular adapters that
|
|
15
|
+
* distinguish create-only and patch-only operations from the generalized
|
|
16
|
+
* `upsert` contract.
|
|
17
|
+
*/
|
|
18
|
+
export interface StoreAccessorProtocol<
|
|
19
|
+
TInput,
|
|
20
|
+
TEntity,
|
|
21
|
+
TId,
|
|
22
|
+
TFilters = unknown,
|
|
23
|
+
> {
|
|
24
|
+
/** Retrieve a single entity by identity. Returns `null` when not found. */
|
|
25
|
+
get(id: TId): Promise<TEntity | null>;
|
|
26
|
+
/** List entities, optionally filtered. Returns `[]` when no rows match. */
|
|
27
|
+
list(filters?: TFilters): Promise<readonly TEntity[]>;
|
|
28
|
+
/** Create-or-replace one entity using the backend-agnostic contract. */
|
|
29
|
+
upsert(input: TInput): Promise<TEntity>;
|
|
30
|
+
/**
|
|
31
|
+
* Remove an entity by identity. Returns `{ deleted: true }` when the row
|
|
32
|
+
* was found and removed, `{ deleted: false }` when no matching row
|
|
33
|
+
* existed (not an error).
|
|
34
|
+
*/
|
|
35
|
+
remove(id: TId): Promise<{ readonly deleted: boolean }>;
|
|
36
|
+
/**
|
|
37
|
+
* Optional insert — available on tabular adapters that distinguish
|
|
38
|
+
* create from update. When absent, synthesized implementations fall back to
|
|
39
|
+
* `upsert`.
|
|
40
|
+
*/
|
|
41
|
+
insert?(input: TInput): Promise<TEntity>;
|
|
42
|
+
/**
|
|
43
|
+
* Optional patch-by-identity — available on tabular adapters. Returns
|
|
44
|
+
* `null` when no row with the given identity exists. When absent,
|
|
45
|
+
* synthesized update implementations fall back to `get` + merge + `upsert`.
|
|
46
|
+
*/
|
|
47
|
+
update?(id: TId, patch: Partial<TInput>): Promise<TEntity | null>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Record shape returned by a resource's `from(ctx)` call, keyed by accessor
|
|
52
|
+
* name. Used by `deriveTrail()` to resolve an accessor by entity name.
|
|
53
|
+
*/
|
|
54
|
+
export type StoreAccessorRecord = Readonly<
|
|
55
|
+
Record<string, StoreAccessorProtocol<unknown, unknown, unknown, unknown>>
|
|
56
|
+
>;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { TrailExample, TrailExampleSignalAssertion } from './trail.js';
|
|
2
|
+
|
|
3
|
+
export interface StructuredTrailExampleProvenance {
|
|
4
|
+
readonly source: 'trail.examples' | 'trail.versions.examples';
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface StructuredSignalExampleProvenance {
|
|
8
|
+
readonly source: 'signal.examples';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type StructuredTrailExampleKind = 'success' | 'error';
|
|
12
|
+
|
|
13
|
+
export interface StructuredTrailExample {
|
|
14
|
+
readonly description?: string | undefined;
|
|
15
|
+
readonly error?: string | undefined;
|
|
16
|
+
readonly expected?: unknown | undefined;
|
|
17
|
+
readonly expectedMatch?: unknown | undefined;
|
|
18
|
+
readonly input: unknown;
|
|
19
|
+
readonly kind: StructuredTrailExampleKind;
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly provenance: StructuredTrailExampleProvenance;
|
|
22
|
+
readonly signals?:
|
|
23
|
+
| readonly StructuredTrailExampleSignalAssertion[]
|
|
24
|
+
| undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface StructuredTrailExampleSignalAssertion {
|
|
28
|
+
readonly payload?: unknown | undefined;
|
|
29
|
+
readonly payloadMatch?: unknown | undefined;
|
|
30
|
+
readonly signalId: string;
|
|
31
|
+
readonly times?: number | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface StructuredSignalExample {
|
|
35
|
+
readonly kind: 'payload';
|
|
36
|
+
readonly payload: unknown;
|
|
37
|
+
readonly provenance: StructuredSignalExampleProvenance;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// `Date`, `RegExp`, `Map`, and `Set` are objects (`typeof === 'object'`),
|
|
41
|
+
// so they would pass the structural walk and reach `JSON.stringify`, which
|
|
42
|
+
// silently coerces them: a `Date` becomes its ISO string, a `RegExp` and
|
|
43
|
+
// any `Map`/`Set` become `{}`. Either way the derived shape diverges
|
|
44
|
+
// from the example author's declared input. Treat them as non-serializable
|
|
45
|
+
// leaves so the example is dropped rather than misrepresented to MCP
|
|
46
|
+
// clients.
|
|
47
|
+
const isNonJsonLeaf = (value: unknown): boolean => {
|
|
48
|
+
const kind = typeof value;
|
|
49
|
+
if (kind === 'function' || kind === 'symbol' || kind === 'bigint') {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return (
|
|
53
|
+
value instanceof Date ||
|
|
54
|
+
value instanceof RegExp ||
|
|
55
|
+
value instanceof Map ||
|
|
56
|
+
value instanceof Set
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// `JSON.stringify` silently drops function / symbol property values rather than
|
|
61
|
+
// throwing, so we walk the value first and reject any example whose graph
|
|
62
|
+
// contains a non-serializable leaf. Without this, an MCP client consuming
|
|
63
|
+
// `ontrails/examples` would receive structurally incorrect inputs.
|
|
64
|
+
const containsNonSerializableLeaf = (
|
|
65
|
+
value: unknown,
|
|
66
|
+
seen: WeakSet<object>
|
|
67
|
+
): boolean => {
|
|
68
|
+
if (isNonJsonLeaf(value)) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (value === null || typeof value !== 'object') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
const obj = value as object;
|
|
75
|
+
if (seen.has(obj)) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
seen.add(obj);
|
|
79
|
+
if (Array.isArray(value)) {
|
|
80
|
+
return value.some((entry) => containsNonSerializableLeaf(entry, seen));
|
|
81
|
+
}
|
|
82
|
+
return Object.values(obj).some((entry) =>
|
|
83
|
+
containsNonSerializableLeaf(entry, seen)
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const toJsonSerializable = (value: unknown): unknown | undefined => {
|
|
88
|
+
if (containsNonSerializableLeaf(value, new WeakSet<object>())) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const encoded = JSON.stringify(value);
|
|
93
|
+
return encoded === undefined ? undefined : JSON.parse(encoded);
|
|
94
|
+
} catch {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const signalIdFromAssertion = (
|
|
100
|
+
assertion: TrailExampleSignalAssertion
|
|
101
|
+
): string | undefined => {
|
|
102
|
+
if (typeof assertion.signal === 'string') {
|
|
103
|
+
return assertion.signal;
|
|
104
|
+
}
|
|
105
|
+
return typeof assertion.signal.id === 'string'
|
|
106
|
+
? assertion.signal.id
|
|
107
|
+
: undefined;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const deriveSignalAssertion = (
|
|
111
|
+
assertion: TrailExampleSignalAssertion
|
|
112
|
+
): StructuredTrailExampleSignalAssertion | undefined => {
|
|
113
|
+
const signalId = signalIdFromAssertion(assertion);
|
|
114
|
+
if (signalId === undefined) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const derived: Record<string, unknown> = { signalId };
|
|
119
|
+
if (assertion.payload !== undefined) {
|
|
120
|
+
const payload = toJsonSerializable(assertion.payload);
|
|
121
|
+
if (payload === undefined) {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
derived['payload'] = payload;
|
|
125
|
+
}
|
|
126
|
+
if (assertion.payloadMatch !== undefined) {
|
|
127
|
+
const payloadMatch = toJsonSerializable(assertion.payloadMatch);
|
|
128
|
+
if (payloadMatch === undefined) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
derived['payloadMatch'] = payloadMatch;
|
|
132
|
+
}
|
|
133
|
+
if (assertion.times !== undefined) {
|
|
134
|
+
derived['times'] = assertion.times;
|
|
135
|
+
}
|
|
136
|
+
return Object.freeze(
|
|
137
|
+
derived
|
|
138
|
+
) as unknown as StructuredTrailExampleSignalAssertion;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
const deriveSignalAssertions = (
|
|
142
|
+
assertions: readonly TrailExampleSignalAssertion[] | undefined
|
|
143
|
+
): readonly StructuredTrailExampleSignalAssertion[] | undefined => {
|
|
144
|
+
if (assertions === undefined) {
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
const derived = assertions.map(deriveSignalAssertion);
|
|
148
|
+
if (derived.some((assertion) => assertion === undefined)) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
return Object.freeze(derived as StructuredTrailExampleSignalAssertion[]);
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const deriveExample = (
|
|
155
|
+
example: TrailExample<unknown, unknown>,
|
|
156
|
+
provenance: StructuredTrailExampleProvenance
|
|
157
|
+
): StructuredTrailExample | undefined => {
|
|
158
|
+
const input = toJsonSerializable(example.input);
|
|
159
|
+
if (input === undefined) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const derived: Record<string, unknown> = {
|
|
164
|
+
input,
|
|
165
|
+
kind: example.error === undefined ? 'success' : 'error',
|
|
166
|
+
name: example.name,
|
|
167
|
+
provenance,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (example.description !== undefined) {
|
|
171
|
+
derived['description'] = example.description;
|
|
172
|
+
}
|
|
173
|
+
if (example.expected !== undefined) {
|
|
174
|
+
const expected = toJsonSerializable(example.expected);
|
|
175
|
+
if (expected === undefined) {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
derived['expected'] = expected;
|
|
179
|
+
}
|
|
180
|
+
if (example.expectedMatch !== undefined) {
|
|
181
|
+
const expectedMatch = toJsonSerializable(example.expectedMatch);
|
|
182
|
+
if (expectedMatch === undefined) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
derived['expectedMatch'] = expectedMatch;
|
|
186
|
+
}
|
|
187
|
+
if (example.error !== undefined) {
|
|
188
|
+
derived['error'] = example.error;
|
|
189
|
+
}
|
|
190
|
+
if (example.signals !== undefined) {
|
|
191
|
+
const signals = deriveSignalAssertions(example.signals);
|
|
192
|
+
if (signals === undefined) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
derived['signals'] = signals;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return Object.freeze(derived) as unknown as StructuredTrailExample;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const deriveSignalExample = (
|
|
202
|
+
payload: unknown
|
|
203
|
+
): StructuredSignalExample | undefined => {
|
|
204
|
+
const serializablePayload = toJsonSerializable(payload);
|
|
205
|
+
if (serializablePayload === undefined) {
|
|
206
|
+
return undefined;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return Object.freeze({
|
|
210
|
+
kind: 'payload',
|
|
211
|
+
payload: serializablePayload,
|
|
212
|
+
provenance: { source: 'signal.examples' },
|
|
213
|
+
} satisfies StructuredSignalExample);
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
export const deriveStructuredTrailExamples = (
|
|
217
|
+
examples: readonly TrailExample<unknown, unknown>[] | undefined,
|
|
218
|
+
options?: { readonly provenance?: StructuredTrailExampleProvenance }
|
|
219
|
+
): readonly StructuredTrailExample[] | undefined => {
|
|
220
|
+
if (examples === undefined || examples.length === 0) {
|
|
221
|
+
return undefined;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const provenance = options?.provenance ?? { source: 'trail.examples' };
|
|
225
|
+
const derived = examples
|
|
226
|
+
.map((example) => deriveExample(example, provenance))
|
|
227
|
+
.filter(
|
|
228
|
+
(example): example is StructuredTrailExample => example !== undefined
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return derived.length > 0 ? Object.freeze(derived) : undefined;
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
export const deriveStructuredSignalExamples = (
|
|
235
|
+
examples: readonly unknown[] | undefined
|
|
236
|
+
): readonly StructuredSignalExample[] | undefined => {
|
|
237
|
+
if (examples === undefined || examples.length === 0) {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const derived = examples
|
|
242
|
+
.map(deriveSignalExample)
|
|
243
|
+
.filter(
|
|
244
|
+
(example): example is StructuredSignalExample => example !== undefined
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return derived.length > 0 ? Object.freeze(derived) : undefined;
|
|
248
|
+
};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Result } from './result.js';
|
|
2
|
+
import type { Layer } from './layer.js';
|
|
3
|
+
import type { Intent } from './trail.js';
|
|
4
|
+
import type { Topo } from './topo.js';
|
|
5
|
+
import type { SurfaceName } from './transport-error-map.js';
|
|
6
|
+
import type { TrailContextInit } from './types.js';
|
|
7
|
+
import { SURFACE_KEY, SURFACE_LAYER_NAMES_KEY } from './types.js';
|
|
8
|
+
import { validateEstablishedTopo } from './validate-established-topo.js';
|
|
9
|
+
|
|
10
|
+
export type SurfaceConfigValues = Readonly<
|
|
11
|
+
Record<string, Record<string, unknown>>
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
export interface SurfaceSelectionOptions {
|
|
15
|
+
/** Glob patterns that remove matching trail IDs. */
|
|
16
|
+
readonly exclude?: readonly string[] | undefined;
|
|
17
|
+
/** Glob patterns that keep only matching trail IDs when provided. */
|
|
18
|
+
readonly include?: readonly string[] | undefined;
|
|
19
|
+
/** Allowed intents for exposed surfaces. Empty arrays act as no filter. */
|
|
20
|
+
readonly intent?: readonly Intent[] | undefined;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SurfaceValidationOptions {
|
|
24
|
+
/** Set to `false` to skip established-topo validation during derivation. */
|
|
25
|
+
readonly validate?: boolean | undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface BaseSurfaceOptions
|
|
29
|
+
extends SurfaceSelectionOptions, SurfaceValidationOptions {
|
|
30
|
+
/** Config values for resources that declare a `config` schema, keyed by resource ID. */
|
|
31
|
+
readonly configValues?: SurfaceConfigValues | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const shouldValidateSurfaceTopo = (
|
|
35
|
+
options?: SurfaceValidationOptions
|
|
36
|
+
): boolean => options?.validate !== false;
|
|
37
|
+
|
|
38
|
+
export const validateSurfaceTopo = (
|
|
39
|
+
graph: Topo,
|
|
40
|
+
options?: SurfaceValidationOptions
|
|
41
|
+
): Result<void, Error> => {
|
|
42
|
+
if (!shouldValidateSurfaceTopo(options)) {
|
|
43
|
+
return Result.ok();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const validated = validateEstablishedTopo(graph);
|
|
47
|
+
return validated.isErr() ? Result.err(validated.error) : Result.ok();
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type SurfaceMarkedContext = Partial<TrailContextInit> & {
|
|
51
|
+
readonly extensions: Readonly<Record<string, unknown>>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const withSurfaceMarker = (
|
|
55
|
+
surface: SurfaceName,
|
|
56
|
+
ctx: Partial<TrailContextInit> = {}
|
|
57
|
+
): SurfaceMarkedContext => ({
|
|
58
|
+
...ctx,
|
|
59
|
+
extensions: {
|
|
60
|
+
...ctx.extensions,
|
|
61
|
+
[SURFACE_KEY]: surface,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const readSurfaceLayerNameRecord = (
|
|
66
|
+
value: unknown
|
|
67
|
+
): Record<string, readonly string[]> =>
|
|
68
|
+
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
69
|
+
? (value as Record<string, readonly string[]>)
|
|
70
|
+
: {};
|
|
71
|
+
|
|
72
|
+
export const withSurfaceLayerNames = (
|
|
73
|
+
surface: SurfaceName,
|
|
74
|
+
layers: readonly Layer[],
|
|
75
|
+
ctx: Partial<TrailContextInit> = {}
|
|
76
|
+
): SurfaceMarkedContext => {
|
|
77
|
+
const existing = readSurfaceLayerNameRecord(
|
|
78
|
+
ctx.extensions?.[SURFACE_LAYER_NAMES_KEY]
|
|
79
|
+
);
|
|
80
|
+
return {
|
|
81
|
+
...ctx,
|
|
82
|
+
extensions: {
|
|
83
|
+
...ctx.extensions,
|
|
84
|
+
[SURFACE_KEY]: surface,
|
|
85
|
+
[SURFACE_LAYER_NAMES_KEY]: {
|
|
86
|
+
...existing,
|
|
87
|
+
[surface]: layers.map((layer) => layer.name),
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
};
|