@ontrails/core 1.0.0-beta.15 → 1.0.0-beta.16
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 +63 -0
- package/README.md +61 -16
- package/package.json +9 -8
- 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/context.ts +4 -0
- package/src/{internal/cross-batch.ts → cross-batch.ts} +3 -11
- package/src/detours.ts +8 -0
- package/src/draft.ts +4 -0
- package/src/error-projection.ts +51 -0
- package/src/errors.ts +192 -39
- package/src/execute.ts +443 -44
- package/src/fire.ts +965 -91
- package/src/index.ts +285 -38
- package/src/layer-projection.ts +192 -0
- package/src/layer.ts +32 -1
- package/src/observe.ts +361 -0
- package/src/permits.ts +12 -0
- package/src/redaction/patterns.ts +6 -3
- package/src/resilience.ts +8 -8
- package/src/resource-config.ts +477 -44
- package/src/resource.ts +21 -1
- package/src/result.ts +18 -0
- package/src/run.ts +2 -2
- package/src/schedule-runtime.ts +689 -0
- package/src/schedule.ts +324 -0
- package/src/serialization.ts +90 -57
- package/src/signal-diagnostics.ts +633 -0
- package/src/{internal/signal-ref.ts → signal-ref.ts} +1 -1
- package/src/signal.ts +37 -0
- package/src/store/accessor-protocol.ts +5 -5
- package/src/structured-examples.ts +245 -0
- package/src/surface-derivation.ts +91 -0
- package/src/surface-filter.ts +7 -13
- package/src/topo.ts +386 -6
- package/src/tracing.ts +345 -0
- package/src/trail.ts +168 -22
- package/src/trails/derive-trail.ts +1 -1
- package/src/trails/ingest.ts +7 -15
- package/src/{internal/trails-db.ts → trails-db.ts} +42 -21
- package/src/transport-error-map.ts +146 -28
- package/src/types.ts +82 -28
- package/src/validate-established-topo.ts +6 -0
- package/src/validate-topo.ts +212 -5
- package/src/validation.ts +58 -1
- package/src/webhook.ts +276 -0
- package/src/{internal/zod-wrappers.ts → zod-wrappers.ts} +0 -6
- package/.turbo/turbo-build.log +0 -1
- package/.turbo/turbo-lint.log +0 -3
- package/.turbo/turbo-typecheck.log +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 -22
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -37
- package/dist/context.js.map +0 -1
- package/dist/contour.d.ts +0 -81
- package/dist/contour.d.ts.map +0 -1
- package/dist/contour.js +0 -176
- package/dist/contour.js.map +0 -1
- package/dist/cross-schema.d.ts +0 -18
- package/dist/cross-schema.d.ts.map +0 -1
- package/dist/cross-schema.js +0 -29
- package/dist/cross-schema.js.map +0 -1
- package/dist/derive.d.ts +0 -44
- package/dist/derive.d.ts.map +0 -1
- package/dist/derive.js +0 -149
- package/dist/derive.js.map +0 -1
- package/dist/draft.d.ts +0 -28
- package/dist/draft.d.ts.map +0 -1
- package/dist/draft.js +0 -157
- package/dist/draft.js.map +0 -1
- package/dist/errors.d.ts +0 -141
- package/dist/errors.d.ts.map +0 -1
- package/dist/errors.js +0 -185
- package/dist/errors.js.map +0 -1
- package/dist/execute.d.ts +0 -52
- package/dist/execute.d.ts.map +0 -1
- package/dist/execute.js +0 -511
- package/dist/execute.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/fire.d.ts +0 -43
- package/dist/fire.d.ts.map +0 -1
- package/dist/fire.js +0 -185
- package/dist/fire.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/index.d.ts +0 -59
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -61
- package/dist/index.js.map +0 -1
- package/dist/internal/cross-batch.d.ts +0 -44
- package/dist/internal/cross-batch.d.ts.map +0 -1
- package/dist/internal/cross-batch.js +0 -58
- package/dist/internal/cross-batch.js.map +0 -1
- package/dist/internal/fork-ctx.d.ts +0 -41
- package/dist/internal/fork-ctx.d.ts.map +0 -1
- package/dist/internal/fork-ctx.js +0 -47
- package/dist/internal/fork-ctx.js.map +0 -1
- package/dist/internal/signal-ref.d.ts +0 -15
- package/dist/internal/signal-ref.d.ts.map +0 -1
- package/dist/internal/signal-ref.js +0 -41
- package/dist/internal/signal-ref.js.map +0 -1
- package/dist/internal/topo-saves.d.ts +0 -53
- package/dist/internal/topo-saves.d.ts.map +0 -1
- package/dist/internal/topo-saves.js +0 -449
- package/dist/internal/topo-saves.js.map +0 -1
- package/dist/internal/topo-snapshots.d.ts +0 -57
- package/dist/internal/topo-snapshots.d.ts.map +0 -1
- package/dist/internal/topo-snapshots.js +0 -372
- package/dist/internal/topo-snapshots.js.map +0 -1
- package/dist/internal/topo-store-read.d.ts +0 -70
- package/dist/internal/topo-store-read.d.ts.map +0 -1
- package/dist/internal/topo-store-read.js +0 -221
- package/dist/internal/topo-store-read.js.map +0 -1
- package/dist/internal/topo-store.d.ts +0 -26
- package/dist/internal/topo-store.d.ts.map +0 -1
- package/dist/internal/topo-store.js +0 -621
- package/dist/internal/topo-store.js.map +0 -1
- package/dist/internal/tracing.d.ts +0 -98
- package/dist/internal/tracing.d.ts.map +0 -1
- package/dist/internal/tracing.js +0 -102
- package/dist/internal/tracing.js.map +0 -1
- package/dist/internal/trails-db.d.ts +0 -16
- package/dist/internal/trails-db.d.ts.map +0 -1
- package/dist/internal/trails-db.js +0 -118
- package/dist/internal/trails-db.js.map +0 -1
- package/dist/internal/zod-wrappers.d.ts +0 -40
- package/dist/internal/zod-wrappers.d.ts.map +0 -1
- package/dist/internal/zod-wrappers.js +0 -59
- package/dist/internal/zod-wrappers.js.map +0 -1
- package/dist/layer.d.ts +0 -23
- 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/resource-config.d.ts +0 -22
- package/dist/resource-config.d.ts.map +0 -1
- package/dist/resource-config.js +0 -210
- package/dist/resource-config.js.map +0 -1
- package/dist/resource.d.ts +0 -74
- package/dist/resource.d.ts.map +0 -1
- package/dist/resource.js +0 -61
- package/dist/resource.js.map +0 -1
- package/dist/result.d.ts +0 -57
- package/dist/result.d.ts.map +0 -1
- package/dist/result.js +0 -156
- package/dist/result.js.map +0 -1
- package/dist/run.d.ts +0 -28
- package/dist/run.d.ts.map +0 -1
- package/dist/run.js +0 -35
- package/dist/run.js.map +0 -1
- package/dist/serialization.d.ts +0 -27
- package/dist/serialization.d.ts.map +0 -1
- package/dist/serialization.js +0 -154
- package/dist/serialization.js.map +0 -1
- package/dist/signal.d.ts +0 -33
- package/dist/signal.d.ts.map +0 -1
- package/dist/signal.js +0 -17
- package/dist/signal.js.map +0 -1
- package/dist/store/accessor-protocol.d.ts +0 -51
- package/dist/store/accessor-protocol.d.ts.map +0 -1
- package/dist/store/accessor-protocol.js +0 -2
- package/dist/store/accessor-protocol.js.map +0 -1
- package/dist/store/index.d.ts +0 -2
- package/dist/store/index.d.ts.map +0 -1
- package/dist/store/index.js +0 -2
- package/dist/store/index.js.map +0 -1
- package/dist/surface-filter.d.ts +0 -13
- package/dist/surface-filter.d.ts.map +0 -1
- package/dist/surface-filter.js +0 -76
- package/dist/surface-filter.js.map +0 -1
- package/dist/topo-store.d.ts +0 -62
- package/dist/topo-store.d.ts.map +0 -1
- package/dist/topo-store.js +0 -319
- package/dist/topo-store.js.map +0 -1
- package/dist/topo.d.ts +0 -39
- package/dist/topo.d.ts.map +0 -1
- package/dist/topo.js +0 -241
- package/dist/topo.js.map +0 -1
- package/dist/trail.d.ts +0 -163
- package/dist/trail.d.ts.map +0 -1
- package/dist/trail.js +0 -75
- package/dist/trail.js.map +0 -1
- package/dist/trails/derive-trail.d.ts +0 -66
- package/dist/trails/derive-trail.d.ts.map +0 -1
- package/dist/trails/derive-trail.js +0 -378
- package/dist/trails/derive-trail.js.map +0 -1
- package/dist/trails/index.d.ts +0 -5
- package/dist/trails/index.d.ts.map +0 -1
- package/dist/trails/index.js +0 -3
- package/dist/trails/index.js.map +0 -1
- package/dist/trails/ingest.d.ts +0 -24
- package/dist/trails/ingest.d.ts.map +0 -1
- package/dist/trails/ingest.js +0 -63
- package/dist/trails/ingest.js.map +0 -1
- package/dist/transport-error-map.d.ts +0 -106
- package/dist/transport-error-map.d.ts.map +0 -1
- package/dist/transport-error-map.js +0 -24
- package/dist/transport-error-map.js.map +0 -1
- package/dist/type-checks.test-d.d.ts +0 -71
- package/dist/type-checks.test-d.d.ts.map +0 -1
- package/dist/type-checks.test-d.js +0 -12
- package/dist/type-checks.test-d.js.map +0 -1
- package/dist/type-utils.d.ts +0 -33
- package/dist/type-utils.d.ts.map +0 -1
- package/dist/type-utils.js +0 -12
- package/dist/type-utils.js.map +0 -1
- package/dist/types.d.ts +0 -177
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -3
- package/dist/types.js.map +0 -1
- package/dist/validate-established-topo.d.ts +0 -76
- package/dist/validate-established-topo.d.ts.map +0 -1
- package/dist/validate-established-topo.js +0 -43
- package/dist/validate-established-topo.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 -193
- 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 -165
- 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 -77
- package/src/__tests__/contour.test.ts +0 -263
- package/src/__tests__/derive-trail.test.ts +0 -668
- package/src/__tests__/derive.test.ts +0 -260
- package/src/__tests__/errors.test.ts +0 -427
- package/src/__tests__/execute.test.ts +0 -1563
- package/src/__tests__/fetch.test.ts +0 -217
- package/src/__tests__/fire.test.ts +0 -1056
- package/src/__tests__/fork-ctx.test.ts +0 -83
- package/src/__tests__/guards.test.ts +0 -102
- package/src/__tests__/ingest.test.ts +0 -191
- package/src/__tests__/layer.test.ts +0 -125
- 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__/resource.test.ts +0 -204
- package/src/__tests__/result.test.ts +0 -155
- package/src/__tests__/run.test.ts +0 -181
- package/src/__tests__/serialization.test.ts +0 -401
- package/src/__tests__/service-config.test.ts +0 -224
- package/src/__tests__/signal.test.ts +0 -82
- package/src/__tests__/surface-filter.test.ts +0 -196
- package/src/__tests__/topo-store-read.test.ts +0 -256
- package/src/__tests__/topo-store.test.ts +0 -869
- package/src/__tests__/topo.test.ts +0 -573
- package/src/__tests__/trail-permit.test.ts +0 -60
- package/src/__tests__/trail.test.ts +0 -426
- package/src/__tests__/trails-db.test.ts +0 -198
- package/src/__tests__/transport-error-map.test.ts +0 -82
- package/src/__tests__/type-utils.test.ts +0 -118
- package/src/__tests__/validate-topo.test.ts +0 -611
- package/src/__tests__/validation.test.ts +0 -336
- package/src/__tests__/workspace.test.ts +0 -183
- package/src/__tests__/zod-wrappers.test.ts +0 -62
- package/src/internal/topo-snapshots.ts +0 -517
- package/src/internal/topo-store-read.ts +0 -481
- package/src/internal/topo-store.ts +0 -1192
- package/src/internal/tracing.ts +0 -187
- package/src/topo-store.ts +0 -510
- package/src/type-checks.test-d.ts +0 -106
- package/tsconfig.json +0 -9
- package/tsconfig.tests.json +0 -10
- package/tsconfig.tsbuildinfo +0 -1
package/src/topo.ts
CHANGED
|
@@ -4,10 +4,22 @@
|
|
|
4
4
|
|
|
5
5
|
import type { AnyContour } from './contour.js';
|
|
6
6
|
import { ValidationError } from './errors.js';
|
|
7
|
+
import type { ActivationEntry } from './activation-source.js';
|
|
7
8
|
import {
|
|
8
9
|
getLateBoundSignalRef,
|
|
9
10
|
parseLateBoundSignalMarker,
|
|
10
|
-
} from './
|
|
11
|
+
} from './signal-ref.js';
|
|
12
|
+
import type { Layer } from './layer.js';
|
|
13
|
+
import {
|
|
14
|
+
hasObserveCapabilities,
|
|
15
|
+
isLogger,
|
|
16
|
+
isLogSink,
|
|
17
|
+
isObserveConfig,
|
|
18
|
+
isObserveInput,
|
|
19
|
+
isTraceSink,
|
|
20
|
+
normalizeObserve,
|
|
21
|
+
} from './observe.js';
|
|
22
|
+
import type { ObserveConfig, TopoOptions } from './observe.js';
|
|
11
23
|
import type { AnySignal } from './signal.js';
|
|
12
24
|
import type { AnyResource } from './resource.js';
|
|
13
25
|
import { isResource } from './resource.js';
|
|
@@ -31,6 +43,16 @@ export interface Topo {
|
|
|
31
43
|
readonly trails: ReadonlyMap<string, AnyTrail>;
|
|
32
44
|
readonly signals: ReadonlyMap<string, AnySignal>;
|
|
33
45
|
readonly resources: ReadonlyMap<string, AnyResource>;
|
|
46
|
+
readonly observe?: ObserveConfig | undefined;
|
|
47
|
+
/**
|
|
48
|
+
* Typed layers attached at topo scope (always present, default `[]`).
|
|
49
|
+
*
|
|
50
|
+
* The CLI/MCP/HTTP surfaces forward these into `executeTrail` as
|
|
51
|
+
* `topoLayers`, where they are composed outermost in the layer chain.
|
|
52
|
+
* The final composition order is `topo → surface → trail → blaze`
|
|
53
|
+
* (outermost-first).
|
|
54
|
+
*/
|
|
55
|
+
readonly layers: readonly Layer[];
|
|
34
56
|
readonly count: number;
|
|
35
57
|
readonly contourCount: number;
|
|
36
58
|
readonly resourceCount: number;
|
|
@@ -72,7 +94,9 @@ const createTopo = (
|
|
|
72
94
|
contours: ReadonlyMap<string, AnyContour>,
|
|
73
95
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
74
96
|
signals: ReadonlyMap<string, AnySignal>,
|
|
75
|
-
resources: ReadonlyMap<string, AnyResource
|
|
97
|
+
resources: ReadonlyMap<string, AnyResource>,
|
|
98
|
+
observe: ObserveConfig | undefined,
|
|
99
|
+
layers: readonly Layer[]
|
|
76
100
|
): Topo => ({
|
|
77
101
|
contourCount: contours.size,
|
|
78
102
|
contourIds(): string[] {
|
|
@@ -121,6 +145,8 @@ const createTopo = (
|
|
|
121
145
|
...(identity.description !== undefined && {
|
|
122
146
|
description: identity.description,
|
|
123
147
|
}),
|
|
148
|
+
...(observe !== undefined && { observe }),
|
|
149
|
+
layers,
|
|
124
150
|
resourceCount: resources.size,
|
|
125
151
|
resourceIds(): string[] {
|
|
126
152
|
return [...resources.keys()];
|
|
@@ -287,6 +313,38 @@ const resolveTrailSignalIds = (
|
|
|
287
313
|
return { changed, ids };
|
|
288
314
|
};
|
|
289
315
|
|
|
316
|
+
const resolveTrailActivationSources = (
|
|
317
|
+
trailId: string,
|
|
318
|
+
activations: readonly ActivationEntry[],
|
|
319
|
+
lateBoundSignalIdsByToken: ReadonlyMap<string, readonly string[]>
|
|
320
|
+
): { changed: boolean; activations: readonly ActivationEntry[] } => {
|
|
321
|
+
let changed = false;
|
|
322
|
+
const resolved = Object.freeze(
|
|
323
|
+
activations.map((entry) => {
|
|
324
|
+
if (entry.source.kind !== 'signal') {
|
|
325
|
+
return entry;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const resolvedId = resolveLateBoundSignalId(
|
|
329
|
+
trailId,
|
|
330
|
+
entry.source.id,
|
|
331
|
+
lateBoundSignalIdsByToken
|
|
332
|
+
);
|
|
333
|
+
if (resolvedId === entry.source.id) {
|
|
334
|
+
return entry;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
changed = true;
|
|
338
|
+
return Object.freeze({
|
|
339
|
+
...entry,
|
|
340
|
+
source: Object.freeze({ ...entry.source, id: resolvedId }),
|
|
341
|
+
});
|
|
342
|
+
})
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return { activations: resolved, changed };
|
|
346
|
+
};
|
|
347
|
+
|
|
290
348
|
const finalizeTrailSignals = (
|
|
291
349
|
trails: ReadonlyMap<string, AnyTrail>,
|
|
292
350
|
resources: ReadonlyMap<string, AnyResource>
|
|
@@ -305,8 +363,17 @@ const finalizeTrailSignals = (
|
|
|
305
363
|
trail.on ?? [],
|
|
306
364
|
lateBoundSignalIdsByToken
|
|
307
365
|
);
|
|
366
|
+
const resolvedActivationSources = resolveTrailActivationSources(
|
|
367
|
+
trail.id,
|
|
368
|
+
trail.activationSources ?? [],
|
|
369
|
+
lateBoundSignalIdsByToken
|
|
370
|
+
);
|
|
308
371
|
|
|
309
|
-
if (
|
|
372
|
+
if (
|
|
373
|
+
!resolvedFires.changed &&
|
|
374
|
+
!resolvedOn.changed &&
|
|
375
|
+
!resolvedActivationSources.changed
|
|
376
|
+
) {
|
|
310
377
|
finalized.set(trail.id, trail);
|
|
311
378
|
continue;
|
|
312
379
|
}
|
|
@@ -315,6 +382,7 @@ const finalizeTrailSignals = (
|
|
|
315
382
|
trail.id,
|
|
316
383
|
Object.freeze({
|
|
317
384
|
...trail,
|
|
385
|
+
activationSources: resolvedActivationSources.activations,
|
|
318
386
|
fires: resolvedFires.ids,
|
|
319
387
|
on: resolvedOn.ids,
|
|
320
388
|
})
|
|
@@ -427,14 +495,320 @@ const registerModuleValues = (
|
|
|
427
495
|
}
|
|
428
496
|
};
|
|
429
497
|
|
|
430
|
-
|
|
498
|
+
const TOPO_OPTION_KEYS = ['layers', 'observe'] as const;
|
|
499
|
+
const TOPO_OPTION_KEY_SET: ReadonlySet<string> = new Set(TOPO_OPTION_KEYS);
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Brand symbol applied by `topo.options()`. The presence of this symbol
|
|
503
|
+
* marks an object as an explicit `TopoOptions` payload, which is the
|
|
504
|
+
* unambiguous way to disambiguate a trailing options object from a
|
|
505
|
+
* trailing module export. Use `topo.options()` whenever a module might
|
|
506
|
+
* legitimately export only fields whose names collide with topo options
|
|
507
|
+
* (for example a module whose sole export is `observe`).
|
|
508
|
+
*/
|
|
509
|
+
const TOPO_OPTIONS_BRAND: unique symbol = Symbol('trails.topo.options');
|
|
510
|
+
|
|
511
|
+
const hasOptionsBrand = (value: object): boolean =>
|
|
512
|
+
(value as { [TOPO_OPTIONS_BRAND]?: true })[TOPO_OPTIONS_BRAND] === true;
|
|
513
|
+
|
|
514
|
+
const looksLikeTopoOptionsShape = (value: object): boolean => {
|
|
515
|
+
const keys = Object.keys(value);
|
|
516
|
+
if (keys.length === 0) {
|
|
517
|
+
return false;
|
|
518
|
+
}
|
|
519
|
+
return keys.every((key) => TOPO_OPTION_KEY_SET.has(key));
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const detectUnknownOptionKeys = (value: object): readonly string[] => {
|
|
523
|
+
const unknown: string[] = [];
|
|
524
|
+
for (const key of Object.keys(value)) {
|
|
525
|
+
if (!TOPO_OPTION_KEY_SET.has(key)) {
|
|
526
|
+
unknown.push(key);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return unknown;
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const hasRegistrableKind = (value: unknown): boolean => {
|
|
533
|
+
if (typeof value !== 'object' || value === null) {
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
const { kind } = value as { kind?: unknown };
|
|
537
|
+
return (
|
|
538
|
+
kind === 'contour' ||
|
|
539
|
+
kind === 'trail' ||
|
|
540
|
+
kind === 'signal' ||
|
|
541
|
+
kind === 'resource'
|
|
542
|
+
);
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* When a branded `topo.options()` payload carries a bare `LogSink` in the
|
|
547
|
+
* `observe` slot, rewrite it to the explicit `{ log: sink }` form. The brand
|
|
548
|
+
* already signals "this is options, not a module"; once that is settled, a
|
|
549
|
+
* bare `LogSink` unambiguously names a log target and should not be rejected
|
|
550
|
+
* as ambiguous downstream.
|
|
551
|
+
*
|
|
552
|
+
* Bare `TraceSink` values (no `name` field) are left untouched — they already
|
|
553
|
+
* round-trip through `normalizeObserve` via the `isTraceSink` fallthrough.
|
|
554
|
+
* Already-disambiguated shapes (`{ log }`, `{ trace }`, `Logger`,
|
|
555
|
+
* `ObserveCapable`, etc.) are also left untouched.
|
|
556
|
+
*/
|
|
557
|
+
const disambiguateBrandedObserve = (options: TopoOptions): TopoOptions => {
|
|
558
|
+
const { observe } = options;
|
|
559
|
+
if (
|
|
560
|
+
observe === undefined ||
|
|
561
|
+
isLogger(observe) ||
|
|
562
|
+
isObserveConfig(observe) ||
|
|
563
|
+
hasObserveCapabilities(observe)
|
|
564
|
+
) {
|
|
565
|
+
return options;
|
|
566
|
+
}
|
|
567
|
+
if (isLogSink(observe)) {
|
|
568
|
+
return { ...options, observe: { log: observe } };
|
|
569
|
+
}
|
|
570
|
+
return options;
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Decide whether the trailing argument should be treated as a
|
|
575
|
+
* `TopoOptions` payload, a module export, or rejected as ambiguous.
|
|
576
|
+
*
|
|
577
|
+
* Resolution rules (in order):
|
|
578
|
+
* 1. Branded via `topo.options()` → always options. Unknown option
|
|
579
|
+
* keys throw, and downstream `normalizeObserve` rejects malformed
|
|
580
|
+
* values. A bare `LogSink` (`{ name, write }`) in the `observe`
|
|
581
|
+
* slot is auto-routed to `{ log: sink }` so the brand is the
|
|
582
|
+
* complete escape hatch the docs promise.
|
|
583
|
+
* 2. The shape does not look like `TopoOptions` (mixed keys or no
|
|
584
|
+
* keys) → module.
|
|
585
|
+
* 3. The trailing arg is a registrable module export
|
|
586
|
+
* (`kind: 'trail' | 'contour' | …`) under a known option key →
|
|
587
|
+
* module. Preserves the "module exporting a single trail named
|
|
588
|
+
* `observe`" case that the warden and existing apps rely on.
|
|
589
|
+
* 4. The `observe` value is a bare `LogSink` or `TraceSink` (a sink
|
|
590
|
+
* shape that could equally plausibly be a module export named
|
|
591
|
+
* `observe`) → throw, since the call is genuinely ambiguous.
|
|
592
|
+
* `topo.options()` exists to disambiguate.
|
|
593
|
+
* 5. The `observe` value is otherwise a recognizable `ObserveInput`
|
|
594
|
+
* (`Logger`, `ObserveConfig`, or `ObserveCapable`) → options.
|
|
595
|
+
* Those shapes carry enough structure that they cannot be
|
|
596
|
+
* mistaken for a generic module export.
|
|
597
|
+
* 6. Otherwise (non-sink helper object, function, primitive, etc.)
|
|
598
|
+
* → module. The non-registrable export is silently ignored, the
|
|
599
|
+
* same as any other unrecognized value in a module record.
|
|
600
|
+
*/
|
|
601
|
+
const classifyTrailingArgument = (
|
|
602
|
+
value: unknown
|
|
603
|
+
):
|
|
604
|
+
| { readonly kind: 'options'; readonly options: TopoOptions }
|
|
605
|
+
| { readonly kind: 'module' }
|
|
606
|
+
| { readonly kind: 'invalid'; readonly message: string } => {
|
|
607
|
+
if (typeof value !== 'object' || value === null) {
|
|
608
|
+
return { kind: 'module' };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (hasOptionsBrand(value)) {
|
|
612
|
+
const unknown = detectUnknownOptionKeys(value);
|
|
613
|
+
if (unknown.length > 0) {
|
|
614
|
+
return {
|
|
615
|
+
kind: 'invalid',
|
|
616
|
+
message: `topo.options() received unknown option keys: ${unknown
|
|
617
|
+
.map((key) => `"${key}"`)
|
|
618
|
+
.join(', ')}. Expected one of: ${TOPO_OPTION_KEYS.map(
|
|
619
|
+
(key) => `"${key}"`
|
|
620
|
+
).join(', ')}.`,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
// Branding via `topo.options()` is the documented escape hatch for
|
|
624
|
+
// disambiguating bare-sink shorthand. Route a bare `LogSink` into the
|
|
625
|
+
// explicit `{ log: sink }` slot before handing off to `normalizeObserve`,
|
|
626
|
+
// which would otherwise reject it as ambiguous (a LogSink shape matches
|
|
627
|
+
// both `isLogSink` and `isTraceSink`). A bare TraceSink (no `name`) does
|
|
628
|
+
// not need rewriting because `normalizeObserve` already routes it via
|
|
629
|
+
// the `isTraceSink` fallthrough.
|
|
630
|
+
return {
|
|
631
|
+
kind: 'options',
|
|
632
|
+
options: disambiguateBrandedObserve(value as TopoOptions),
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (!looksLikeTopoOptionsShape(value)) {
|
|
637
|
+
return { kind: 'module' };
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// The shape matches `TopoOptions`. Decide whether the values are
|
|
641
|
+
// valid options, a registrable module export, or a non-registrable
|
|
642
|
+
// helper that should be treated as a (silently ignored) module
|
|
643
|
+
// export.
|
|
644
|
+
const observeValue = (value as TopoOptions).observe;
|
|
645
|
+
const layersValue = (value as TopoOptions).layers;
|
|
646
|
+
|
|
647
|
+
// A module export named `layers` would be a non-registrable array, but
|
|
648
|
+
// an explicit `topo.options({ layers: [...] })` is the documented escape
|
|
649
|
+
// hatch for that disambiguation. Without the brand, accept the trailing
|
|
650
|
+
// argument as options when `layers` is an array and `observe` is either
|
|
651
|
+
// unset or already an unambiguous option shape — this mirrors how
|
|
652
|
+
// `observe`-only options are accepted today.
|
|
653
|
+
if (Array.isArray(layersValue) && observeValue === undefined) {
|
|
654
|
+
return { kind: 'options', options: value as TopoOptions };
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
if (hasRegistrableKind(observeValue)) {
|
|
658
|
+
return { kind: 'module' };
|
|
659
|
+
}
|
|
660
|
+
// Unambiguous option shapes: `Logger`, `ObserveConfig`, and
|
|
661
|
+
// `ObserveCapable` carry enough structure that they cannot be
|
|
662
|
+
// confused with a generic module export. Check these first so the
|
|
663
|
+
// sink-ambiguity guard below does not accidentally reject an
|
|
664
|
+
// `ObserveCapable` whose underlying shape happens to also satisfy
|
|
665
|
+
// `isLogSink` / `isTraceSink`.
|
|
666
|
+
if (
|
|
667
|
+
isLogger(observeValue) ||
|
|
668
|
+
isObserveConfig(observeValue) ||
|
|
669
|
+
hasObserveCapabilities(observeValue)
|
|
670
|
+
) {
|
|
671
|
+
return { kind: 'options', options: value as TopoOptions };
|
|
672
|
+
}
|
|
673
|
+
// Bare sink shapes (`{ write }` / `{ name, write }`) are genuinely
|
|
674
|
+
// ambiguous — they may equally plausibly be a module export named
|
|
675
|
+
// `observe`. Refuse to guess; require `topo.options()` to make the
|
|
676
|
+
// intent explicit.
|
|
677
|
+
if (isLogSink(observeValue) || isTraceSink(observeValue)) {
|
|
678
|
+
return {
|
|
679
|
+
kind: 'invalid',
|
|
680
|
+
message:
|
|
681
|
+
'topo() received a trailing argument shaped like `{ observe: sink }` that is ambiguous: ' +
|
|
682
|
+
'the value matches both a TopoOptions sink and a non-registrable module export. ' +
|
|
683
|
+
'Wrap the options with `topo.options({ observe: sink })` to disambiguate.',
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
if (isObserveInput(observeValue)) {
|
|
687
|
+
// Catch-all for any future `ObserveInput` variant added to the
|
|
688
|
+
// type. Today this branch is unreachable given the guards above.
|
|
689
|
+
return { kind: 'options', options: value as TopoOptions };
|
|
690
|
+
}
|
|
691
|
+
// Non-registrable, non-sink helper. Treat as a module export; the
|
|
692
|
+
// unrecognized value is silently ignored during registration, matching
|
|
693
|
+
// the behavior of any other non-registrable export.
|
|
694
|
+
return { kind: 'module' };
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
const splitTopoArguments = (
|
|
698
|
+
modulesOrOptions: readonly (Record<string, unknown> | TopoOptions)[]
|
|
699
|
+
): {
|
|
700
|
+
readonly modules: readonly Record<string, unknown>[];
|
|
701
|
+
readonly options: TopoOptions | undefined;
|
|
702
|
+
} => {
|
|
703
|
+
// A branded `topo.options(...)` payload is an explicit user signal and must
|
|
704
|
+
// appear last. If it shows up in a non-trailing position the caller almost
|
|
705
|
+
// certainly intended it as options but lost the configuration silently.
|
|
706
|
+
// Reject it so the misconfiguration is visible at construction.
|
|
707
|
+
for (let i = 0; i < modulesOrOptions.length - 1; i += 1) {
|
|
708
|
+
const arg = modulesOrOptions[i];
|
|
709
|
+
if (typeof arg === 'object' && arg !== null && hasOptionsBrand(arg)) {
|
|
710
|
+
throw new ValidationError(
|
|
711
|
+
`topo.options(...) must be the final argument to topo(); received at position ${i + 2} of ${modulesOrOptions.length + 1}.`
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const last = modulesOrOptions.at(-1);
|
|
717
|
+
const classification = classifyTrailingArgument(last);
|
|
718
|
+
|
|
719
|
+
if (classification.kind === 'invalid') {
|
|
720
|
+
throw new ValidationError(classification.message);
|
|
721
|
+
}
|
|
722
|
+
if (classification.kind === 'options') {
|
|
723
|
+
return {
|
|
724
|
+
modules: modulesOrOptions.slice(0, -1) as Record<string, unknown>[],
|
|
725
|
+
options: classification.options,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return {
|
|
729
|
+
modules: modulesOrOptions as readonly Record<string, unknown>[],
|
|
730
|
+
options: undefined,
|
|
731
|
+
};
|
|
732
|
+
};
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Brand a plain `TopoOptions` payload so `topo()` treats the trailing
|
|
736
|
+
* argument as options unambiguously, regardless of which keys it
|
|
737
|
+
* contains. Useful when a module export shape would otherwise collide
|
|
738
|
+
* with the inline shorthand (e.g. a module exporting only `observe`).
|
|
739
|
+
*
|
|
740
|
+
* @example
|
|
741
|
+
* ```ts
|
|
742
|
+
* topo('app', userTrails, topo.options({ observe: traceSink }));
|
|
743
|
+
* ```
|
|
744
|
+
*/
|
|
745
|
+
const describeNonPlainObject = (value: unknown): string => {
|
|
746
|
+
if (value === null) {
|
|
747
|
+
return 'null';
|
|
748
|
+
}
|
|
749
|
+
if (Array.isArray(value)) {
|
|
750
|
+
return 'array';
|
|
751
|
+
}
|
|
752
|
+
return typeof value;
|
|
753
|
+
};
|
|
754
|
+
|
|
755
|
+
const brandTopoOptions = (options: TopoOptions): TopoOptions => {
|
|
756
|
+
// Reject non-plain-object inputs up front. `{ ...options }` happily
|
|
757
|
+
// accepts `null`, `undefined`, primitives, and arrays, silently
|
|
758
|
+
// producing an empty branded payload that callers would then assume
|
|
759
|
+
// carried real options. Throwing here mirrors the strict handling
|
|
760
|
+
// applied to other malformed options elsewhere in the classifier.
|
|
761
|
+
if (
|
|
762
|
+
typeof options !== 'object' ||
|
|
763
|
+
options === null ||
|
|
764
|
+
Array.isArray(options)
|
|
765
|
+
) {
|
|
766
|
+
throw new ValidationError(
|
|
767
|
+
`topo.options() expects a plain options object; received ${describeNonPlainObject(
|
|
768
|
+
options
|
|
769
|
+
)}`
|
|
770
|
+
);
|
|
771
|
+
}
|
|
772
|
+
// Return a fresh object rather than mutating the caller's payload.
|
|
773
|
+
// Mutating in place breaks frozen / non-extensible inputs (for example
|
|
774
|
+
// `topo.options(Object.freeze({ observe: sink }))`), which would throw
|
|
775
|
+
// a `TypeError` from `Object.defineProperty` even though the value is
|
|
776
|
+
// a valid `TopoOptions` shape.
|
|
777
|
+
const branded = { ...options };
|
|
778
|
+
Object.defineProperty(branded, TOPO_OPTIONS_BRAND, {
|
|
779
|
+
configurable: false,
|
|
780
|
+
enumerable: false,
|
|
781
|
+
value: true,
|
|
782
|
+
writable: false,
|
|
783
|
+
});
|
|
784
|
+
return branded;
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
interface TopoFn {
|
|
788
|
+
(
|
|
789
|
+
nameOrIdentity: string | TopoIdentity,
|
|
790
|
+
...modulesOrOptions: (Record<string, unknown> | TopoOptions)[]
|
|
791
|
+
): Topo;
|
|
792
|
+
/**
|
|
793
|
+
* Brand a plain `TopoOptions` payload so `topo()` treats the trailing
|
|
794
|
+
* argument as options unambiguously, regardless of key shape. Use this
|
|
795
|
+
* when a module export shape might otherwise collide with the inline
|
|
796
|
+
* options shorthand.
|
|
797
|
+
*/
|
|
798
|
+
readonly options: (options: TopoOptions) => TopoOptions;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const topoImpl = (
|
|
431
802
|
nameOrIdentity: string | TopoIdentity,
|
|
432
|
-
...
|
|
803
|
+
...modulesOrOptions: (Record<string, unknown> | TopoOptions)[]
|
|
433
804
|
): Topo => {
|
|
434
805
|
const identity: TopoIdentity =
|
|
435
806
|
typeof nameOrIdentity === 'string'
|
|
436
807
|
? { name: nameOrIdentity }
|
|
437
808
|
: nameOrIdentity;
|
|
809
|
+
const { modules, options } = splitTopoArguments(modulesOrOptions);
|
|
810
|
+
const observe = normalizeObserve(options?.observe);
|
|
811
|
+
const layers = Object.freeze([...(options?.layers ?? [])]);
|
|
438
812
|
|
|
439
813
|
const contours = new Map<string, AnyContour>();
|
|
440
814
|
const trails = new Map<string, AnyTrail>();
|
|
@@ -450,6 +824,12 @@ export const topo = (
|
|
|
450
824
|
contours,
|
|
451
825
|
finalizeTrailSignals(trails, resources),
|
|
452
826
|
signals,
|
|
453
|
-
resources
|
|
827
|
+
resources,
|
|
828
|
+
observe,
|
|
829
|
+
layers
|
|
454
830
|
);
|
|
455
831
|
};
|
|
832
|
+
|
|
833
|
+
export const topo: TopoFn = Object.assign(topoImpl, {
|
|
834
|
+
options: brandTopoOptions,
|
|
835
|
+
});
|