@ontrails/core 1.0.0-beta.2 → 1.0.0-beta.21
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 +283 -5
- package/README.md +113 -33
- package/package.json +11 -1
- package/src/activation-provenance.ts +111 -0
- package/src/activation-source-compatibility.ts +430 -0
- package/src/activation-source-projection.ts +221 -0
- package/src/activation-source.ts +91 -0
- package/src/blob-ref.ts +51 -0
- package/src/branded.ts +1 -1
- package/src/compose-batch.ts +69 -0
- package/src/compose-schema.ts +36 -0
- package/src/context.ts +44 -10
- package/src/contour.ts +346 -0
- package/src/derive.ts +59 -18
- package/src/detours.ts +8 -0
- package/src/draft.ts +342 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +309 -48
- package/src/execute.ts +1577 -0
- package/src/fire.ts +1169 -0
- package/src/index.ts +477 -32
- package/src/internal/fork-ctx.ts +69 -0
- package/src/layer-projection.ts +193 -0
- package/src/layer.ts +43 -6
- package/src/observe.ts +361 -0
- package/src/path-security.ts +1 -1
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +11 -11
- package/src/resource-config.ts +792 -0
- package/src/resource.ts +194 -0
- package/src/result.ts +36 -4
- package/src/run.ts +76 -0
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +326 -0
- package/src/serialization.ts +119 -39
- package/src/signal-diagnostics.ts +633 -0
- package/src/signal-ref.ts +87 -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 +176 -0
- package/src/surface-versioning.ts +42 -0
- package/src/topo.ts +769 -57
- package/src/tracing.ts +345 -0
- package/src/trail.ts +1161 -29
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +139 -0
- package/src/trails-db.ts +208 -0
- package/src/transport-error-map.ts +174 -0
- package/src/type-utils.ts +87 -0
- package/src/types.ts +254 -12
- package/src/validate-established-topo.ts +69 -0
- package/src/validate-topo.ts +442 -35
- package/src/validation.ts +152 -4
- 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 +278 -0
- package/src/workspace.ts +1 -1
- package/src/zod-wrappers.ts +72 -0
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +0 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/blob-ref.d.ts +0 -20
- package/dist/blob-ref.d.ts.map +0 -1
- package/dist/blob-ref.js +0 -22
- package/dist/blob-ref.js.map +0 -1
- package/dist/branded.d.ts +0 -36
- package/dist/branded.d.ts.map +0 -1
- package/dist/branded.js +0 -89
- package/dist/branded.js.map +0 -1
- package/dist/collections.d.ts +0 -31
- package/dist/collections.d.ts.map +0 -1
- package/dist/collections.js +0 -60
- package/dist/collections.js.map +0 -1
- package/dist/context.d.ts +0 -10
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -15
- package/dist/context.js.map +0 -1
- package/dist/derive.d.ts +0 -33
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -122
- package/dist/derive.js.map +0 -1
- package/dist/errors.d.ts +0 -83
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -142
- package/dist/errors.js.map +0 -1
- package/dist/event.d.ts +0 -45
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -17
- package/dist/event.js.map +0 -1
- package/dist/fetch.d.ts +0 -15
- package/dist/fetch.d.ts.map +0 -1
- package/dist/fetch.js +0 -102
- package/dist/fetch.js.map +0 -1
- package/dist/guards.d.ts +0 -17
- package/dist/guards.d.ts.map +0 -1
- package/dist/guards.js +0 -25
- package/dist/guards.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/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/dist/index.d.ts +0 -34
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -38
- package/dist/index.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/layer.d.ts +0 -17
- package/dist/layer.d.ts.map +0 -1
- package/dist/layer.js +0 -21
- package/dist/layer.js.map +0 -1
- package/dist/path-security.d.ts +0 -28
- package/dist/path-security.d.ts.map +0 -1
- package/dist/path-security.js +0 -63
- package/dist/path-security.js.map +0 -1
- package/dist/patterns/bulk.d.ts +0 -15
- package/dist/patterns/bulk.d.ts.map +0 -1
- package/dist/patterns/bulk.js +0 -14
- package/dist/patterns/bulk.js.map +0 -1
- package/dist/patterns/change.d.ts +0 -10
- package/dist/patterns/change.d.ts.map +0 -1
- package/dist/patterns/change.js +0 -10
- package/dist/patterns/change.js.map +0 -1
- package/dist/patterns/date-range.d.ts +0 -10
- package/dist/patterns/date-range.d.ts.map +0 -1
- package/dist/patterns/date-range.js +0 -10
- package/dist/patterns/date-range.js.map +0 -1
- package/dist/patterns/index.d.ts +0 -9
- package/dist/patterns/index.d.ts.map +0 -1
- package/dist/patterns/index.js +0 -9
- package/dist/patterns/index.js.map +0 -1
- package/dist/patterns/pagination.d.ts +0 -18
- package/dist/patterns/pagination.d.ts.map +0 -1
- package/dist/patterns/pagination.js +0 -18
- package/dist/patterns/pagination.js.map +0 -1
- package/dist/patterns/progress.d.ts +0 -11
- package/dist/patterns/progress.d.ts.map +0 -1
- package/dist/patterns/progress.js +0 -11
- package/dist/patterns/progress.js.map +0 -1
- package/dist/patterns/sorting.d.ts +0 -13
- package/dist/patterns/sorting.d.ts.map +0 -1
- package/dist/patterns/sorting.js +0 -10
- package/dist/patterns/sorting.js.map +0 -1
- package/dist/patterns/status.d.ts +0 -15
- package/dist/patterns/status.d.ts.map +0 -1
- package/dist/patterns/status.js +0 -9
- package/dist/patterns/status.js.map +0 -1
- package/dist/patterns/timestamps.d.ts +0 -10
- package/dist/patterns/timestamps.d.ts.map +0 -1
- package/dist/patterns/timestamps.js +0 -10
- package/dist/patterns/timestamps.js.map +0 -1
- package/dist/redaction/index.d.ts +0 -4
- package/dist/redaction/index.d.ts.map +0 -1
- package/dist/redaction/index.js +0 -3
- package/dist/redaction/index.js.map +0 -1
- package/dist/redaction/patterns.d.ts +0 -9
- package/dist/redaction/patterns.d.ts.map +0 -1
- package/dist/redaction/patterns.js +0 -39
- package/dist/redaction/patterns.js.map +0 -1
- package/dist/redaction/redactor.d.ts +0 -27
- package/dist/redaction/redactor.d.ts.map +0 -1
- package/dist/redaction/redactor.js +0 -89
- package/dist/redaction/redactor.js.map +0 -1
- package/dist/resilience.d.ts +0 -34
- package/dist/resilience.d.ts.map +0 -1
- package/dist/resilience.js +0 -164
- package/dist/resilience.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -145
- package/dist/result.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -115
- package/dist/serialization.js.map +0 -1
- package/dist/topo.d.ts +0 -18
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -74
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -83
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -16
- package/dist/trail.js.map +0 -1
- package/dist/types.d.ts +0 -46
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/validate-topo.d.ts +0 -24
- package/dist/validate-topo.d.ts.map +0 -1
- package/dist/validate-topo.js +0 -108
- package/dist/validate-topo.js.map +0 -1
- package/dist/validation.d.ts +0 -27
- package/dist/validation.d.ts.map +0 -1
- package/dist/validation.js +0 -134
- package/dist/validation.js.map +0 -1
- package/dist/workspace.d.ts +0 -25
- package/dist/workspace.d.ts.map +0 -1
- package/dist/workspace.js +0 -57
- package/dist/workspace.js.map +0 -1
- package/src/__tests__/blob-ref.test.ts +0 -103
- package/src/__tests__/branded.test.ts +0 -148
- package/src/__tests__/collections.test.ts +0 -126
- package/src/__tests__/context.test.ts +0 -66
- package/src/__tests__/derive.test.ts +0 -159
- package/src/__tests__/errors.test.ts +0 -309
- package/src/__tests__/event.test.ts +0 -82
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/hike.test.ts +0 -117
- package/src/__tests__/job.test.ts +0 -98
- package/src/__tests__/layer.test.ts +0 -224
- package/src/__tests__/path-security.test.ts +0 -114
- package/src/__tests__/patterns.test.ts +0 -273
- package/src/__tests__/redaction.test.ts +0 -244
- package/src/__tests__/resilience.test.ts +0 -246
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/serialization.test.ts +0 -236
- package/src/__tests__/topo.test.ts +0 -184
- package/src/__tests__/trail.test.ts +0 -179
- package/src/__tests__/validate-topo.test.ts +0 -201
- package/src/__tests__/validation.test.ts +0 -283
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/adapters.ts +0 -68
- package/src/event.ts +0 -77
- package/src/health.ts +0 -23
- package/src/hike.ts +0 -77
- package/src/job.ts +0 -20
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
package/src/derive.ts
CHANGED
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Schema-driven field derivation for @ontrails/core
|
|
3
3
|
*
|
|
4
|
-
* Introspects Zod v4 schemas to produce a
|
|
5
|
-
* that UI
|
|
4
|
+
* Introspects Zod v4 schemas to produce a runtime-agnostic Field[] descriptor
|
|
5
|
+
* that UI consumers (CLI prompts, web forms, etc.) can consume.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { z } from 'zod';
|
|
9
9
|
|
|
10
|
+
import { ValidationError } from './errors.js';
|
|
11
|
+
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// Public types
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
13
15
|
|
|
14
|
-
/** A
|
|
16
|
+
/** A runtime-agnostic field descriptor derived from a Zod schema. */
|
|
15
17
|
export interface Field {
|
|
16
18
|
readonly name: string;
|
|
17
|
-
readonly type:
|
|
19
|
+
readonly type:
|
|
20
|
+
| 'string'
|
|
21
|
+
| 'number'
|
|
22
|
+
| 'boolean'
|
|
23
|
+
| 'enum'
|
|
24
|
+
| 'multiselect'
|
|
25
|
+
| 'string[]'
|
|
26
|
+
| 'number[]';
|
|
18
27
|
readonly label: string;
|
|
19
28
|
readonly required: boolean;
|
|
20
29
|
readonly default?: unknown | undefined;
|
|
@@ -133,15 +142,25 @@ interface DerivedFieldType {
|
|
|
133
142
|
type: Field['type'];
|
|
134
143
|
}
|
|
135
144
|
|
|
136
|
-
const fieldTypeByDef: Record<
|
|
145
|
+
const fieldTypeByDef: Record<
|
|
146
|
+
string,
|
|
147
|
+
(s: ZodInternals) => DerivedFieldType | null
|
|
148
|
+
> = {
|
|
137
149
|
array: (s) => {
|
|
138
150
|
const element = s._zod.def['element'] as unknown as ZodInternals;
|
|
139
|
-
const
|
|
151
|
+
const { inner } = unwrap(element);
|
|
152
|
+
const elementType = inner._zod.def['type'] as string;
|
|
140
153
|
if (elementType === 'enum') {
|
|
141
|
-
const entries =
|
|
154
|
+
const entries = inner._zod.def['entries'] as Record<string, string>;
|
|
142
155
|
return { options: Object.values(entries), type: 'multiselect' };
|
|
143
156
|
}
|
|
144
|
-
|
|
157
|
+
if (elementType !== 'number' && elementType !== 'string') {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
options: undefined,
|
|
162
|
+
type: elementType === 'number' ? 'number[]' : 'string[]',
|
|
163
|
+
};
|
|
145
164
|
},
|
|
146
165
|
boolean: () => ({ options: undefined, type: 'boolean' }),
|
|
147
166
|
enum: (s) => {
|
|
@@ -153,10 +172,10 @@ const fieldTypeByDef: Record<string, (s: ZodInternals) => DerivedFieldType> = {
|
|
|
153
172
|
};
|
|
154
173
|
|
|
155
174
|
/** Derive field type and raw options from the unwrapped Zod def. */
|
|
156
|
-
const deriveFieldType = (s: ZodInternals): DerivedFieldType => {
|
|
175
|
+
const deriveFieldType = (s: ZodInternals): DerivedFieldType | null => {
|
|
157
176
|
const defType = s._zod.def['type'] as string;
|
|
158
177
|
const derive = fieldTypeByDef[defType];
|
|
159
|
-
return derive ? derive(s) :
|
|
178
|
+
return derive ? derive(s) : null;
|
|
160
179
|
};
|
|
161
180
|
|
|
162
181
|
/** Build options array, merging with overrides when present. */
|
|
@@ -179,29 +198,49 @@ const buildOptions = (
|
|
|
179
198
|
});
|
|
180
199
|
};
|
|
181
200
|
|
|
201
|
+
/**
|
|
202
|
+
* Derive the canonical ordered CLI path from a trail ID.
|
|
203
|
+
*
|
|
204
|
+
* @throws {ValidationError} if the trail ID contains empty segments (e.g. consecutive dots).
|
|
205
|
+
*/
|
|
206
|
+
export const deriveCliPath = (trailId: string): string[] => {
|
|
207
|
+
const segments = trailId.split('.');
|
|
208
|
+
const emptyIndex = segments.findIndex((s) => s.length === 0);
|
|
209
|
+
if (emptyIndex !== -1) {
|
|
210
|
+
throw new ValidationError(
|
|
211
|
+
`Trail ID "${trailId}" contains an empty segment at position ${emptyIndex}`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
return segments;
|
|
215
|
+
};
|
|
216
|
+
|
|
182
217
|
// ---------------------------------------------------------------------------
|
|
183
218
|
// Public API
|
|
184
219
|
// ---------------------------------------------------------------------------
|
|
185
220
|
|
|
186
|
-
/**
|
|
187
|
-
* Derive a surface-agnostic Field[] from a Zod object schema.
|
|
188
|
-
*
|
|
189
|
-
* Uses Zod v4's `_zod.def` for introspection. Returns fields sorted by name.
|
|
190
|
-
*/
|
|
191
221
|
/** Derive a single field from a shape entry. */
|
|
192
222
|
const deriveField = (
|
|
193
223
|
key: string,
|
|
194
224
|
value: ZodInternals,
|
|
195
225
|
overrides?: Record<string, FieldOverride>
|
|
196
|
-
): Field => {
|
|
226
|
+
): Field | null => {
|
|
197
227
|
const { inner, required, defaultValue, description } = unwrap(value);
|
|
198
|
-
const
|
|
228
|
+
const derived = deriveFieldType(inner);
|
|
229
|
+
if (!derived) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const { type, options: rawOptions } = derived;
|
|
199
233
|
const override = overrides?.[key];
|
|
200
234
|
const label = override?.label ?? description ?? humanize(key);
|
|
201
235
|
const options = buildOptions(rawOptions, override?.options);
|
|
202
236
|
return { default: defaultValue, label, name: key, options, required, type };
|
|
203
237
|
};
|
|
204
238
|
|
|
239
|
+
/**
|
|
240
|
+
* Derive a runtime-agnostic Field[] from a Zod object schema.
|
|
241
|
+
*
|
|
242
|
+
* Uses Zod v4's `_zod.def` for introspection. Returns fields sorted by name.
|
|
243
|
+
*/
|
|
205
244
|
export const deriveFields = (
|
|
206
245
|
schema: z.ZodType,
|
|
207
246
|
overrides?: Record<string, FieldOverride>
|
|
@@ -219,5 +258,7 @@ export const deriveFields = (
|
|
|
219
258
|
const fields = Object.entries(shape).map(([key, value]) =>
|
|
220
259
|
deriveField(key, value, overrides)
|
|
221
260
|
);
|
|
222
|
-
return fields
|
|
261
|
+
return fields
|
|
262
|
+
.filter((field): field is Field => field !== null)
|
|
263
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
223
264
|
};
|
package/src/detours.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard upper bound for detour recovery attempts.
|
|
3
|
+
*
|
|
4
|
+
* Execution and derived surface/topo projections both clamp declared detour
|
|
5
|
+
* attempts to this value so runtime behavior and inspectable contracts stay in
|
|
6
|
+
* lockstep.
|
|
7
|
+
*/
|
|
8
|
+
export const DETOUR_MAX_ATTEMPTS_CAP = 5;
|
package/src/draft.ts
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import type { AnyContour } from './contour.js';
|
|
2
|
+
import { getContourReferences } from './contour.js';
|
|
3
|
+
import { ValidationError } from './errors.js';
|
|
4
|
+
import type { AnySignal } from './signal.js';
|
|
5
|
+
import type { AnyResource } from './resource.js';
|
|
6
|
+
import { Result } from './result.js';
|
|
7
|
+
import type { Topo } from './topo.js';
|
|
8
|
+
import type { AnyTrail } from './trail.js';
|
|
9
|
+
|
|
10
|
+
export const DRAFT_ID_PREFIX = '_draft.';
|
|
11
|
+
|
|
12
|
+
export type DraftDependencyKind =
|
|
13
|
+
| 'compose'
|
|
14
|
+
| 'contour'
|
|
15
|
+
| 'resource'
|
|
16
|
+
| 'replaced-by'
|
|
17
|
+
| 'schema-reference'
|
|
18
|
+
| 'signal-fire'
|
|
19
|
+
| 'signal-on'
|
|
20
|
+
| 'signal-from';
|
|
21
|
+
|
|
22
|
+
export interface DraftDependency {
|
|
23
|
+
readonly fromId: string;
|
|
24
|
+
readonly kind: DraftDependencyKind;
|
|
25
|
+
readonly toId: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DraftFinding {
|
|
29
|
+
readonly id: string;
|
|
30
|
+
readonly kind: 'contour' | 'resource' | 'signal' | 'trail' | 'unknown';
|
|
31
|
+
readonly message: string;
|
|
32
|
+
readonly rule: 'draft-contamination' | 'draft-id';
|
|
33
|
+
readonly via?: DraftDependencyKind | undefined;
|
|
34
|
+
readonly dependsOn?: string | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface DraftReport {
|
|
38
|
+
readonly contaminatedIds: ReadonlySet<string>;
|
|
39
|
+
readonly declaredDraftIds: ReadonlySet<string>;
|
|
40
|
+
readonly dependencies: readonly DraftDependency[];
|
|
41
|
+
readonly findings: readonly DraftFinding[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface DraftReason {
|
|
45
|
+
readonly dependsOn: string;
|
|
46
|
+
readonly via: DraftDependencyKind;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
type TopoNode = AnyContour | AnyResource | AnySignal | AnyTrail;
|
|
50
|
+
|
|
51
|
+
const replacedByTarget = (value: TopoNode): string | undefined => {
|
|
52
|
+
const raw = value as unknown as { replacedBy?: unknown };
|
|
53
|
+
return typeof raw.replacedBy === 'string' ? raw.replacedBy : undefined;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const isDraftId = (id: string): boolean =>
|
|
57
|
+
id.startsWith(DRAFT_ID_PREFIX);
|
|
58
|
+
|
|
59
|
+
const dependenciesFromIds = (
|
|
60
|
+
fromId: string,
|
|
61
|
+
toIds: readonly string[],
|
|
62
|
+
kind: DraftDependencyKind
|
|
63
|
+
): DraftDependency[] =>
|
|
64
|
+
toIds.map((toId) => ({
|
|
65
|
+
fromId,
|
|
66
|
+
kind,
|
|
67
|
+
toId,
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
const dependencyFromTarget = (
|
|
71
|
+
fromId: string,
|
|
72
|
+
toId: string | undefined,
|
|
73
|
+
kind: DraftDependencyKind
|
|
74
|
+
): DraftDependency[] =>
|
|
75
|
+
toId === undefined
|
|
76
|
+
? []
|
|
77
|
+
: [
|
|
78
|
+
{
|
|
79
|
+
fromId,
|
|
80
|
+
kind,
|
|
81
|
+
toId,
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
const trailDependencies = (trail: AnyTrail): DraftDependency[] => [
|
|
86
|
+
...dependenciesFromIds(
|
|
87
|
+
trail.id,
|
|
88
|
+
(trail.contours ?? []).map((contour) => contour.name),
|
|
89
|
+
'contour'
|
|
90
|
+
),
|
|
91
|
+
...dependenciesFromIds(trail.id, trail.composes, 'compose'),
|
|
92
|
+
...dependenciesFromIds(
|
|
93
|
+
trail.id,
|
|
94
|
+
trail.resources.map(({ id }) => id),
|
|
95
|
+
'resource'
|
|
96
|
+
),
|
|
97
|
+
...dependenciesFromIds(trail.id, trail.fires ?? [], 'signal-fire'),
|
|
98
|
+
...dependenciesFromIds(trail.id, trail.on ?? [], 'signal-on'),
|
|
99
|
+
...dependencyFromTarget(trail.id, replacedByTarget(trail), 'replaced-by'),
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
const contourDependencies = (contour: AnyContour): DraftDependency[] =>
|
|
103
|
+
dependenciesFromIds(
|
|
104
|
+
contour.name,
|
|
105
|
+
getContourReferences(contour).map((reference) => reference.contour),
|
|
106
|
+
'schema-reference'
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const signalDependencies = (signal: AnySignal): DraftDependency[] => [
|
|
110
|
+
...dependenciesFromIds(signal.id, signal.from ?? [], 'signal-from'),
|
|
111
|
+
...dependencyFromTarget(signal.id, replacedByTarget(signal), 'replaced-by'),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
const resourceDependencies = (resource: AnyResource): DraftDependency[] =>
|
|
115
|
+
dependencyFromTarget(resource.id, replacedByTarget(resource), 'replaced-by');
|
|
116
|
+
|
|
117
|
+
const nodeKind = (
|
|
118
|
+
id: string,
|
|
119
|
+
contours: ReadonlyMap<string, AnyContour>,
|
|
120
|
+
trails: ReadonlyMap<string, AnyTrail>,
|
|
121
|
+
signals: ReadonlyMap<string, AnySignal>,
|
|
122
|
+
resources: ReadonlyMap<string, AnyResource>
|
|
123
|
+
): DraftFinding['kind'] => {
|
|
124
|
+
if (contours.has(id)) {
|
|
125
|
+
return 'contour';
|
|
126
|
+
}
|
|
127
|
+
if (trails.has(id)) {
|
|
128
|
+
return 'trail';
|
|
129
|
+
}
|
|
130
|
+
if (signals.has(id)) {
|
|
131
|
+
return 'signal';
|
|
132
|
+
}
|
|
133
|
+
if (resources.has(id)) {
|
|
134
|
+
return 'resource';
|
|
135
|
+
}
|
|
136
|
+
return 'unknown';
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const displayKind = (kind: DraftFinding['kind']): string =>
|
|
140
|
+
kind === 'unknown' ? 'Node' : kind[0]?.toUpperCase() + kind.slice(1);
|
|
141
|
+
|
|
142
|
+
const draftIdsFromKeys = (keys: Iterable<string>): string[] =>
|
|
143
|
+
[...keys].filter(isDraftId);
|
|
144
|
+
|
|
145
|
+
const collectDeclaredDraftIds = (topo: Topo): ReadonlySet<string> =>
|
|
146
|
+
new Set([
|
|
147
|
+
...draftIdsFromKeys(topo.contours.keys()),
|
|
148
|
+
...draftIdsFromKeys(topo.trails.keys()),
|
|
149
|
+
...draftIdsFromKeys(topo.signals.keys()),
|
|
150
|
+
...draftIdsFromKeys(topo.resources.keys()),
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
const findingForDraftId = (
|
|
154
|
+
id: string,
|
|
155
|
+
kind: DraftFinding['kind']
|
|
156
|
+
): DraftFinding => ({
|
|
157
|
+
id,
|
|
158
|
+
kind,
|
|
159
|
+
message: `${displayKind(id ? kind : 'unknown')} "${id}" is draft and cannot appear in the established graph.`,
|
|
160
|
+
rule: 'draft-id',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const findingForContamination = (
|
|
164
|
+
id: string,
|
|
165
|
+
kind: DraftFinding['kind'],
|
|
166
|
+
reason: DraftReason
|
|
167
|
+
): DraftFinding => {
|
|
168
|
+
const dependencyLabel = isDraftId(reason.dependsOn)
|
|
169
|
+
? `draft "${reason.dependsOn}"`
|
|
170
|
+
: `draft-contaminated "${reason.dependsOn}"`;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
dependsOn: reason.dependsOn,
|
|
174
|
+
id,
|
|
175
|
+
kind,
|
|
176
|
+
message:
|
|
177
|
+
`Established ${kind} "${id}" depends on ${dependencyLabel} ` +
|
|
178
|
+
`via ${reason.via} and cannot appear in the established graph.`,
|
|
179
|
+
rule: 'draft-contamination',
|
|
180
|
+
via: reason.via,
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const contaminationReason = (
|
|
185
|
+
dependency: DraftDependency,
|
|
186
|
+
contaminatedIds: ReadonlySet<string>
|
|
187
|
+
): DraftReason | undefined => {
|
|
188
|
+
if (contaminatedIds.has(dependency.fromId)) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!isDraftId(dependency.toId) && !contaminatedIds.has(dependency.toId)) {
|
|
193
|
+
return undefined;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return {
|
|
197
|
+
dependsOn: dependency.toId,
|
|
198
|
+
via: dependency.kind,
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const markContaminatedDependency = (
|
|
203
|
+
dependency: DraftDependency,
|
|
204
|
+
contaminatedIds: Set<string>,
|
|
205
|
+
reasons: Map<string, DraftReason>
|
|
206
|
+
): boolean => {
|
|
207
|
+
const reason = contaminationReason(dependency, contaminatedIds);
|
|
208
|
+
|
|
209
|
+
if (reason === undefined) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
contaminatedIds.add(dependency.fromId);
|
|
214
|
+
reasons.set(dependency.fromId, reason);
|
|
215
|
+
return true;
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const propagateContaminatedIds = (
|
|
219
|
+
declaredDraftIds: ReadonlySet<string>,
|
|
220
|
+
dependencies: readonly DraftDependency[]
|
|
221
|
+
): { contaminatedIds: Set<string>; reasons: Map<string, DraftReason> } => {
|
|
222
|
+
const contaminatedIds = new Set<string>(declaredDraftIds);
|
|
223
|
+
const reasons = new Map<string, DraftReason>();
|
|
224
|
+
|
|
225
|
+
const visit = (): void => {
|
|
226
|
+
if (
|
|
227
|
+
dependencies.some((dependency) =>
|
|
228
|
+
markContaminatedDependency(dependency, contaminatedIds, reasons)
|
|
229
|
+
)
|
|
230
|
+
) {
|
|
231
|
+
visit();
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
visit();
|
|
236
|
+
return { contaminatedIds, reasons };
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const contaminationFindingForId = (
|
|
240
|
+
id: string,
|
|
241
|
+
declaredDraftIds: ReadonlySet<string>,
|
|
242
|
+
reasons: ReadonlyMap<string, DraftReason>,
|
|
243
|
+
contours: ReadonlyMap<string, AnyContour>,
|
|
244
|
+
trails: ReadonlyMap<string, AnyTrail>,
|
|
245
|
+
signals: ReadonlyMap<string, AnySignal>,
|
|
246
|
+
resources: ReadonlyMap<string, AnyResource>
|
|
247
|
+
): DraftFinding | undefined => {
|
|
248
|
+
if (declaredDraftIds.has(id)) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const reason = reasons.get(id);
|
|
253
|
+
if (reason === undefined) {
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return findingForContamination(
|
|
258
|
+
id,
|
|
259
|
+
nodeKind(id, contours, trails, signals, resources),
|
|
260
|
+
reason
|
|
261
|
+
);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const collectFindings = (
|
|
265
|
+
declaredDraftIds: ReadonlySet<string>,
|
|
266
|
+
contaminatedIds: ReadonlySet<string>,
|
|
267
|
+
reasons: ReadonlyMap<string, DraftReason>,
|
|
268
|
+
contours: ReadonlyMap<string, AnyContour>,
|
|
269
|
+
trails: ReadonlyMap<string, AnyTrail>,
|
|
270
|
+
signals: ReadonlyMap<string, AnySignal>,
|
|
271
|
+
resources: ReadonlyMap<string, AnyResource>
|
|
272
|
+
): DraftFinding[] => [
|
|
273
|
+
...[...declaredDraftIds]
|
|
274
|
+
.toSorted()
|
|
275
|
+
.map((id) =>
|
|
276
|
+
findingForDraftId(id, nodeKind(id, contours, trails, signals, resources))
|
|
277
|
+
),
|
|
278
|
+
...[...contaminatedIds].toSorted().flatMap((id) => {
|
|
279
|
+
const finding = contaminationFindingForId(
|
|
280
|
+
id,
|
|
281
|
+
declaredDraftIds,
|
|
282
|
+
reasons,
|
|
283
|
+
contours,
|
|
284
|
+
trails,
|
|
285
|
+
signals,
|
|
286
|
+
resources
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
return finding === undefined ? [] : [finding];
|
|
290
|
+
}),
|
|
291
|
+
];
|
|
292
|
+
|
|
293
|
+
const collectDependencies = (topo: Topo): DraftDependency[] => [
|
|
294
|
+
...[...topo.contours.values()].flatMap(contourDependencies),
|
|
295
|
+
...[...topo.trails.values()].flatMap(trailDependencies),
|
|
296
|
+
...[...topo.signals.values()].flatMap(signalDependencies),
|
|
297
|
+
...[...topo.resources.values()].flatMap(resourceDependencies),
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
export const deriveDraftReport = (topo: Topo): DraftReport => {
|
|
301
|
+
const declaredDraftIds = collectDeclaredDraftIds(topo);
|
|
302
|
+
const dependencies = collectDependencies(topo);
|
|
303
|
+
const { contaminatedIds, reasons } = propagateContaminatedIds(
|
|
304
|
+
declaredDraftIds,
|
|
305
|
+
dependencies
|
|
306
|
+
);
|
|
307
|
+
const findings = collectFindings(
|
|
308
|
+
declaredDraftIds,
|
|
309
|
+
contaminatedIds,
|
|
310
|
+
reasons,
|
|
311
|
+
topo.contours,
|
|
312
|
+
topo.trails,
|
|
313
|
+
topo.signals,
|
|
314
|
+
topo.resources
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
contaminatedIds,
|
|
319
|
+
declaredDraftIds,
|
|
320
|
+
dependencies,
|
|
321
|
+
findings,
|
|
322
|
+
};
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const validateDraftFreeTopo = (
|
|
326
|
+
topo: Topo
|
|
327
|
+
): Result<void, ValidationError> => {
|
|
328
|
+
const analysis = deriveDraftReport(topo);
|
|
329
|
+
|
|
330
|
+
if (analysis.findings.length === 0) {
|
|
331
|
+
return Result.ok();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return Result.err(
|
|
335
|
+
new ValidationError(
|
|
336
|
+
`Established topo validation failed with ${analysis.findings.length} draft issue(s)`,
|
|
337
|
+
{
|
|
338
|
+
context: { issues: analysis.findings },
|
|
339
|
+
}
|
|
340
|
+
)
|
|
341
|
+
);
|
|
342
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { ErrorCategory } from './errors.js';
|
|
2
|
+
import { isTrailsError } from './errors.js';
|
|
3
|
+
import { createRedactor } from './redaction/index.js';
|
|
4
|
+
|
|
5
|
+
const errorRedactor = createRedactor();
|
|
6
|
+
|
|
7
|
+
export const INTERNAL_ERROR_PUBLIC_MESSAGE = 'Internal server error';
|
|
8
|
+
|
|
9
|
+
export interface ErrorDiagnosticsProjection {
|
|
10
|
+
readonly category?: ErrorCategory | undefined;
|
|
11
|
+
readonly context?: Record<string, unknown> | undefined;
|
|
12
|
+
readonly message: string;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly retryable?: boolean | undefined;
|
|
15
|
+
readonly stack?: string | undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const redactErrorString = (value: string): string =>
|
|
19
|
+
errorRedactor.redact(value);
|
|
20
|
+
|
|
21
|
+
export const redactErrorContext = (
|
|
22
|
+
context: Record<string, unknown> | undefined
|
|
23
|
+
): Record<string, unknown> | undefined =>
|
|
24
|
+
context === undefined ? undefined : errorRedactor.redactObject(context);
|
|
25
|
+
|
|
26
|
+
export const redactErrorStack = (
|
|
27
|
+
stack: string | undefined
|
|
28
|
+
): string | undefined =>
|
|
29
|
+
stack === undefined ? undefined : redactErrorString(stack);
|
|
30
|
+
|
|
31
|
+
export const projectErrorDiagnostics = (
|
|
32
|
+
error: Error
|
|
33
|
+
): ErrorDiagnosticsProjection => {
|
|
34
|
+
const context = isTrailsError(error)
|
|
35
|
+
? redactErrorContext(error.context)
|
|
36
|
+
: undefined;
|
|
37
|
+
const stack = redactErrorStack(error.stack);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...(isTrailsError(error)
|
|
41
|
+
? {
|
|
42
|
+
category: error.category,
|
|
43
|
+
retryable: error.retryable,
|
|
44
|
+
}
|
|
45
|
+
: {}),
|
|
46
|
+
...(context === undefined ? {} : { context }),
|
|
47
|
+
message: redactErrorString(error.message),
|
|
48
|
+
name: error.name || error.constructor.name || 'Error',
|
|
49
|
+
...(stack === undefined ? {} : { stack }),
|
|
50
|
+
};
|
|
51
|
+
};
|