@ontrails/core 1.0.0-beta.0 → 1.0.0-beta.10
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/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +109 -0
- package/README.md +101 -131
- package/dist/derive.d.ts +1 -1
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +4 -1
- package/dist/derive.js.map +1 -1
- package/dist/dispatch.d.ts +27 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +34 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/event.d.ts +2 -2
- package/dist/event.d.ts.map +1 -1
- package/dist/event.js +1 -1
- package/dist/event.js.map +1 -1
- package/dist/execute.d.ts +30 -0
- package/dist/execute.d.ts.map +1 -0
- package/dist/execute.js +64 -0
- package/dist/execute.js.map +1 -0
- package/dist/index.d.ts +10 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -2
- package/dist/index.js.map +1 -1
- package/dist/patterns/status.d.ts +1 -1
- package/dist/result.d.ts +11 -11
- package/dist/result.d.ts.map +1 -1
- package/dist/result.js +15 -4
- package/dist/result.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +45 -7
- package/dist/serialization.js.map +1 -1
- package/dist/topo.d.ts +4 -4
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +12 -16
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +16 -10
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +4 -2
- package/dist/trail.js.map +1 -1
- package/dist/type-utils.d.ts +24 -0
- package/dist/type-utils.d.ts.map +1 -0
- package/dist/type-utils.js +12 -0
- package/dist/type-utils.js.map +1 -0
- package/dist/types.d.ts +1 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validate-topo.d.ts +2 -2
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +59 -9
- package/dist/validate-topo.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/context.test.ts +4 -5
- package/src/__tests__/derive.test.ts +44 -0
- package/src/__tests__/dispatch.test.ts +154 -0
- package/src/__tests__/event.test.ts +5 -5
- package/src/__tests__/execute.test.ts +208 -0
- package/src/__tests__/layer.test.ts +11 -111
- package/src/__tests__/serialization.test.ts +166 -1
- package/src/__tests__/topo.test.ts +101 -79
- package/src/__tests__/trail.test.ts +73 -35
- package/src/__tests__/type-utils.test.ts +90 -0
- package/src/__tests__/validate-topo.test.ts +97 -20
- package/src/derive.ts +12 -2
- package/src/dispatch.ts +54 -0
- package/src/event.ts +3 -3
- package/src/execute.ts +96 -0
- package/src/index.ts +22 -18
- package/src/result.ts +29 -15
- package/src/serialization.ts +56 -11
- package/src/topo.ts +18 -23
- package/src/trail.ts +24 -13
- package/src/type-utils.ts +45 -0
- package/src/types.ts +1 -3
- package/src/validate-topo.ts +70 -10
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/hike.d.ts +0 -36
- package/dist/hike.d.ts.map +0 -1
- package/dist/hike.js +0 -20
- package/dist/hike.js.map +0 -1
- package/src/__tests__/hike.test.ts +0 -117
- package/src/__tests__/job.test.ts +0 -98
- package/src/adapters.ts +0 -68
- package/src/health.ts +0 -23
- package/src/hike.ts +0 -77
- package/src/job.ts +0 -20
package/src/topo.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
import { ValidationError } from './errors.js';
|
|
6
6
|
import type { AnyEvent } from './event.js';
|
|
7
|
-
import type { AnyHike } from './hike.js';
|
|
8
7
|
import type { AnyTrail } from './trail.js';
|
|
9
8
|
|
|
10
9
|
// ---------------------------------------------------------------------------
|
|
@@ -14,11 +13,12 @@ import type { AnyTrail } from './trail.js';
|
|
|
14
13
|
export interface Topo {
|
|
15
14
|
readonly name: string;
|
|
16
15
|
readonly trails: ReadonlyMap<string, AnyTrail>;
|
|
17
|
-
readonly hikes: ReadonlyMap<string, AnyHike>;
|
|
18
16
|
readonly events: ReadonlyMap<string, AnyEvent>;
|
|
19
|
-
|
|
17
|
+
readonly count: number;
|
|
18
|
+
get(id: string): AnyTrail | undefined;
|
|
20
19
|
has(id: string): boolean;
|
|
21
|
-
|
|
20
|
+
ids(): string[];
|
|
21
|
+
list(): AnyTrail[];
|
|
22
22
|
listEvents(): AnyEvent[];
|
|
23
23
|
}
|
|
24
24
|
|
|
@@ -26,14 +26,14 @@ export interface Topo {
|
|
|
26
26
|
// Kind discriminant check
|
|
27
27
|
// ---------------------------------------------------------------------------
|
|
28
28
|
|
|
29
|
-
type Registrable = AnyTrail |
|
|
29
|
+
type Registrable = AnyTrail | AnyEvent;
|
|
30
30
|
|
|
31
31
|
const isRegistrable = (value: unknown): value is Registrable => {
|
|
32
32
|
if (typeof value !== 'object' || value === null) {
|
|
33
33
|
return false;
|
|
34
34
|
}
|
|
35
35
|
const { kind } = value as Record<string, unknown>;
|
|
36
|
-
return kind === 'trail' || kind === '
|
|
36
|
+
return kind === 'trail' || kind === 'event';
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
// ---------------------------------------------------------------------------
|
|
@@ -43,20 +43,23 @@ const isRegistrable = (value: unknown): value is Registrable => {
|
|
|
43
43
|
const createTopo = (
|
|
44
44
|
name: string,
|
|
45
45
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
46
|
-
hikes: ReadonlyMap<string, AnyHike>,
|
|
47
46
|
events: ReadonlyMap<string, AnyEvent>
|
|
48
47
|
): Topo => ({
|
|
48
|
+
count: trails.size,
|
|
49
49
|
events,
|
|
50
|
-
get(id: string): AnyTrail |
|
|
51
|
-
return trails.get(id)
|
|
50
|
+
get(id: string): AnyTrail | undefined {
|
|
51
|
+
return trails.get(id);
|
|
52
52
|
},
|
|
53
53
|
has(id: string): boolean {
|
|
54
|
-
return trails.has(id)
|
|
54
|
+
return trails.has(id);
|
|
55
55
|
},
|
|
56
|
-
hikes,
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
return [...trails.
|
|
57
|
+
ids(): string[] {
|
|
58
|
+
return [...trails.keys()];
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
list(): AnyTrail[] {
|
|
62
|
+
return [...trails.values()];
|
|
60
63
|
},
|
|
61
64
|
|
|
62
65
|
listEvents(): AnyEvent[] {
|
|
@@ -76,7 +79,6 @@ const createTopo = (
|
|
|
76
79
|
const register = (
|
|
77
80
|
value: Registrable,
|
|
78
81
|
trails: Map<string, AnyTrail>,
|
|
79
|
-
hikes: Map<string, AnyHike>,
|
|
80
82
|
events: Map<string, AnyEvent>
|
|
81
83
|
): void => {
|
|
82
84
|
const { id } = value as { id: string };
|
|
@@ -87,12 +89,6 @@ const register = (
|
|
|
87
89
|
}
|
|
88
90
|
events.set(id, value as AnyEvent);
|
|
89
91
|
},
|
|
90
|
-
hike: () => {
|
|
91
|
-
if (hikes.has(id)) {
|
|
92
|
-
throw new ValidationError(`Duplicate hike ID: "${id}"`);
|
|
93
|
-
}
|
|
94
|
-
hikes.set(id, value as AnyHike);
|
|
95
|
-
},
|
|
96
92
|
trail: () => {
|
|
97
93
|
if (trails.has(id)) {
|
|
98
94
|
throw new ValidationError(`Duplicate trail ID: "${id}"`);
|
|
@@ -108,16 +104,15 @@ export const topo = (
|
|
|
108
104
|
...modules: Record<string, unknown>[]
|
|
109
105
|
): Topo => {
|
|
110
106
|
const trails = new Map<string, AnyTrail>();
|
|
111
|
-
const hikes = new Map<string, AnyHike>();
|
|
112
107
|
const events = new Map<string, AnyEvent>();
|
|
113
108
|
|
|
114
109
|
for (const mod of modules) {
|
|
115
110
|
for (const value of Object.values(mod)) {
|
|
116
111
|
if (isRegistrable(value)) {
|
|
117
|
-
register(value, trails,
|
|
112
|
+
register(value, trails, events);
|
|
118
113
|
}
|
|
119
114
|
}
|
|
120
115
|
}
|
|
121
116
|
|
|
122
|
-
return createTopo(name, trails,
|
|
117
|
+
return createTopo(name, trails, events);
|
|
123
118
|
};
|
package/src/trail.ts
CHANGED
|
@@ -39,34 +39,44 @@ export interface TrailSpec<I, O> {
|
|
|
39
39
|
/** Zod schema for validating output (optional — some trails are fire-and-forget) */
|
|
40
40
|
readonly output?: z.ZodType<O> | undefined;
|
|
41
41
|
/** The pure function that does the work (sync or async authoring) */
|
|
42
|
-
readonly
|
|
42
|
+
readonly run: Implementation<I, O>;
|
|
43
43
|
/** Human-readable description */
|
|
44
44
|
readonly description?: string | undefined;
|
|
45
45
|
/** Named examples for docs and testing */
|
|
46
46
|
readonly examples?: readonly TrailExample<I, O>[] | undefined;
|
|
47
|
-
/**
|
|
48
|
-
readonly
|
|
49
|
-
/** Trail is destructive (deletes or overwrites data) */
|
|
50
|
-
readonly destructive?: boolean | undefined;
|
|
47
|
+
/** What this trail does to the world: read, write (default), or destroy */
|
|
48
|
+
readonly intent?: 'read' | 'write' | 'destroy' | undefined;
|
|
51
49
|
/** Trail is idempotent (safe to retry) */
|
|
52
50
|
readonly idempotent?: boolean | undefined;
|
|
53
51
|
/** Arbitrary metadata for tooling and filtering */
|
|
54
|
-
readonly
|
|
52
|
+
readonly metadata?: Readonly<Record<string, unknown>> | undefined;
|
|
55
53
|
/** Named sets of downstream trail IDs that may be invoked */
|
|
56
54
|
readonly detours?: Readonly<Record<string, readonly string[]>> | undefined;
|
|
57
55
|
/** Per-field overrides for deriveFields() (labels, hints, options) */
|
|
58
56
|
readonly fields?: Readonly<Record<string, FieldOverride>> | undefined;
|
|
57
|
+
/** IDs of downstream trails this trail may invoke via ctx.follow() */
|
|
58
|
+
readonly follow?: readonly string[] | undefined;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
// ---------------------------------------------------------------------------
|
|
62
62
|
// Trail (the frozen runtime object)
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
|
|
65
|
+
/** Intent describes what a trail does to the world */
|
|
66
|
+
export type Intent = 'read' | 'write' | 'destroy';
|
|
67
|
+
|
|
65
68
|
/** A fully-defined trail — the unit of work in the Trails system */
|
|
66
|
-
export interface Trail<I, O> extends Omit<
|
|
69
|
+
export interface Trail<I, O> extends Omit<
|
|
70
|
+
TrailSpec<I, O>,
|
|
71
|
+
'run' | 'follow' | 'intent'
|
|
72
|
+
> {
|
|
67
73
|
readonly kind: 'trail';
|
|
68
74
|
readonly id: string;
|
|
69
|
-
readonly
|
|
75
|
+
readonly run: Implementation<I, O>;
|
|
76
|
+
/** IDs of downstream trails this trail may invoke via ctx.follow() (always present, default []) */
|
|
77
|
+
readonly follow: readonly string[];
|
|
78
|
+
/** What this trail does to the world (always present, default 'write') */
|
|
79
|
+
readonly intent: Intent;
|
|
70
80
|
}
|
|
71
81
|
|
|
72
82
|
// ---------------------------------------------------------------------------
|
|
@@ -84,14 +94,14 @@ export interface Trail<I, O> extends Omit<TrailSpec<I, O>, 'implementation'> {
|
|
|
84
94
|
* // ID as first argument (recommended for human authoring)
|
|
85
95
|
* const show = trail("entity.show", {
|
|
86
96
|
* input: z.object({ name: z.string() }),
|
|
87
|
-
*
|
|
97
|
+
* run: (input) => Result.ok(entity),
|
|
88
98
|
* });
|
|
89
99
|
*
|
|
90
100
|
* // Full spec object (for programmatic generation)
|
|
91
101
|
* const show = trail({
|
|
92
102
|
* id: "entity.show",
|
|
93
103
|
* input: z.object({ name: z.string() }),
|
|
94
|
-
*
|
|
104
|
+
* run: (input) => Result.ok(entity),
|
|
95
105
|
* });
|
|
96
106
|
* ```
|
|
97
107
|
*/
|
|
@@ -112,14 +122,15 @@ export function trail<I, O>(
|
|
|
112
122
|
throw new TypeError('trail() requires a spec when an id is provided');
|
|
113
123
|
}
|
|
114
124
|
|
|
115
|
-
const {
|
|
125
|
+
const { run, follow: rawFollow, intent: rawIntent, ...spec } = resolved.spec;
|
|
116
126
|
|
|
117
127
|
return Object.freeze({
|
|
118
128
|
...spec,
|
|
129
|
+
follow: Object.freeze([...(rawFollow ?? [])]),
|
|
119
130
|
id: resolved.id,
|
|
120
|
-
|
|
121
|
-
await implementation(input, ctx),
|
|
131
|
+
intent: rawIntent ?? 'write',
|
|
122
132
|
kind: 'trail' as const,
|
|
133
|
+
run: async (input: I, ctx: TrailContext) => await run(input, ctx),
|
|
123
134
|
});
|
|
124
135
|
}
|
|
125
136
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type utilities for extracting input/output types from trails.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Result } from './result.js';
|
|
6
|
+
import type { AnyTrail, Trail } from './trail.js';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Utility types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
/* oxlint-disable no-explicit-any -- `any` required for conditional type inference; `unknown` breaks inference */
|
|
13
|
+
|
|
14
|
+
/** Extract the input type from a Trail. */
|
|
15
|
+
export type TrailInput<T extends AnyTrail> =
|
|
16
|
+
T extends Trail<infer I, any> ? I : never;
|
|
17
|
+
|
|
18
|
+
/** Extract the output type from a Trail. */
|
|
19
|
+
export type TrailOutput<T extends AnyTrail> =
|
|
20
|
+
T extends Trail<any, infer O> ? O : never;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extracts the full `Result<Output, Error>` type from a trail definition.
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* type SearchResult = TrailResult<typeof searchTrail>;
|
|
28
|
+
* // Result<{ results: Item[]; count: number }, Error>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export type TrailResult<T extends AnyTrail> = Result<TrailOutput<T>, Error>;
|
|
32
|
+
|
|
33
|
+
/* oxlint-enable no-explicit-any */
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Runtime schema accessors
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/** Get the input Zod schema from a trail, preserving the specific schema type. */
|
|
40
|
+
export const inputOf = <T extends AnyTrail>(trail: T): T['input'] =>
|
|
41
|
+
trail.input;
|
|
42
|
+
|
|
43
|
+
/** Get the output Zod schema from a trail, if defined, preserving the specific schema type. */
|
|
44
|
+
export const outputOf = <T extends AnyTrail>(trail: T): T['output'] =>
|
|
45
|
+
trail.output;
|
package/src/types.ts
CHANGED
|
@@ -52,7 +52,5 @@ export interface TrailContext {
|
|
|
52
52
|
readonly progress?: ProgressCallback | undefined;
|
|
53
53
|
readonly cwd?: string | undefined;
|
|
54
54
|
readonly env?: Record<string, string | undefined> | undefined;
|
|
55
|
-
readonly
|
|
55
|
+
readonly extensions?: Readonly<Record<string, unknown>> | undefined;
|
|
56
56
|
}
|
|
57
|
-
|
|
58
|
-
export type Surface = 'cli' | 'mcp' | 'http' | 'ws';
|
package/src/validate-topo.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Structural validation for a Topo graph.
|
|
3
3
|
*
|
|
4
|
-
* Checks
|
|
4
|
+
* Checks trail follow references, example input validity, event origin
|
|
5
5
|
* references, and output schema completeness. Returns a Result with all
|
|
6
6
|
* issues collected into a single ValidationError.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { ValidationError } from './errors.js';
|
|
10
10
|
import type { AnyEvent } from './event.js';
|
|
11
|
-
import type { AnyHike } from './hike.js';
|
|
12
11
|
import { Result } from './result.js';
|
|
13
12
|
import type { Topo } from './topo.js';
|
|
14
13
|
import type { AnyTrail } from './trail.js';
|
|
@@ -28,28 +27,89 @@ export interface TopoIssue {
|
|
|
28
27
|
// Validators
|
|
29
28
|
// ---------------------------------------------------------------------------
|
|
30
29
|
|
|
30
|
+
const WHITE = 0;
|
|
31
|
+
const GRAY = 1;
|
|
32
|
+
const BLACK = 2;
|
|
33
|
+
|
|
34
|
+
/** Build an adjacency list and initial color map from trails with follow. */
|
|
35
|
+
const buildFollowGraph = (
|
|
36
|
+
trails: ReadonlyMap<string, AnyTrail>
|
|
37
|
+
): {
|
|
38
|
+
graph: Map<string, readonly string[]>;
|
|
39
|
+
color: Map<string, number>;
|
|
40
|
+
} => {
|
|
41
|
+
const graph = new Map<string, readonly string[]>();
|
|
42
|
+
for (const [id, t] of trails) {
|
|
43
|
+
if (t.follow.length > 0) {
|
|
44
|
+
graph.set(id, t.follow);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const color = new Map<string, number>();
|
|
48
|
+
for (const id of graph.keys()) {
|
|
49
|
+
color.set(id, WHITE);
|
|
50
|
+
}
|
|
51
|
+
return { color, graph };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** Detect multi-node cycles in the trail follow graph via DFS. */
|
|
55
|
+
const detectFollowCycles = (
|
|
56
|
+
trails: ReadonlyMap<string, AnyTrail>
|
|
57
|
+
): TopoIssue[] => {
|
|
58
|
+
const issues: TopoIssue[] = [];
|
|
59
|
+
const { color, graph } = buildFollowGraph(trails);
|
|
60
|
+
|
|
61
|
+
const dfs = (node: string, path: string[]): void => {
|
|
62
|
+
color.set(node, GRAY);
|
|
63
|
+
for (const next of graph.get(node) ?? []) {
|
|
64
|
+
if (!graph.has(next)) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const c = color.get(next) ?? WHITE;
|
|
68
|
+
if (c === GRAY) {
|
|
69
|
+
const cycle = [...path.slice(path.indexOf(next)), next];
|
|
70
|
+
issues.push({
|
|
71
|
+
message: `Cycle detected: ${cycle.join(' → ')}`,
|
|
72
|
+
rule: 'follow-cycle',
|
|
73
|
+
trailId: next,
|
|
74
|
+
});
|
|
75
|
+
} else if (c === WHITE) {
|
|
76
|
+
dfs(next, [...path, next]);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
color.set(node, BLACK);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (const id of graph.keys()) {
|
|
83
|
+
if (color.get(id) === WHITE) {
|
|
84
|
+
dfs(id, [id]);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return issues;
|
|
88
|
+
};
|
|
89
|
+
|
|
31
90
|
const checkFollows = (
|
|
32
|
-
|
|
91
|
+
trails: ReadonlyMap<string, AnyTrail>,
|
|
33
92
|
topo: Topo
|
|
34
93
|
): TopoIssue[] => {
|
|
35
94
|
const issues: TopoIssue[] = [];
|
|
36
|
-
for (const [id,
|
|
37
|
-
for (const followId of
|
|
95
|
+
for (const [id, trail] of trails) {
|
|
96
|
+
for (const followId of trail.follow) {
|
|
38
97
|
if (followId === id) {
|
|
39
98
|
issues.push({
|
|
40
|
-
message: `
|
|
99
|
+
message: `Trail follows itself`,
|
|
41
100
|
rule: 'no-self-follow',
|
|
42
101
|
trailId: id,
|
|
43
102
|
});
|
|
44
103
|
} else if (!topo.has(followId)) {
|
|
45
104
|
issues.push({
|
|
46
105
|
message: `Follows "${followId}" which is not in the topo`,
|
|
47
|
-
rule: '
|
|
106
|
+
rule: 'follow-exists',
|
|
48
107
|
trailId: id,
|
|
49
108
|
});
|
|
50
109
|
}
|
|
51
110
|
}
|
|
52
111
|
}
|
|
112
|
+
issues.push(...detectFollowCycles(trails));
|
|
53
113
|
return issues;
|
|
54
114
|
};
|
|
55
115
|
|
|
@@ -66,7 +126,7 @@ const checkOneExample = (
|
|
|
66
126
|
): TopoIssue[] => {
|
|
67
127
|
const issues: TopoIssue[] = [];
|
|
68
128
|
const result = validateInput(inputSchema as AnyTrail['input'], example.input);
|
|
69
|
-
if (result.isErr() && example.error
|
|
129
|
+
if (result.isErr() && example.error !== 'ValidationError') {
|
|
70
130
|
issues.push({
|
|
71
131
|
message: `Example "${example.name}" input does not parse against schema`,
|
|
72
132
|
rule: 'example-input-valid',
|
|
@@ -125,13 +185,13 @@ const checkEventOrigins = (
|
|
|
125
185
|
/**
|
|
126
186
|
* Validate the structural integrity of a Topo graph.
|
|
127
187
|
*
|
|
128
|
-
* Checks
|
|
188
|
+
* Checks follow references, example inputs, event origins, and output
|
|
129
189
|
* schema presence. Returns `Result.ok()` when no issues are found, or
|
|
130
190
|
* `Result.err(ValidationError)` with all issues in the error context.
|
|
131
191
|
*/
|
|
132
192
|
export const validateTopo = (topo: Topo): Result<void, ValidationError> => {
|
|
133
193
|
const issues = [
|
|
134
|
-
...checkFollows(topo.
|
|
194
|
+
...checkFollows(topo.trails, topo),
|
|
135
195
|
...checkExamples(topo.trails),
|
|
136
196
|
...checkEventOrigins(topo.events, topo),
|
|
137
197
|
];
|
package/tsconfig.tsbuildinfo
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["./src/
|
|
1
|
+
{"root":["./src/blob-ref.ts","./src/branded.ts","./src/collections.ts","./src/context.ts","./src/derive.ts","./src/dispatch.ts","./src/errors.ts","./src/event.ts","./src/execute.ts","./src/fetch.ts","./src/guards.ts","./src/index.ts","./src/layer.ts","./src/path-security.ts","./src/resilience.ts","./src/result.ts","./src/serialization.ts","./src/topo.ts","./src/trail.ts","./src/type-utils.ts","./src/types.ts","./src/validate-topo.ts","./src/validation.ts","./src/workspace.ts","./src/patterns/bulk.ts","./src/patterns/change.ts","./src/patterns/date-range.ts","./src/patterns/index.ts","./src/patterns/pagination.ts","./src/patterns/progress.ts","./src/patterns/sorting.ts","./src/patterns/status.ts","./src/patterns/timestamps.ts","./src/redaction/index.ts","./src/redaction/patterns.ts","./src/redaction/redactor.ts"],"version":"5.9.3"}
|
package/dist/hike.d.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hike — a composition that follows trails.
|
|
3
|
-
*/
|
|
4
|
-
import type { Trail, TrailSpec } from './trail.js';
|
|
5
|
-
export interface HikeSpec<I, O> extends TrailSpec<I, O> {
|
|
6
|
-
readonly follows: readonly string[];
|
|
7
|
-
}
|
|
8
|
-
export interface Hike<I, O> extends Omit<Trail<I, O>, 'kind'> {
|
|
9
|
-
readonly kind: 'hike';
|
|
10
|
-
readonly follows: readonly string[];
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Create a hike definition.
|
|
14
|
-
*
|
|
15
|
-
* A hike is a composition that declares which trails it follows.
|
|
16
|
-
* Returns a frozen object with `kind: "hike"` and all spec fields.
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```typescript
|
|
20
|
-
* // ID as first argument
|
|
21
|
-
* const onboard = hike("entity.onboard", {
|
|
22
|
-
* follows: ["entity.add", "entity.relate"],
|
|
23
|
-
* input: z.object({ name: z.string() }),
|
|
24
|
-
* implementation: (input, ctx) => Result.ok(...),
|
|
25
|
-
* });
|
|
26
|
-
*
|
|
27
|
-
* // Full spec object (programmatic)
|
|
28
|
-
* const onboard = hike({ id: "entity.onboard", follows: [...], ... });
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export declare function hike<I, O>(id: string, spec: HikeSpec<I, O>): Hike<I, O>;
|
|
32
|
-
export declare function hike<I, O>(spec: HikeSpec<I, O> & {
|
|
33
|
-
readonly id: string;
|
|
34
|
-
}): Hike<I, O>;
|
|
35
|
-
export type AnyHike = Hike<any, any>;
|
|
36
|
-
//# sourceMappingURL=hike.d.ts.map
|
package/dist/hike.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hike.d.ts","sourceRoot":"","sources":["../src/hike.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAOnD,MAAM,WAAW,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAE,SAAQ,SAAS,CAAC,CAAC,EAAE,CAAC,CAAC;IACrD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAMD,MAAM,WAAW,IAAI,CAAC,CAAC,EAAE,CAAC,CAAE,SAAQ,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC;IAC3D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAMD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,IAAI,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACzE,wBAAgB,IAAI,CAAC,CAAC,EAAE,CAAC,EACvB,IAAI,EAAE,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAC7C,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AA0Bd,MAAM,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC"}
|
package/dist/hike.js
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Hike — a composition that follows trails.
|
|
3
|
-
*/
|
|
4
|
-
export function hike(idOrSpec, maybeSpec) {
|
|
5
|
-
const resolved = typeof idOrSpec === 'string'
|
|
6
|
-
? { id: idOrSpec, spec: maybeSpec }
|
|
7
|
-
: { id: idOrSpec.id, spec: idOrSpec };
|
|
8
|
-
if (!resolved.spec) {
|
|
9
|
-
throw new TypeError('hike() requires a spec when an id is provided');
|
|
10
|
-
}
|
|
11
|
-
const { follows, implementation, ...rest } = resolved.spec;
|
|
12
|
-
return Object.freeze({
|
|
13
|
-
...rest,
|
|
14
|
-
follows: Object.freeze([...follows]),
|
|
15
|
-
id: resolved.id,
|
|
16
|
-
implementation: async (input, ctx) => await implementation(input, ctx),
|
|
17
|
-
kind: 'hike',
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
//# sourceMappingURL=hike.js.map
|
package/dist/hike.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"hike.js","sourceRoot":"","sources":["../src/hike.ts"],"names":[],"mappings":"AAAA;;GAEG;AAiDH,MAAM,UAAU,IAAI,CAClB,QAA6D,EAC7D,SAA0B;IAE1B,MAAM,QAAQ,GACZ,OAAO,QAAQ,KAAK,QAAQ;QAC1B,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE;QACnC,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC;IAE1C,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,MAAM,IAAI,SAAS,CAAC,+CAA+C,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,EAAE,OAAO,EAAE,cAAc,EAAE,GAAG,IAAI,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC;IAC3D,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,GAAG,IAAI;QACP,OAAO,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC;QACpC,EAAE,EAAE,QAAQ,CAAC,EAAE;QACf,cAAc,EAAE,KAAK,EAAE,KAAQ,EAAE,GAAiB,EAAE,EAAE,CACpD,MAAM,cAAc,CAAC,KAAK,EAAE,GAAG,CAAC;QAClC,IAAI,EAAE,MAAe;KACtB,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { Result } from '../result';
|
|
6
|
-
import { hike } from '../hike';
|
|
7
|
-
import type { TrailContext } from '../types';
|
|
8
|
-
|
|
9
|
-
const stubCtx: TrailContext = {
|
|
10
|
-
requestId: 'test-123',
|
|
11
|
-
signal: AbortSignal.timeout(5000),
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
describe('hike()', () => {
|
|
15
|
-
const inputSchema = z.object({ userId: z.string() });
|
|
16
|
-
const outputSchema = z.object({ profile: z.string() });
|
|
17
|
-
|
|
18
|
-
const fetchProfile = hike('fetch-profile', {
|
|
19
|
-
description: 'Fetch a user profile',
|
|
20
|
-
follows: ['authenticate', 'validate-session'],
|
|
21
|
-
implementation: (input) =>
|
|
22
|
-
Result.ok({ profile: `Profile for ${input.userId}` }),
|
|
23
|
-
input: inputSchema,
|
|
24
|
-
output: outputSchema,
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
describe('basics', () => {
|
|
28
|
-
test("returns kind 'hike'", () => {
|
|
29
|
-
expect(fetchProfile.kind).toBe('hike');
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test('returns correct id', () => {
|
|
33
|
-
expect(fetchProfile.id).toBe('fetch-profile');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('preserves follows array', () => {
|
|
37
|
-
expect(fetchProfile.follows).toEqual([
|
|
38
|
-
'authenticate',
|
|
39
|
-
'validate-session',
|
|
40
|
-
]);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test('follows array is frozen', () => {
|
|
44
|
-
expect(Object.isFrozen(fetchProfile.follows)).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('trail compatibility', () => {
|
|
49
|
-
test('extends Trail — has input schema', () => {
|
|
50
|
-
const parsed = fetchProfile.input.safeParse({ userId: 'u-1' });
|
|
51
|
-
expect(parsed.success).toBe(true);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
test('extends Trail — has output schema', () => {
|
|
55
|
-
expect(fetchProfile.output).toBeDefined();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('extends Trail — implementation is callable', async () => {
|
|
59
|
-
const result = await fetchProfile.implementation(
|
|
60
|
-
{ userId: 'u-1' },
|
|
61
|
-
stubCtx
|
|
62
|
-
);
|
|
63
|
-
expect(result.isOk()).toBe(true);
|
|
64
|
-
expect(result.unwrap()).toEqual({ profile: 'Profile for u-1' });
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('preserves description', () => {
|
|
68
|
-
expect(fetchProfile.description).toBe('Fetch a user profile');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test('result object is frozen', () => {
|
|
72
|
-
expect(Object.isFrozen(fetchProfile)).toBe(true);
|
|
73
|
-
});
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('markers are preserved', () => {
|
|
77
|
-
const withMarkers = hike('tagged-hike', {
|
|
78
|
-
follows: ['setup'],
|
|
79
|
-
implementation: () => Result.ok(),
|
|
80
|
-
input: z.object({}),
|
|
81
|
-
markers: { domain: 'auth' },
|
|
82
|
-
});
|
|
83
|
-
expect(withMarkers.markers).toEqual({ domain: 'auth' });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
describe('single-object overload', () => {
|
|
87
|
-
test('accepts spec with id property', () => {
|
|
88
|
-
const r = hike({
|
|
89
|
-
follows: ['entity.add', 'entity.relate'],
|
|
90
|
-
id: 'entity.onboard',
|
|
91
|
-
implementation: () => Result.ok(),
|
|
92
|
-
input: z.object({}),
|
|
93
|
-
});
|
|
94
|
-
expect(r.id).toBe('entity.onboard');
|
|
95
|
-
expect(r.kind).toBe('hike');
|
|
96
|
-
expect(r.follows).toEqual(['entity.add', 'entity.relate']);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('sync implementations are normalized to an awaitable runtime function', async () => {
|
|
100
|
-
const r = hike({
|
|
101
|
-
follows: ['entity.add'],
|
|
102
|
-
id: 'entity.check',
|
|
103
|
-
implementation: (input: { userId: string }) =>
|
|
104
|
-
Result.ok({ profile: input.userId }),
|
|
105
|
-
input: inputSchema,
|
|
106
|
-
output: outputSchema,
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const promise = r.implementation({ userId: 'u-2' }, stubCtx);
|
|
110
|
-
expect(promise).toBeInstanceOf(Promise);
|
|
111
|
-
|
|
112
|
-
const result = await promise;
|
|
113
|
-
expect(result.isOk()).toBe(true);
|
|
114
|
-
expect(result.unwrap()).toEqual({ profile: 'u-2' });
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from 'bun:test';
|
|
2
|
-
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
|
|
5
|
-
import { jobOutputSchema } from '../job';
|
|
6
|
-
|
|
7
|
-
describe('jobOutputSchema', () => {
|
|
8
|
-
test('parses a valid completed job output', () => {
|
|
9
|
-
const input = {
|
|
10
|
-
completedAt: '2026-03-25T00:01:00Z',
|
|
11
|
-
current: 10,
|
|
12
|
-
jobId: 'job-123',
|
|
13
|
-
percentage: 100,
|
|
14
|
-
result: { rows: 42 },
|
|
15
|
-
startedAt: '2026-03-25T00:00:00Z',
|
|
16
|
-
status: 'completed',
|
|
17
|
-
total: 10,
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const parsed = jobOutputSchema.parse(input);
|
|
21
|
-
|
|
22
|
-
expect(parsed.jobId).toBe('job-123');
|
|
23
|
-
expect(parsed.status).toBe('completed');
|
|
24
|
-
expect(parsed.current).toBe(10);
|
|
25
|
-
expect(parsed.total).toBe(10);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('parses a minimal pending job output', () => {
|
|
29
|
-
const input = {
|
|
30
|
-
current: 0,
|
|
31
|
-
jobId: 'job-456',
|
|
32
|
-
status: 'pending',
|
|
33
|
-
total: 100,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const parsed = jobOutputSchema.parse(input);
|
|
37
|
-
|
|
38
|
-
expect(parsed.jobId).toBe('job-456');
|
|
39
|
-
expect(parsed.status).toBe('pending');
|
|
40
|
-
expect(parsed.result).toBeUndefined();
|
|
41
|
-
expect(parsed.error).toBeUndefined();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
test('parses a failed job with error', () => {
|
|
45
|
-
const input = {
|
|
46
|
-
current: 3,
|
|
47
|
-
error: 'connection reset',
|
|
48
|
-
jobId: 'job-789',
|
|
49
|
-
status: 'failed',
|
|
50
|
-
total: 10,
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const parsed = jobOutputSchema.parse(input);
|
|
54
|
-
|
|
55
|
-
expect(parsed.status).toBe('failed');
|
|
56
|
-
expect(parsed.error).toBe('connection reset');
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
test('rejects when jobId is missing', () => {
|
|
60
|
-
const input = {
|
|
61
|
-
current: 1,
|
|
62
|
-
status: 'running',
|
|
63
|
-
total: 5,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
expect(() => jobOutputSchema.parse(input)).toThrow();
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('rejects an invalid status value', () => {
|
|
70
|
-
const input = {
|
|
71
|
-
current: 0,
|
|
72
|
-
jobId: 'job-bad',
|
|
73
|
-
status: 'unknown',
|
|
74
|
-
total: 0,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
expect(() => jobOutputSchema.parse(input)).toThrow();
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
test('composes statusFields and progressFields correctly', () => {
|
|
81
|
-
const { shape } = jobOutputSchema;
|
|
82
|
-
|
|
83
|
-
// Status field from statusFields()
|
|
84
|
-
expect(shape.status).toBeInstanceOf(z.ZodEnum);
|
|
85
|
-
|
|
86
|
-
// Progress fields from progressFields()
|
|
87
|
-
expect(shape.current).toBeInstanceOf(z.ZodNumber);
|
|
88
|
-
expect(shape.total).toBeInstanceOf(z.ZodNumber);
|
|
89
|
-
expect(shape.percentage).toBeDefined();
|
|
90
|
-
|
|
91
|
-
// Job-specific fields
|
|
92
|
-
expect(shape.jobId).toBeInstanceOf(z.ZodString);
|
|
93
|
-
expect(shape.error).toBeDefined();
|
|
94
|
-
expect(shape.result).toBeDefined();
|
|
95
|
-
expect(shape.startedAt).toBeDefined();
|
|
96
|
-
expect(shape.completedAt).toBeDefined();
|
|
97
|
-
});
|
|
98
|
-
});
|