@ontrails/core 1.0.0-beta.12 → 1.0.0-beta.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +65 -35
  3. package/README.md +21 -20
  4. package/dist/context.d.ts +1 -1
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +5 -4
  7. package/dist/context.js.map +1 -1
  8. package/dist/derive.d.ts +14 -3
  9. package/dist/derive.d.ts.map +1 -1
  10. package/dist/derive.js +36 -12
  11. package/dist/derive.js.map +1 -1
  12. package/dist/draft.d.ts +28 -0
  13. package/dist/draft.d.ts.map +1 -0
  14. package/dist/draft.js +156 -0
  15. package/dist/draft.js.map +1 -0
  16. package/dist/event.d.ts +4 -41
  17. package/dist/event.d.ts.map +1 -1
  18. package/dist/event.js +4 -14
  19. package/dist/event.js.map +1 -1
  20. package/dist/execute.d.ts +10 -10
  21. package/dist/execute.d.ts.map +1 -1
  22. package/dist/execute.js +35 -28
  23. package/dist/execute.js.map +1 -1
  24. package/dist/gate.d.ts +17 -0
  25. package/dist/gate.d.ts.map +1 -0
  26. package/dist/gate.js +21 -0
  27. package/dist/gate.js.map +1 -0
  28. package/dist/index.d.ts +16 -9
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -9
  31. package/dist/index.js.map +1 -1
  32. package/dist/internal/topo-saves.d.ts +47 -0
  33. package/dist/internal/topo-saves.d.ts.map +1 -0
  34. package/dist/internal/topo-saves.js +310 -0
  35. package/dist/internal/topo-saves.js.map +1 -0
  36. package/dist/internal/topo-store-read.d.ts +67 -0
  37. package/dist/internal/topo-store-read.d.ts.map +1 -0
  38. package/dist/internal/topo-store-read.js +222 -0
  39. package/dist/internal/topo-store-read.js.map +1 -0
  40. package/dist/internal/topo-store.d.ts +12 -0
  41. package/dist/internal/topo-store.d.ts.map +1 -0
  42. package/dist/internal/topo-store.js +571 -0
  43. package/dist/internal/topo-store.js.map +1 -0
  44. package/dist/internal/trails-db.d.ts +16 -0
  45. package/dist/internal/trails-db.d.ts.map +1 -0
  46. package/dist/internal/trails-db.js +118 -0
  47. package/dist/internal/trails-db.js.map +1 -0
  48. package/dist/layer.d.ts +5 -16
  49. package/dist/layer.d.ts.map +1 -1
  50. package/dist/layer.js +3 -20
  51. package/dist/layer.js.map +1 -1
  52. package/dist/provision-config.d.ts +22 -0
  53. package/dist/provision-config.d.ts.map +1 -0
  54. package/dist/provision-config.js +210 -0
  55. package/dist/provision-config.js.map +1 -0
  56. package/dist/provision.d.ts +71 -0
  57. package/dist/provision.d.ts.map +1 -0
  58. package/dist/provision.js +56 -0
  59. package/dist/provision.js.map +1 -0
  60. package/dist/run.d.ts +27 -0
  61. package/dist/run.d.ts.map +1 -0
  62. package/dist/run.js +34 -0
  63. package/dist/run.js.map +1 -0
  64. package/dist/service-config.d.ts +7 -7
  65. package/dist/service-config.d.ts.map +1 -1
  66. package/dist/service-config.js +75 -73
  67. package/dist/service-config.js.map +1 -1
  68. package/dist/service.d.ts +32 -36
  69. package/dist/service.d.ts.map +1 -1
  70. package/dist/service.js +21 -21
  71. package/dist/service.js.map +1 -1
  72. package/dist/signal.d.ts +33 -0
  73. package/dist/signal.d.ts.map +1 -0
  74. package/dist/signal.js +17 -0
  75. package/dist/signal.js.map +1 -0
  76. package/dist/topo-store.d.ts +48 -0
  77. package/dist/topo-store.d.ts.map +1 -0
  78. package/dist/topo-store.js +175 -0
  79. package/dist/topo-store.js.map +1 -0
  80. package/dist/topo.d.ts +10 -10
  81. package/dist/topo.d.ts.map +1 -1
  82. package/dist/topo.js +48 -35
  83. package/dist/topo.js.map +1 -1
  84. package/dist/trail.d.ts +16 -16
  85. package/dist/trail.d.ts.map +1 -1
  86. package/dist/trail.js +5 -4
  87. package/dist/trail.js.map +1 -1
  88. package/dist/types.d.ts +10 -10
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +2 -2
  91. package/dist/types.js.map +1 -1
  92. package/dist/validate-established-topo.d.ts +76 -0
  93. package/dist/validate-established-topo.d.ts.map +1 -0
  94. package/dist/validate-established-topo.js +43 -0
  95. package/dist/validate-established-topo.js.map +1 -0
  96. package/dist/validate-topo.d.ts +2 -2
  97. package/dist/validate-topo.d.ts.map +1 -1
  98. package/dist/validate-topo.js +34 -32
  99. package/dist/validate-topo.js.map +1 -1
  100. package/package.json +4 -1
  101. package/src/__tests__/context.test.ts +8 -8
  102. package/src/__tests__/derive.test.ts +58 -1
  103. package/src/__tests__/execute.test.ts +124 -122
  104. package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
  105. package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
  106. package/src/__tests__/service-config.test.ts +41 -37
  107. package/src/__tests__/service.test.ts +71 -71
  108. package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
  109. package/src/__tests__/topo-store-read.test.ts +251 -0
  110. package/src/__tests__/topo-store.test.ts +469 -0
  111. package/src/__tests__/topo.test.ts +54 -54
  112. package/src/__tests__/trail-permit.test.ts +4 -4
  113. package/src/__tests__/trail.test.ts +58 -58
  114. package/src/__tests__/trails-db.test.ts +191 -0
  115. package/src/__tests__/type-utils.test.ts +3 -3
  116. package/src/__tests__/validate-topo.test.ts +205 -38
  117. package/src/context.ts +5 -4
  118. package/src/derive.ts +47 -16
  119. package/src/draft.ts +334 -0
  120. package/src/event.ts +11 -73
  121. package/src/execute.ts +56 -40
  122. package/src/{layer.ts → gate.ts} +13 -13
  123. package/src/index.ts +54 -23
  124. package/src/internal/topo-saves.ts +429 -0
  125. package/src/internal/topo-store-read.ts +473 -0
  126. package/src/internal/topo-store.ts +1087 -0
  127. package/src/internal/trails-db.ts +189 -0
  128. package/src/{service-config.ts → provision-config.ts} +124 -105
  129. package/src/provision.ts +148 -0
  130. package/src/{dispatch.ts → run.ts} +8 -8
  131. package/src/signal.ts +65 -0
  132. package/src/topo-store.ts +301 -0
  133. package/src/topo.ts +74 -52
  134. package/src/trail.ts +23 -22
  135. package/src/types.ts +11 -11
  136. package/src/validate-established-topo.ts +63 -0
  137. package/src/validate-topo.ts +38 -34
  138. package/tsconfig.tsbuildinfo +1 -1
  139. package/src/service.ts +0 -145
package/src/topo.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  */
4
4
 
5
5
  import { ValidationError } from './errors.js';
6
- import type { AnyEvent } from './event.js';
7
- import type { AnyService } from './service.js';
8
- import { isService } from './service.js';
6
+ import type { AnySignal } from './event.js';
7
+ import type { AnyProvision } from './provision.js';
8
+ import { isProvision } from './provision.js';
9
9
  import type { AnyTrail } from './trail.js';
10
10
 
11
11
  // ---------------------------------------------------------------------------
@@ -15,33 +15,33 @@ import type { AnyTrail } from './trail.js';
15
15
  export interface Topo {
16
16
  readonly name: string;
17
17
  readonly trails: ReadonlyMap<string, AnyTrail>;
18
- readonly events: ReadonlyMap<string, AnyEvent>;
19
- readonly services: ReadonlyMap<string, AnyService>;
18
+ readonly signals: ReadonlyMap<string, AnySignal>;
19
+ readonly provisions: ReadonlyMap<string, AnyProvision>;
20
20
  readonly count: number;
21
- readonly serviceCount: number;
21
+ readonly provisionCount: number;
22
22
  get(id: string): AnyTrail | undefined;
23
- getService(id: string): AnyService | undefined;
23
+ getProvision(id: string): AnyProvision | undefined;
24
24
  has(id: string): boolean;
25
- hasService(id: string): boolean;
25
+ hasProvision(id: string): boolean;
26
26
  ids(): string[];
27
- serviceIds(): string[];
27
+ provisionIds(): string[];
28
28
  list(): AnyTrail[];
29
- listEvents(): AnyEvent[];
30
- listServices(): AnyService[];
29
+ listSignals(): AnySignal[];
30
+ listProvisions(): AnyProvision[];
31
31
  }
32
32
 
33
33
  // ---------------------------------------------------------------------------
34
34
  // Kind discriminant check
35
35
  // ---------------------------------------------------------------------------
36
36
 
37
- type Registrable = AnyTrail | AnyEvent | AnyService;
37
+ type Registrable = AnyTrail | AnySignal | AnyProvision;
38
38
 
39
39
  const isRegistrable = (value: unknown): value is Registrable => {
40
40
  if (typeof value !== 'object' || value === null) {
41
41
  return false;
42
42
  }
43
43
  const { kind } = value as Record<string, unknown>;
44
- return kind === 'trail' || kind === 'event';
44
+ return kind === 'trail' || kind === 'signal';
45
45
  };
46
46
 
47
47
  // ---------------------------------------------------------------------------
@@ -51,24 +51,22 @@ const isRegistrable = (value: unknown): value is Registrable => {
51
51
  const createTopo = (
52
52
  name: string,
53
53
  trails: ReadonlyMap<string, AnyTrail>,
54
- events: ReadonlyMap<string, AnyEvent>,
55
- services: ReadonlyMap<string, AnyService>
54
+ signals: ReadonlyMap<string, AnySignal>,
55
+ provisions: ReadonlyMap<string, AnyProvision>
56
56
  ): Topo => ({
57
57
  count: trails.size,
58
- events,
59
58
  get(id: string): AnyTrail | undefined {
60
59
  return trails.get(id);
61
60
  },
62
- getService(id: string): AnyService | undefined {
63
- return services.get(id);
61
+ getProvision(id: string): AnyProvision | undefined {
62
+ return provisions.get(id);
64
63
  },
65
64
  has(id: string): boolean {
66
65
  return trails.has(id);
67
66
  },
68
- hasService(id: string): boolean {
69
- return services.has(id);
67
+ hasProvision(id: string): boolean {
68
+ return provisions.has(id);
70
69
  },
71
-
72
70
  ids(): string[] {
73
71
  return [...trails.keys()];
74
72
  },
@@ -76,21 +74,22 @@ const createTopo = (
76
74
  list(): AnyTrail[] {
77
75
  return [...trails.values()];
78
76
  },
79
-
80
- listEvents(): AnyEvent[] {
81
- return [...events.values()];
77
+ listProvisions(): AnyProvision[] {
78
+ return [...provisions.values()];
82
79
  },
83
- listServices(): AnyService[] {
84
- return [...services.values()];
80
+
81
+ listSignals(): AnySignal[] {
82
+ return [...signals.values()];
85
83
  },
86
84
 
87
85
  name,
88
- serviceCount: services.size,
89
- serviceIds(): string[] {
90
- return [...services.keys()];
91
- },
86
+ provisionCount: provisions.size,
92
87
 
93
- services,
88
+ provisionIds(): string[] {
89
+ return [...provisions.keys()];
90
+ },
91
+ provisions,
92
+ signals,
94
93
  trails,
95
94
  });
96
95
 
@@ -102,22 +101,22 @@ const createTopo = (
102
101
  const register = (
103
102
  value: Registrable,
104
103
  trails: Map<string, AnyTrail>,
105
- events: Map<string, AnyEvent>,
106
- services: Map<string, AnyService>
104
+ signals: Map<string, AnySignal>,
105
+ provisions: Map<string, AnyProvision>
107
106
  ): void => {
108
107
  const { id } = value as { id: string };
109
108
  const registrars: Record<string, () => void> = {
110
- event: () => {
111
- if (events.has(id)) {
112
- throw new ValidationError(`Duplicate event ID: "${id}"`);
109
+ provision: () => {
110
+ if (provisions.has(id)) {
111
+ throw new ValidationError(`Duplicate provision ID: "${id}"`);
113
112
  }
114
- events.set(id, value as AnyEvent);
113
+ provisions.set(id, value as AnyProvision);
115
114
  },
116
- service: () => {
117
- if (services.has(id)) {
118
- throw new ValidationError(`Duplicate service ID: "${id}"`);
115
+ signal: () => {
116
+ if (signals.has(id)) {
117
+ throw new ValidationError(`Duplicate signal ID: "${id}"`);
119
118
  }
120
- services.set(id, value as AnyService);
119
+ signals.set(id, value as AnySignal);
121
120
  },
122
121
  trail: () => {
123
122
  if (trails.has(id)) {
@@ -129,20 +128,43 @@ const register = (
129
128
  registrars[value.kind]?.();
130
129
  };
131
130
 
131
+ const markUniqueObject = (
132
+ value: unknown,
133
+ seenValues: WeakSet<object>
134
+ ): boolean => {
135
+ if (typeof value !== 'object' || value === null) {
136
+ return true;
137
+ }
138
+ if (seenValues.has(value)) {
139
+ return false;
140
+ }
141
+ seenValues.add(value);
142
+ return true;
143
+ };
144
+
145
+ const registerModuleValue = (
146
+ value: unknown,
147
+ trails: Map<string, AnyTrail>,
148
+ signals: Map<string, AnySignal>,
149
+ provisions: Map<string, AnyProvision>
150
+ ): void => {
151
+ if (isProvision(value) || isRegistrable(value)) {
152
+ register(value, trails, signals, provisions);
153
+ }
154
+ };
155
+
132
156
  const registerModuleValues = (
133
157
  mod: Record<string, unknown>,
134
158
  trails: Map<string, AnyTrail>,
135
- events: Map<string, AnyEvent>,
136
- services: Map<string, AnyService>
159
+ signals: Map<string, AnySignal>,
160
+ provisions: Map<string, AnyProvision>
137
161
  ): void => {
162
+ const seenValues = new WeakSet<object>();
138
163
  for (const value of Object.values(mod)) {
139
- if (isService(value)) {
140
- register(value, trails, events, services);
164
+ if (!markUniqueObject(value, seenValues)) {
141
165
  continue;
142
166
  }
143
- if (isRegistrable(value)) {
144
- register(value, trails, events, services);
145
- }
167
+ registerModuleValue(value, trails, signals, provisions);
146
168
  }
147
169
  };
148
170
 
@@ -151,12 +173,12 @@ export const topo = (
151
173
  ...modules: Record<string, unknown>[]
152
174
  ): Topo => {
153
175
  const trails = new Map<string, AnyTrail>();
154
- const events = new Map<string, AnyEvent>();
155
- const services = new Map<string, AnyService>();
176
+ const signals = new Map<string, AnySignal>();
177
+ const provisions = new Map<string, AnyProvision>();
156
178
 
157
179
  for (const mod of modules) {
158
- registerModuleValues(mod, trails, events, services);
180
+ registerModuleValues(mod, trails, signals, provisions);
159
181
  }
160
182
 
161
- return createTopo(name, trails, events, services);
183
+ return createTopo(name, trails, signals, provisions);
162
184
  };
package/src/trail.ts CHANGED
@@ -2,7 +2,7 @@ import type { z } from 'zod';
2
2
 
3
3
  import type { FieldOverride } from './derive.js';
4
4
  import type { Result } from './result.js';
5
- import type { AnyService } from './service.js';
5
+ import type { AnyProvision } from './provision.js';
6
6
  import type {
7
7
  Implementation,
8
8
  PermitRequirement,
@@ -44,7 +44,7 @@ export interface TrailSpec<I, O> {
44
44
  /** Zod schema for validating output (optional — some trails are fire-and-forget) */
45
45
  readonly output?: z.ZodType<O> | undefined;
46
46
  /** The pure function that does the work (sync or async authoring) */
47
- readonly run: Implementation<I, O>;
47
+ readonly blaze: Implementation<I, O>;
48
48
  /** Human-readable description */
49
49
  readonly description?: string | undefined;
50
50
  /** Named examples for docs and testing */
@@ -53,16 +53,16 @@ export interface TrailSpec<I, O> {
53
53
  readonly intent?: 'read' | 'write' | 'destroy' | undefined;
54
54
  /** Trail is idempotent (safe to retry) */
55
55
  readonly idempotent?: boolean | undefined;
56
- /** Arbitrary metadata for tooling and filtering */
57
- readonly metadata?: Readonly<Record<string, unknown>> | undefined;
56
+ /** Arbitrary meta for tooling and filtering */
57
+ readonly meta?: Readonly<Record<string, unknown>> | undefined;
58
58
  /** Named sets of downstream trail IDs that may be invoked */
59
59
  readonly detours?: Readonly<Record<string, readonly string[]>> | undefined;
60
60
  /** Per-field overrides for deriveFields() (labels, hints, options) */
61
61
  readonly fields?: Readonly<Record<string, FieldOverride>> | undefined;
62
- /** IDs of downstream trails this trail may invoke via ctx.follow() */
63
- readonly follow?: readonly string[] | undefined;
64
- /** Services this trail may access via service.from(ctx) */
65
- readonly services?: readonly AnyService[] | undefined;
62
+ /** IDs of downstream trails this trail may invoke via ctx.cross() */
63
+ readonly crosses?: readonly string[] | undefined;
64
+ /** Provisions this trail may access via provision.from(ctx) */
65
+ readonly provisions?: readonly AnyProvision[] | undefined;
66
66
  /** Auth requirement: scopes object, 'public', or omitted (undeclared) */
67
67
  readonly permit?: PermitRequirement | undefined;
68
68
  }
@@ -77,15 +77,15 @@ export type Intent = 'read' | 'write' | 'destroy';
77
77
  /** A fully-defined trail — the unit of work in the Trails system */
78
78
  export interface Trail<I, O> extends Omit<
79
79
  TrailSpec<I, O>,
80
- 'run' | 'follow' | 'intent' | 'services'
80
+ 'blaze' | 'crosses' | 'intent' | 'provisions'
81
81
  > {
82
82
  readonly kind: 'trail';
83
83
  readonly id: string;
84
- readonly run: Implementation<I, O>;
85
- /** IDs of downstream trails this trail may invoke via ctx.follow() (always present, default []) */
86
- readonly follow: readonly string[];
87
- /** Services this trail may access via service.from(ctx) (always present, default []) */
88
- readonly services: readonly AnyService[];
84
+ readonly blaze: Implementation<I, O>;
85
+ /** IDs of downstream trails this trail may invoke via ctx.cross() (always present, default []) */
86
+ readonly crosses: readonly string[];
87
+ /** Provisions this trail may access via provision.from(ctx) (always present, default []) */
88
+ readonly provisions: readonly AnyProvision[];
89
89
  /** What this trail does to the world (always present, default 'write') */
90
90
  readonly intent: Intent;
91
91
  }
@@ -105,14 +105,14 @@ export interface Trail<I, O> extends Omit<
105
105
  * // ID as first argument (recommended for human authoring)
106
106
  * const show = trail("entity.show", {
107
107
  * input: z.object({ name: z.string() }),
108
- * run: (input) => Result.ok(entity),
108
+ * blaze: (input) => Result.ok(entity),
109
109
  * });
110
110
  *
111
111
  * // Full spec object (for programmatic generation)
112
112
  * const show = trail({
113
113
  * id: "entity.show",
114
114
  * input: z.object({ name: z.string() }),
115
- * run: (input) => Result.ok(entity),
115
+ * blaze: (input) => Result.ok(entity),
116
116
  * });
117
117
  * ```
118
118
  */
@@ -134,21 +134,22 @@ export function trail<I, O>(
134
134
  }
135
135
 
136
136
  const {
137
- run,
138
- follow: rawFollow,
137
+ blaze,
138
+ crosses: rawCrosses,
139
139
  intent: rawIntent,
140
- services: rawServices,
140
+ provisions: rawProvisions,
141
141
  ...spec
142
142
  } = resolved.spec;
143
+ const provisions = Object.freeze([...(rawProvisions ?? [])]);
143
144
 
144
145
  return Object.freeze({
145
146
  ...spec,
146
- follow: Object.freeze([...(rawFollow ?? [])]),
147
+ blaze: async (input: I, ctx: TrailContext) => await blaze(input, ctx),
148
+ crosses: Object.freeze([...(rawCrosses ?? [])]),
147
149
  id: resolved.id,
148
150
  intent: rawIntent ?? 'write',
149
151
  kind: 'trail' as const,
150
- run: async (input: I, ctx: TrailContext) => await run(input, ctx),
151
- services: Object.freeze([...(rawServices ?? [])]),
152
+ provisions,
152
153
  });
153
154
  }
154
155
 
package/src/types.ts CHANGED
@@ -12,14 +12,14 @@ export type Implementation<I, O> = (
12
12
  ) => Result<O, Error> | Promise<Result<O, Error>>;
13
13
 
14
14
  /** Invoke another trail by id — used for trail composition */
15
- export type FollowFn = <O>(
15
+ export type CrossFn = <O>(
16
16
  id: string,
17
17
  input: unknown
18
18
  ) => Promise<Result<O, Error>>;
19
19
 
20
- /** Resolve a service instance from the current trail context. */
21
- export type ServiceLookup = <T = unknown>(
22
- serviceOrId: { readonly id: string } | string
20
+ /** Resolve a provision instance from the current trail context. */
21
+ export type ProvisionLookup = <T = unknown>(
22
+ provisionOrId: { readonly id: string } | string
23
23
  ) => T;
24
24
 
25
25
  /** Callback for reporting progress from long-running trails */
@@ -46,8 +46,8 @@ export interface Logger {
46
46
  child(context: Record<string, unknown>): Logger;
47
47
  }
48
48
 
49
- /** Context extension key for the invoking surface name. */
50
- export const SURFACE_KEY = '__trails_surface' as const;
49
+ /** Context extension key for the invoking trailhead name. */
50
+ export const TRAILHEAD_KEY = '__trails_trailhead' as const;
51
51
 
52
52
  /** Minimal permit shape available on TrailContext. Permits extends this. */
53
53
  export interface BasePermit {
@@ -58,8 +58,8 @@ export interface BasePermit {
58
58
  /** Runtime context threaded through every trail execution */
59
59
  export interface TrailContext {
60
60
  readonly requestId: string;
61
- readonly signal: AbortSignal;
62
- readonly follow?: FollowFn | undefined;
61
+ readonly abortSignal: AbortSignal;
62
+ readonly cross?: CrossFn | undefined;
63
63
  readonly permit?: BasePermit;
64
64
  readonly workspaceRoot?: string | undefined;
65
65
  readonly logger?: Logger | undefined;
@@ -67,7 +67,7 @@ export interface TrailContext {
67
67
  readonly cwd?: string | undefined;
68
68
  readonly env?: Record<string, string | undefined> | undefined;
69
69
  readonly extensions?: Readonly<Record<string, unknown>> | undefined;
70
- readonly service?: ServiceLookup | undefined;
70
+ readonly provision?: ProvisionLookup | undefined;
71
71
  }
72
72
 
73
73
  /**
@@ -82,6 +82,6 @@ export type PermitRequirement =
82
82
  | 'public';
83
83
 
84
84
  /** Input shape used to seed a runtime TrailContext before resolution. */
85
- export type TrailContextInit = Omit<TrailContext, 'service'> & {
86
- readonly service?: ServiceLookup | undefined;
85
+ export type TrailContextInit = Omit<TrailContext, 'provision'> & {
86
+ readonly provision?: ProvisionLookup | undefined;
87
87
  };
@@ -0,0 +1,63 @@
1
+ import { ValidationError } from './errors.js';
2
+ import { Result } from './result.js';
3
+ import type { Topo } from './topo.js';
4
+ import { validateEstablishedTopo as validateDraftFreeTopo } from './draft.js';
5
+ import type { TopoIssue } from './validate-topo.js';
6
+ import { validateTopo } from './validate-topo.js';
7
+
8
+ const PROJECTION_BLOCKING_RULES = new Set([
9
+ 'cross-cycle',
10
+ 'cross-exists',
11
+ 'no-self-cross',
12
+ 'provision-exists',
13
+ 'signal-origin-exists',
14
+ ]);
15
+
16
+ const keepProjectionBlockingIssues = (
17
+ result: ReturnType<typeof validateTopo>
18
+ ) => {
19
+ if (result.isOk()) {
20
+ return result;
21
+ }
22
+
23
+ const issues = (
24
+ result.error.context as { issues?: readonly TopoIssue[] } | undefined
25
+ )?.issues;
26
+ const remainingIssues = issues?.filter((issue) =>
27
+ PROJECTION_BLOCKING_RULES.has(issue.rule)
28
+ );
29
+
30
+ if (remainingIssues === undefined || remainingIssues.length === 0) {
31
+ return Result.ok();
32
+ }
33
+
34
+ return Result.err(
35
+ new ValidationError(
36
+ `Topo validation failed with ${remainingIssues.length} issue(s)`,
37
+ {
38
+ cause: result.error,
39
+ context: { issues: remainingIssues },
40
+ }
41
+ )
42
+ );
43
+ };
44
+
45
+ /**
46
+ * Validate that a topo is ready for established outputs.
47
+ *
48
+ * Established surfaces still require the authored graph to be structurally
49
+ * valid, and they must also reject any remaining draft state.
50
+ */
51
+ export const validateEstablishedTopo = (topo: Topo) => {
52
+ const structural = keepProjectionBlockingIssues(validateTopo(topo));
53
+ if (structural.isErr()) {
54
+ return structural;
55
+ }
56
+
57
+ const established = validateDraftFreeTopo(topo);
58
+ if (established.isErr()) {
59
+ return established;
60
+ }
61
+
62
+ return Result.ok();
63
+ };
@@ -1,13 +1,14 @@
1
1
  /**
2
2
  * Structural validation for a Topo graph.
3
3
  *
4
- * Checks trail follow references, example input validity, event origin
4
+ * Checks trail crossing 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
- import type { AnyEvent } from './event.js';
10
+ import { isDraftId } from './draft.js';
11
+ import type { AnySignal } from './event.js';
11
12
  import { Result } from './result.js';
12
13
  import type { Topo } from './topo.js';
13
14
  import type { AnyTrail } from './trail.js';
@@ -31,8 +32,8 @@ const WHITE = 0;
31
32
  const GRAY = 1;
32
33
  const BLACK = 2;
33
34
 
34
- /** Build an adjacency list and initial color map from trails with follow. */
35
- const buildFollowGraph = (
35
+ /** Build an adjacency list and initial color map from trails with crossings. */
36
+ const buildCrossGraph = (
36
37
  trails: ReadonlyMap<string, AnyTrail>
37
38
  ): {
38
39
  graph: Map<string, readonly string[]>;
@@ -40,8 +41,8 @@ const buildFollowGraph = (
40
41
  } => {
41
42
  const graph = new Map<string, readonly string[]>();
42
43
  for (const [id, t] of trails) {
43
- if (t.follow.length > 0) {
44
- graph.set(id, t.follow);
44
+ if (t.crosses.length > 0) {
45
+ graph.set(id, t.crosses);
45
46
  }
46
47
  }
47
48
  const color = new Map<string, number>();
@@ -51,12 +52,12 @@ const buildFollowGraph = (
51
52
  return { color, graph };
52
53
  };
53
54
 
54
- /** Detect multi-node cycles in the trail follow graph via DFS. */
55
- const detectFollowCycles = (
55
+ /** Detect multi-node cycles in the trail crossing graph via DFS. */
56
+ const detectCrossCycles = (
56
57
  trails: ReadonlyMap<string, AnyTrail>
57
58
  ): TopoIssue[] => {
58
59
  const issues: TopoIssue[] = [];
59
- const { color, graph } = buildFollowGraph(trails);
60
+ const { color, graph } = buildCrossGraph(trails);
60
61
 
61
62
  const dfs = (node: string, path: string[]): void => {
62
63
  color.set(node, GRAY);
@@ -69,7 +70,7 @@ const detectFollowCycles = (
69
70
  const cycle = [...path.slice(path.indexOf(next)), next];
70
71
  issues.push({
71
72
  message: `Cycle detected: ${cycle.join(' → ')}`,
72
- rule: 'follow-cycle',
73
+ rule: 'cross-cycle',
73
74
  trailId: next,
74
75
  });
75
76
  } else if (c === WHITE) {
@@ -87,44 +88,47 @@ const detectFollowCycles = (
87
88
  return issues;
88
89
  };
89
90
 
90
- const checkFollows = (
91
+ const checkCrosses = (
91
92
  trails: ReadonlyMap<string, AnyTrail>,
92
93
  topo: Topo
93
94
  ): TopoIssue[] => {
94
95
  const issues: TopoIssue[] = [];
95
96
  for (const [id, trail] of trails) {
96
- for (const followId of trail.follow) {
97
- if (followId === id) {
97
+ for (const crossedId of trail.crosses) {
98
+ if (crossedId === id) {
98
99
  issues.push({
99
- message: `Trail follows itself`,
100
- rule: 'no-self-follow',
100
+ message: `Trail crosses itself`,
101
+ rule: 'no-self-cross',
101
102
  trailId: id,
102
103
  });
103
- } else if (!topo.has(followId)) {
104
+ } else if (!topo.has(crossedId) && !isDraftId(crossedId)) {
104
105
  issues.push({
105
- message: `Follows "${followId}" which is not in the topo`,
106
- rule: 'follow-exists',
106
+ message: `Crosses "${crossedId}" which is not in the topo`,
107
+ rule: 'cross-exists',
107
108
  trailId: id,
108
109
  });
109
110
  }
110
111
  }
111
112
  }
112
- issues.push(...detectFollowCycles(trails));
113
+ issues.push(...detectCrossCycles(trails));
113
114
  return issues;
114
115
  };
115
116
 
116
- const checkServices = (
117
+ const checkProvisions = (
117
118
  trails: ReadonlyMap<string, AnyTrail>,
118
119
  topo: Topo
119
120
  ): TopoIssue[] => {
120
121
  const issues: TopoIssue[] = [];
121
122
 
122
123
  for (const [id, trail] of trails) {
123
- for (const declaredService of trail.services) {
124
- if (!topo.hasService(declaredService.id)) {
124
+ for (const declaredProvision of trail.provisions) {
125
+ if (
126
+ !topo.hasProvision(declaredProvision.id) &&
127
+ !isDraftId(declaredProvision.id)
128
+ ) {
125
129
  issues.push({
126
- message: `Service "${declaredService.id}" is not in the topo`,
127
- rule: 'service-exists',
130
+ message: `Provision "${declaredProvision.id}" is not in the topo`,
131
+ rule: 'provision-exists',
128
132
  trailId: id,
129
133
  });
130
134
  }
@@ -177,20 +181,20 @@ const checkExamples = (trails: ReadonlyMap<string, AnyTrail>): TopoIssue[] => {
177
181
  return issues;
178
182
  };
179
183
 
180
- const checkEventOrigins = (
181
- events: ReadonlyMap<string, AnyEvent>,
184
+ const checkSignalOrigins = (
185
+ signals: ReadonlyMap<string, AnySignal>,
182
186
  topo: Topo
183
187
  ): TopoIssue[] => {
184
188
  const issues: TopoIssue[] = [];
185
- for (const [id, evt] of events) {
189
+ for (const [id, evt] of signals) {
186
190
  if (!evt.from) {
187
191
  continue;
188
192
  }
189
193
  for (const originId of evt.from) {
190
- if (!topo.has(originId)) {
194
+ if (!topo.has(originId) && !isDraftId(originId)) {
191
195
  issues.push({
192
- message: `Event origin "${originId}" is not in the topo`,
193
- rule: 'event-origin-exists',
196
+ message: `Signal origin "${originId}" is not in the topo`,
197
+ rule: 'signal-origin-exists',
194
198
  trailId: id,
195
199
  });
196
200
  }
@@ -206,16 +210,16 @@ const checkEventOrigins = (
206
210
  /**
207
211
  * Validate the structural integrity of a Topo graph.
208
212
  *
209
- * Checks follow references, example inputs, event origins, and output
213
+ * Checks crossing references, example inputs, event origins, and output
210
214
  * schema presence. Returns `Result.ok()` when no issues are found, or
211
215
  * `Result.err(ValidationError)` with all issues in the error context.
212
216
  */
213
217
  export const validateTopo = (topo: Topo): Result<void, ValidationError> => {
214
218
  const issues = [
215
- ...checkFollows(topo.trails, topo),
216
- ...checkServices(topo.trails, topo),
219
+ ...checkCrosses(topo.trails, topo),
220
+ ...checkProvisions(topo.trails, topo),
217
221
  ...checkExamples(topo.trails),
218
- ...checkEventOrigins(topo.events, topo),
222
+ ...checkSignalOrigins(topo.signals, topo),
219
223
  ];
220
224
 
221
225
  if (issues.length === 0) {
@@ -1 +1 @@
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/service-config.ts","./src/service.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"}
1
+ {"root":["./src/blob-ref.ts","./src/branded.ts","./src/collections.ts","./src/context.ts","./src/derive.ts","./src/draft.ts","./src/errors.ts","./src/event.ts","./src/execute.ts","./src/fetch.ts","./src/gate.ts","./src/guards.ts","./src/index.ts","./src/path-security.ts","./src/provision-config.ts","./src/provision.ts","./src/resilience.ts","./src/result.ts","./src/run.ts","./src/serialization.ts","./src/signal.ts","./src/topo-store.ts","./src/topo.ts","./src/trail.ts","./src/type-utils.ts","./src/types.ts","./src/validate-established-topo.ts","./src/validate-topo.ts","./src/validation.ts","./src/workspace.ts","./src/internal/topo-saves.ts","./src/internal/topo-store-read.ts","./src/internal/topo-store.ts","./src/internal/trails-db.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"}