@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
package/src/types.ts
ADDED
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import type { TrailsError } from './errors.js';
|
|
2
|
+
import type { BasePermit } from './permits.js';
|
|
3
|
+
import type { Result } from './result.js';
|
|
4
|
+
import type { Signal } from './signal.js';
|
|
5
|
+
import type { AnyTrail } from './trail.js';
|
|
6
|
+
import type { ComposeInput, TrailOutput } from './type-utils.js';
|
|
7
|
+
import type { ActivationProvenance } from './activation-provenance.js';
|
|
8
|
+
import type { TrailVersionReference } from './version-resolution.js';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Detour
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/** A recovery path that activates when a trail's implementation fails with a matching error. */
|
|
15
|
+
export interface Detour<Input, Output, TErr extends TrailsError = TrailsError> {
|
|
16
|
+
/* oxlint-disable-next-line no-explicit-any -- standard pattern for matching abstract+concrete class constructors */
|
|
17
|
+
readonly on: abstract new (...args: any[]) => TErr;
|
|
18
|
+
readonly maxAttempts?: number | undefined;
|
|
19
|
+
readonly recover: (
|
|
20
|
+
attempt: DetourAttempt<Input, TErr>,
|
|
21
|
+
ctx: TrailContext
|
|
22
|
+
) => Promise<Result<Output, TrailsError>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Context passed to a detour's recover function on each attempt. */
|
|
26
|
+
export interface DetourAttempt<Input, TErr extends TrailsError = TrailsError> {
|
|
27
|
+
/** 1-indexed attempt number */
|
|
28
|
+
readonly attempt: number;
|
|
29
|
+
/** The matched error */
|
|
30
|
+
readonly error: TErr;
|
|
31
|
+
/** Original trail input */
|
|
32
|
+
readonly input: Input;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type ComposeBatchCall<TTarget extends AnyTrail | string = AnyTrail | string> =
|
|
36
|
+
TTarget extends AnyTrail
|
|
37
|
+
? readonly [trail: TTarget, input: ComposeInput<TTarget>]
|
|
38
|
+
: readonly [id: string, input: unknown];
|
|
39
|
+
|
|
40
|
+
type ComposeBatchResult<TTarget extends AnyTrail | string> =
|
|
41
|
+
TTarget extends AnyTrail
|
|
42
|
+
? Result<TrailOutput<TTarget>, Error>
|
|
43
|
+
: Result<unknown, Error>;
|
|
44
|
+
|
|
45
|
+
type ComposeBatchResults<TCalls extends readonly ComposeBatchCall[]> = {
|
|
46
|
+
readonly [K in keyof TCalls]: TCalls[K] extends readonly [
|
|
47
|
+
infer TTarget,
|
|
48
|
+
unknown,
|
|
49
|
+
]
|
|
50
|
+
? TTarget extends AnyTrail | string
|
|
51
|
+
? ComposeBatchResult<TTarget>
|
|
52
|
+
: never
|
|
53
|
+
: never;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Runtime options for batch `ctx.compose([...])` calls. */
|
|
57
|
+
export interface ComposeBatchOptions {
|
|
58
|
+
/**
|
|
59
|
+
* Maximum number of branches to execute concurrently.
|
|
60
|
+
*
|
|
61
|
+
* Omit for unbounded concurrency. `1` is equivalent to sequential execution.
|
|
62
|
+
*/
|
|
63
|
+
readonly concurrency?: number | undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Runtime options for a single `ctx.compose(trail, input, options)` call. */
|
|
67
|
+
export interface ComposeOptions {
|
|
68
|
+
/**
|
|
69
|
+
* Execute a specific live version of the composed trail.
|
|
70
|
+
*
|
|
71
|
+
* Omit to keep composition current by default. Historical revision entries
|
|
72
|
+
* transpose through the current trail; fork entries run their own implementation.
|
|
73
|
+
*/
|
|
74
|
+
readonly version?: TrailVersionReference | undefined;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Trail implementation — sync or async.
|
|
79
|
+
*
|
|
80
|
+
* Authors can return `Result` directly or wrap it in a `Promise`. The framework
|
|
81
|
+
* normalizes with `await` at every call site, so both forms work transparently.
|
|
82
|
+
*/
|
|
83
|
+
export type Implementation<I, O, Ctx extends TrailContext = TrailContext> = (
|
|
84
|
+
input: I,
|
|
85
|
+
ctx: Ctx
|
|
86
|
+
) => Result<O, Error> | Promise<Result<O, Error>>;
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Invoke another trail — used for trail composition.
|
|
90
|
+
*
|
|
91
|
+
* Two call shapes:
|
|
92
|
+
*
|
|
93
|
+
* - **By trail object** (typed): `ctx.compose(showGist, { id })` — the compiler
|
|
94
|
+
* infers `I` and `O` from the trail's schemas, so the result is fully typed.
|
|
95
|
+
* - **By string id** (untyped escape hatch): `ctx.compose('gist.show', { id })`
|
|
96
|
+
* — returns `Result<O, Error>` where `O` defaults to `unknown`.
|
|
97
|
+
* - **By batch**: `ctx.compose([[showGist, { id }], ['audit.log', payload]])`
|
|
98
|
+
* — executes every composing concurrently and resolves once all results are
|
|
99
|
+
* available. Result ordering always matches the input tuple ordering. Pass
|
|
100
|
+
* `{ concurrency: N }` as the second argument to limit how many branches
|
|
101
|
+
* run at once.
|
|
102
|
+
*/
|
|
103
|
+
export interface ComposeFn {
|
|
104
|
+
<const TCalls extends readonly ComposeBatchCall[]>(
|
|
105
|
+
calls: TCalls,
|
|
106
|
+
options?: ComposeBatchOptions
|
|
107
|
+
): Promise<ComposeBatchResults<TCalls>>;
|
|
108
|
+
<T extends AnyTrail>(
|
|
109
|
+
trail: T,
|
|
110
|
+
input: ComposeInput<T>,
|
|
111
|
+
options?: ComposeOptions
|
|
112
|
+
): Promise<Result<TrailOutput<T>, Error>>;
|
|
113
|
+
<O = unknown>(
|
|
114
|
+
id: string,
|
|
115
|
+
input: unknown,
|
|
116
|
+
options?: ComposeOptions
|
|
117
|
+
): Promise<Result<O, Error>>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Emit a signal — used for signal-driven activation.
|
|
122
|
+
*
|
|
123
|
+
* Fan-out to consumer trails (those with the signal in their `on:` array) is
|
|
124
|
+
* the framework's responsibility. Producers call with a `Signal<T>` value and
|
|
125
|
+
* get best-effort `Promise<void>` semantics: payload validation, missing topo
|
|
126
|
+
* entries, guard suppression, and consumer failures are observable through
|
|
127
|
+
* diagnostics/logging but do not become producer-facing `Result` plumbing.
|
|
128
|
+
* Consumers fan out in parallel, each with its own derived context. Runtime
|
|
129
|
+
* cycle suppression is still signal-id-based against the current fire stack:
|
|
130
|
+
* it prevents re-entrant loops but can over-suppress legitimate diamond
|
|
131
|
+
* re-fires, with a debug breadcrumb and a warn emitted when suppression
|
|
132
|
+
* happens.
|
|
133
|
+
*/
|
|
134
|
+
export type FireFn = <T>(signal: Signal<T>, payload: T) => Promise<void>;
|
|
135
|
+
|
|
136
|
+
/** Resolve a resource instance from the current trail context. */
|
|
137
|
+
export type ResourceLookup = <T = unknown>(
|
|
138
|
+
resourceOrId: { readonly id: string } | string
|
|
139
|
+
) => T;
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Wrap the execution of `fn` in a child trace span.
|
|
143
|
+
*
|
|
144
|
+
* Creates a nested span under the current trail's root trace record, times
|
|
145
|
+
* the callback, records success or failure (including error category), and
|
|
146
|
+
* writes the completed span to the registered sink. Errors thrown by `fn`
|
|
147
|
+
* are recorded on the span and then rethrown — tracing never swallows them.
|
|
148
|
+
*/
|
|
149
|
+
export type TraceFn = <T>(
|
|
150
|
+
label: string,
|
|
151
|
+
fn: () => T | Promise<T>
|
|
152
|
+
) => Promise<T>;
|
|
153
|
+
|
|
154
|
+
/** Callback for reporting progress from long-running trails */
|
|
155
|
+
export type ProgressCallback = (event: ProgressEvent) => void;
|
|
156
|
+
|
|
157
|
+
/** Structured progress event emitted during trail execution */
|
|
158
|
+
export interface ProgressEvent {
|
|
159
|
+
readonly type: 'start' | 'progress' | 'complete' | 'error';
|
|
160
|
+
readonly current?: number | undefined;
|
|
161
|
+
readonly total?: number | undefined;
|
|
162
|
+
readonly message?: string | undefined;
|
|
163
|
+
readonly ts: string;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Minimal logger interface — implementations can bridge to any logging library */
|
|
167
|
+
export interface Logger {
|
|
168
|
+
readonly name?: string | undefined;
|
|
169
|
+
trace(message: string, data?: Record<string, unknown>): void;
|
|
170
|
+
debug(message: string, data?: Record<string, unknown>): void;
|
|
171
|
+
info(message: string, data?: Record<string, unknown>): void;
|
|
172
|
+
warn(message: string, data?: Record<string, unknown>): void;
|
|
173
|
+
error(message: string, data?: Record<string, unknown>): void;
|
|
174
|
+
fatal(message: string, data?: Record<string, unknown>): void;
|
|
175
|
+
child(context: Record<string, unknown>): Logger;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export type LogLevel =
|
|
179
|
+
| 'debug'
|
|
180
|
+
| 'error'
|
|
181
|
+
| 'fatal'
|
|
182
|
+
| 'info'
|
|
183
|
+
| 'silent'
|
|
184
|
+
| 'trace'
|
|
185
|
+
| 'warn';
|
|
186
|
+
|
|
187
|
+
export interface LogRecord {
|
|
188
|
+
readonly category: string;
|
|
189
|
+
readonly level: LogLevel;
|
|
190
|
+
readonly message: string;
|
|
191
|
+
readonly metadata: Record<string, unknown>;
|
|
192
|
+
readonly timestamp: Date;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface LogSink {
|
|
196
|
+
readonly name: string;
|
|
197
|
+
readonly write: (record: LogRecord) => void;
|
|
198
|
+
readonly flush?: (() => Promise<void>) | undefined;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface LogFormatter {
|
|
202
|
+
format(record: LogRecord): string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Context extension key for the invoking surface name.
|
|
207
|
+
*/
|
|
208
|
+
export const SURFACE_KEY = '__trails_surface' as const;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Context extension key for the layer names attached by the invoking surface.
|
|
212
|
+
*/
|
|
213
|
+
export const SURFACE_LAYER_NAMES_KEY = '__trails_surface_layer_names' as const;
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Context extension key carrying per-layer runtime input.
|
|
217
|
+
*
|
|
218
|
+
* Surfaces (CLI, MCP, HTTP) render each typed layer's `input` schema onto
|
|
219
|
+
* their native idioms (flags, tool params, query strings). At execute time
|
|
220
|
+
* the parsed values are partitioned per layer and stored under this key as
|
|
221
|
+
* `Record<layerName, unknown>`. Layers that need runtime input read their
|
|
222
|
+
* own slot via `ctx.extensions?.[LAYER_INPUTS_KEY]?.[layer.name]`.
|
|
223
|
+
*
|
|
224
|
+
* @see TRL-473 for the CLI derivation contract.
|
|
225
|
+
*/
|
|
226
|
+
export const LAYER_INPUTS_KEY = '__trails_layer_inputs' as const;
|
|
227
|
+
|
|
228
|
+
/** Runtime context threaded through every trail execution */
|
|
229
|
+
export interface TrailContext {
|
|
230
|
+
readonly activation?: ActivationProvenance | undefined;
|
|
231
|
+
readonly requestId: string;
|
|
232
|
+
readonly abortSignal: AbortSignal;
|
|
233
|
+
readonly compose?: ComposeFn | undefined;
|
|
234
|
+
/**
|
|
235
|
+
* Emit a typed signal. Fans out to every trail with the signal in its
|
|
236
|
+
* `on:` declaration. Bound by the runner that holds the topo (typically
|
|
237
|
+
* `run()`); undefined when a context is constructed without topo access.
|
|
238
|
+
*/
|
|
239
|
+
readonly fire?: FireFn | undefined;
|
|
240
|
+
readonly permit?: BasePermit;
|
|
241
|
+
readonly workspaceRoot?: string | undefined;
|
|
242
|
+
readonly logger?: Logger | undefined;
|
|
243
|
+
readonly progress?: ProgressCallback | undefined;
|
|
244
|
+
readonly cwd?: string | undefined;
|
|
245
|
+
readonly env?: Record<string, string | undefined> | undefined;
|
|
246
|
+
readonly extensions?: Readonly<Record<string, unknown>> | undefined;
|
|
247
|
+
readonly resource?: ResourceLookup | undefined;
|
|
248
|
+
/**
|
|
249
|
+
* Whether the current invocation is a dry run.
|
|
250
|
+
*
|
|
251
|
+
* Defaults to `false`. Trails that don't read this field are unaffected.
|
|
252
|
+
* Trails that do read it decide what dry-run means for their domain — for
|
|
253
|
+
* example: preview the change without committing, validate inputs without
|
|
254
|
+
* performing side effects, or return what would happen without actually
|
|
255
|
+
* doing it.
|
|
256
|
+
*
|
|
257
|
+
* The framework only carries the flag from the surface (e.g. CLI
|
|
258
|
+
* `--dry-run`) into the context. It never short-circuits trail execution
|
|
259
|
+
* on its own based on this field.
|
|
260
|
+
*
|
|
261
|
+
* Pair this runtime signal with `TrailSpec.dryRun`, which declares whether a
|
|
262
|
+
* trail supports dry-run semantics for governance, derivation, and surface
|
|
263
|
+
* tooling.
|
|
264
|
+
*
|
|
265
|
+
* @remarks Always defined on contexts produced by `executeTrail` or
|
|
266
|
+
* `createTrailContext` (normalized to `false` when not provided).
|
|
267
|
+
*/
|
|
268
|
+
readonly dryRun?: boolean | undefined;
|
|
269
|
+
/**
|
|
270
|
+
* Wrap a callback in a child trace span.
|
|
271
|
+
*
|
|
272
|
+
* Always present on contexts produced by `executeTrail` or
|
|
273
|
+
* `createTrailContext`. Optional on the interface so manually constructed
|
|
274
|
+
* contexts (tests, ad-hoc compositions) don't have to supply one — call
|
|
275
|
+
* sites tolerate `undefined` by falling back to a no-op passthrough.
|
|
276
|
+
*/
|
|
277
|
+
readonly trace?: TraceFn | undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** Trail context for implementations that declare trail composition. */
|
|
281
|
+
export interface ComposeTrailContext extends TrailContext {
|
|
282
|
+
readonly compose: ComposeFn;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Permit requirement declared on a trail spec.
|
|
287
|
+
*
|
|
288
|
+
* A scopes object means the trail requires a permit with those scopes.
|
|
289
|
+
* `'public'` means the trail has explicitly opted out of auth.
|
|
290
|
+
* Omitting the field entirely means the trail hasn't declared an auth posture.
|
|
291
|
+
*/
|
|
292
|
+
export type PermitRequirement =
|
|
293
|
+
| { readonly scopes: readonly string[] }
|
|
294
|
+
| 'public';
|
|
295
|
+
|
|
296
|
+
/** Input shape used to seed a runtime TrailContext before resolution. */
|
|
297
|
+
export type TrailContextInit = Omit<TrailContext, 'resource' | 'trace'> & {
|
|
298
|
+
readonly resource?: ResourceLookup | undefined;
|
|
299
|
+
readonly trace?: TraceFn | undefined;
|
|
300
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { ValidationError } from './errors.js';
|
|
2
|
+
import { Result } from './result.js';
|
|
3
|
+
import type { Topo } from './topo.js';
|
|
4
|
+
import { validateDraftFreeTopo } from './draft.js';
|
|
5
|
+
import type { TopoDiagnostic } from './validate-topo.js';
|
|
6
|
+
import { validateTopo } from './validate-topo.js';
|
|
7
|
+
|
|
8
|
+
const DERIVATION_BLOCKING_RULES = new Set([
|
|
9
|
+
'compose-cycle',
|
|
10
|
+
'compose-exists',
|
|
11
|
+
'no-self-compose',
|
|
12
|
+
'activation-source-definition-unique',
|
|
13
|
+
'activation-source-edge-unique',
|
|
14
|
+
'activation-source-kind-known',
|
|
15
|
+
'activation-queue-valid',
|
|
16
|
+
'activation-schedule-valid',
|
|
17
|
+
'resource-exists',
|
|
18
|
+
'signal-fire-exists',
|
|
19
|
+
'signal-on-exists',
|
|
20
|
+
'signal-origin-exists',
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
const isDerivationBlockingIssue = (issue: TopoDiagnostic): boolean =>
|
|
24
|
+
DERIVATION_BLOCKING_RULES.has(issue.rule) ||
|
|
25
|
+
(issue.rule === 'activation-source-input-compatible' &&
|
|
26
|
+
issue.sourceKind === 'queue');
|
|
27
|
+
|
|
28
|
+
const keepDerivationBlockingIssues = (
|
|
29
|
+
result: ReturnType<typeof validateTopo>
|
|
30
|
+
) => {
|
|
31
|
+
if (result.isOk()) {
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const issues = (
|
|
36
|
+
result.error.context as { issues?: readonly TopoDiagnostic[] } | undefined
|
|
37
|
+
)?.issues;
|
|
38
|
+
const remainingIssues = issues?.filter(isDerivationBlockingIssue);
|
|
39
|
+
|
|
40
|
+
if (remainingIssues === undefined || remainingIssues.length === 0) {
|
|
41
|
+
return Result.ok();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return Result.err(
|
|
45
|
+
new ValidationError(
|
|
46
|
+
`Topo validation failed with ${remainingIssues.length} issue(s)`,
|
|
47
|
+
{
|
|
48
|
+
cause: result.error,
|
|
49
|
+
context: { issues: remainingIssues },
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Validate that a topo is ready for established outputs.
|
|
57
|
+
*
|
|
58
|
+
* Established surfaces still require the authored graph to be structurally
|
|
59
|
+
* valid, and they must also reject any remaining draft state.
|
|
60
|
+
*/
|
|
61
|
+
export const validateEstablishedTopo = (topo: Topo) => {
|
|
62
|
+
const structural = keepDerivationBlockingIssues(validateTopo(topo));
|
|
63
|
+
if (structural.isErr()) {
|
|
64
|
+
return structural;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const established = validateDraftFreeTopo(topo);
|
|
68
|
+
if (established.isErr()) {
|
|
69
|
+
return established;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return Result.ok();
|
|
73
|
+
};
|