@ontrails/core 1.0.0-beta.13 → 1.0.0-beta.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +17 -0
- package/README.md +19 -14
- package/dist/branded.d.ts +1 -1
- package/dist/branded.d.ts.map +1 -1
- package/dist/branded.js +1 -1
- package/dist/branded.js.map +1 -1
- package/dist/context.d.ts +13 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +19 -3
- package/dist/context.js.map +1 -1
- package/dist/contour.d.ts +81 -0
- package/dist/contour.d.ts.map +1 -0
- package/dist/contour.js +176 -0
- package/dist/contour.js.map +1 -0
- package/dist/cross-schema.d.ts +18 -0
- package/dist/cross-schema.d.ts.map +1 -0
- package/dist/cross-schema.js +29 -0
- package/dist/cross-schema.js.map +1 -0
- package/dist/derive.d.ts +6 -0
- package/dist/derive.d.ts.map +1 -1
- package/dist/derive.js +29 -5
- package/dist/derive.js.map +1 -1
- package/dist/draft.d.ts +28 -0
- package/dist/draft.d.ts.map +1 -0
- package/dist/draft.js +157 -0
- package/dist/draft.js.map +1 -0
- package/dist/errors.d.ts +62 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +26 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +441 -20
- package/dist/execute.js.map +1 -1
- package/dist/fire.d.ts +43 -0
- package/dist/fire.d.ts.map +1 -0
- package/dist/fire.js +185 -0
- package/dist/fire.js.map +1 -0
- package/dist/index.d.ts +33 -17
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +26 -12
- package/dist/index.js.map +1 -1
- package/dist/internal/cross-batch.d.ts +44 -0
- package/dist/internal/cross-batch.d.ts.map +1 -0
- package/dist/internal/cross-batch.js +58 -0
- package/dist/internal/cross-batch.js.map +1 -0
- package/dist/internal/fork-ctx.d.ts +41 -0
- package/dist/internal/fork-ctx.d.ts.map +1 -0
- package/dist/internal/fork-ctx.js +47 -0
- package/dist/internal/fork-ctx.js.map +1 -0
- package/dist/internal/signal-ref.d.ts +15 -0
- package/dist/internal/signal-ref.d.ts.map +1 -0
- package/dist/internal/signal-ref.js +41 -0
- package/dist/internal/signal-ref.js.map +1 -0
- package/dist/internal/topo-saves.d.ts +53 -0
- package/dist/internal/topo-saves.d.ts.map +1 -0
- package/dist/internal/topo-saves.js +449 -0
- package/dist/internal/topo-saves.js.map +1 -0
- package/dist/internal/topo-snapshots.d.ts +57 -0
- package/dist/internal/topo-snapshots.d.ts.map +1 -0
- package/dist/internal/topo-snapshots.js +372 -0
- package/dist/internal/topo-snapshots.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +70 -0
- package/dist/internal/topo-store-read.d.ts.map +1 -0
- package/dist/internal/topo-store-read.js +221 -0
- package/dist/internal/topo-store-read.js.map +1 -0
- package/dist/internal/topo-store.d.ts +26 -0
- package/dist/internal/topo-store.d.ts.map +1 -0
- package/dist/internal/topo-store.js +621 -0
- package/dist/internal/topo-store.js.map +1 -0
- package/dist/internal/tracing.d.ts +98 -0
- package/dist/internal/tracing.d.ts.map +1 -0
- package/dist/internal/tracing.js +102 -0
- package/dist/internal/tracing.js.map +1 -0
- package/dist/internal/trails-db.d.ts +16 -0
- package/dist/internal/trails-db.d.ts.map +1 -0
- package/dist/internal/trails-db.js +118 -0
- package/dist/internal/trails-db.js.map +1 -0
- package/dist/internal/zod-wrappers.d.ts +40 -0
- package/dist/internal/zod-wrappers.d.ts.map +1 -0
- package/dist/internal/zod-wrappers.js +59 -0
- package/dist/internal/zod-wrappers.js.map +1 -0
- package/dist/layer.d.ts +22 -5
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +20 -3
- package/dist/layer.js.map +1 -1
- package/dist/path-security.d.ts +1 -1
- package/dist/path-security.d.ts.map +1 -1
- package/dist/path-security.js +1 -1
- package/dist/path-security.js.map +1 -1
- package/dist/resilience.d.ts +1 -1
- package/dist/resilience.d.ts.map +1 -1
- package/dist/resilience.js +3 -3
- package/dist/resilience.js.map +1 -1
- package/dist/resource-config.d.ts +22 -0
- package/dist/resource-config.d.ts.map +1 -0
- package/dist/resource-config.js +210 -0
- package/dist/resource-config.js.map +1 -0
- package/dist/resource.d.ts +74 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +61 -0
- package/dist/resource.js.map +1 -0
- package/dist/run.d.ts +4 -3
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +5 -4
- package/dist/run.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +2 -1
- package/dist/serialization.js.map +1 -1
- package/dist/store/accessor-protocol.d.ts +51 -0
- package/dist/store/accessor-protocol.d.ts.map +1 -0
- package/dist/store/accessor-protocol.js +2 -0
- package/dist/store/accessor-protocol.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/surface-filter.d.ts +13 -0
- package/dist/surface-filter.d.ts.map +1 -0
- package/dist/surface-filter.js +76 -0
- package/dist/surface-filter.js.map +1 -0
- package/dist/topo-store.d.ts +62 -0
- package/dist/topo-store.d.ts.map +1 -0
- package/dist/topo-store.js +319 -0
- package/dist/topo-store.js.map +1 -0
- package/dist/topo.d.ts +23 -9
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +175 -46
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +86 -19
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +59 -4
- package/dist/trail.js.map +1 -1
- package/dist/trails/derive-trail.d.ts +66 -0
- package/dist/trails/derive-trail.d.ts.map +1 -0
- package/dist/trails/derive-trail.js +378 -0
- package/dist/trails/derive-trail.js.map +1 -0
- package/dist/trails/index.d.ts +5 -0
- package/dist/trails/index.d.ts.map +1 -0
- package/dist/trails/index.js +3 -0
- package/dist/trails/index.js.map +1 -0
- package/dist/trails/ingest.d.ts +24 -0
- package/dist/trails/ingest.d.ts.map +1 -0
- package/dist/trails/ingest.js +63 -0
- package/dist/trails/ingest.js.map +1 -0
- package/dist/transport-error-map.d.ts +106 -0
- package/dist/transport-error-map.d.ts.map +1 -0
- package/dist/transport-error-map.js +24 -0
- package/dist/transport-error-map.js.map +1 -0
- package/dist/type-checks.test-d.d.ts +71 -0
- package/dist/type-checks.test-d.d.ts.map +1 -0
- package/dist/type-checks.test-d.js +12 -0
- package/dist/type-checks.test-d.js.map +1 -0
- package/dist/type-utils.d.ts +11 -2
- package/dist/type-utils.d.ts.map +1 -1
- package/dist/type-utils.js.map +1 -1
- package/dist/types.d.ts +113 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate-established-topo.d.ts +76 -0
- package/dist/validate-established-topo.d.ts.map +1 -0
- package/dist/validate-established-topo.js +43 -0
- package/dist/validate-established-topo.js.map +1 -0
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +27 -8
- package/dist/validate-topo.js.map +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +10 -1
- package/src/__tests__/branded.test.ts +5 -5
- package/src/__tests__/context.test.ts +3 -3
- package/src/__tests__/contour.test.ts +263 -0
- package/src/__tests__/derive-trail.test.ts +668 -0
- package/src/__tests__/derive.test.ts +58 -1
- package/src/__tests__/errors.test.ts +118 -0
- package/src/__tests__/execute.test.ts +1179 -141
- package/src/__tests__/fire.test.ts +1056 -0
- package/src/__tests__/fork-ctx.test.ts +83 -0
- package/src/__tests__/ingest.test.ts +191 -0
- package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
- package/src/__tests__/path-security.test.ts +7 -7
- package/src/__tests__/resilience.test.ts +8 -8
- package/src/__tests__/resource.test.ts +204 -0
- package/src/__tests__/run.test.ts +13 -13
- package/src/__tests__/service-config.test.ts +28 -32
- package/src/__tests__/surface-filter.test.ts +196 -0
- package/src/__tests__/topo-store-read.test.ts +256 -0
- package/src/__tests__/topo-store.test.ts +869 -0
- package/src/__tests__/topo.test.ts +350 -54
- package/src/__tests__/trail.test.ts +189 -24
- package/src/__tests__/trails-db.test.ts +198 -0
- package/src/__tests__/transport-error-map.test.ts +82 -0
- package/src/__tests__/type-utils.test.ts +29 -1
- package/src/__tests__/validate-topo.test.ts +305 -16
- package/src/__tests__/workspace.test.ts +6 -6
- package/src/__tests__/zod-wrappers.test.ts +62 -0
- package/src/branded.ts +1 -1
- package/src/context.ts +24 -4
- package/src/contour.ts +344 -0
- package/src/cross-schema.ts +36 -0
- package/src/derive.ts +39 -8
- package/src/draft.ts +338 -0
- package/src/errors.ts +62 -18
- package/src/execute.ts +840 -29
- package/src/fire.ts +295 -0
- package/src/index.ts +142 -26
- package/src/internal/cross-batch.ts +77 -0
- package/src/internal/fork-ctx.ts +69 -0
- package/src/internal/signal-ref.ts +87 -0
- package/src/internal/topo-snapshots.ts +517 -0
- package/src/internal/topo-store-read.ts +481 -0
- package/src/internal/topo-store.ts +1192 -0
- package/src/internal/tracing.ts +187 -0
- package/src/internal/trails-db.ts +187 -0
- package/src/internal/zod-wrappers.ts +78 -0
- package/src/layer.ts +50 -0
- package/src/path-security.ts +1 -1
- package/src/resilience.ts +3 -3
- package/src/{provision-config.ts → resource-config.ts} +109 -123
- package/src/resource.ts +155 -0
- package/src/run.ts +5 -4
- package/src/serialization.ts +2 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/surface-filter.ts +182 -0
- package/src/topo-store.ts +510 -0
- package/src/topo.ts +328 -57
- package/src/trail.ts +199 -31
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +146 -0
- package/src/transport-error-map.ts +56 -0
- package/src/type-checks.test-d.ts +106 -0
- package/src/type-utils.ts +17 -2
- package/src/types.ts +148 -11
- package/src/validate-established-topo.ts +63 -0
- package/src/validate-topo.ts +37 -9
- package/src/workspace.ts +1 -1
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -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/dispatch.d.ts +0 -27
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/dispatch.js +0 -34
- package/dist/dispatch.js.map +0 -1
- package/dist/event.d.ts +0 -8
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -7
- package/dist/event.js.map +0 -1
- package/dist/gate.d.ts +0 -17
- package/dist/gate.d.ts.map +0 -1
- package/dist/gate.js +0 -21
- package/dist/gate.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/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/provision-config.d.ts +0 -22
- package/dist/provision-config.d.ts.map +0 -1
- package/dist/provision-config.js +0 -210
- package/dist/provision-config.js.map +0 -1
- package/dist/provision.d.ts +0 -71
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -56
- package/dist/provision.js.map +0 -1
- package/dist/service-config.d.ts +0 -22
- package/dist/service-config.d.ts.map +0 -1
- package/dist/service-config.js +0 -210
- package/dist/service-config.js.map +0 -1
- package/dist/service.d.ts +0 -71
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -56
- package/dist/service.js.map +0 -1
- package/src/__tests__/service.test.ts +0 -197
- package/src/event.ts +0 -15
- package/src/gate.ts +0 -44
- package/src/provision.ts +0 -148
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intrinsic tracing primitives.
|
|
3
|
+
*
|
|
4
|
+
* This module is the home for the trace record type, the sink interface,
|
|
5
|
+
* the sink registry, and the helpers `executeTrail` uses to create root
|
|
6
|
+
* trace records and child spans. It is intentionally framework-internal —
|
|
7
|
+
* the `@ontrails/tracing` package re-exports the public surface so callers
|
|
8
|
+
* can continue to import from there.
|
|
9
|
+
*
|
|
10
|
+
* Tracing is intrinsic: every `executeTrail` call automatically produces a
|
|
11
|
+
* root `TraceRecord`, and `ctx.trace(label, fn)` creates nested child spans
|
|
12
|
+
* underneath it. A default no-op sink is installed at module load so core
|
|
13
|
+
* never crashes when no real sink has been registered.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Evidence of a single trail execution or manual span. */
|
|
17
|
+
export interface TraceRecord {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly traceId: string;
|
|
20
|
+
readonly rootId: string;
|
|
21
|
+
readonly parentId?: string | undefined;
|
|
22
|
+
readonly kind: 'trail' | 'span';
|
|
23
|
+
readonly name: string;
|
|
24
|
+
readonly trailId?: string | undefined;
|
|
25
|
+
readonly trailhead?: 'cli' | 'mcp' | 'http' | 'ws' | undefined;
|
|
26
|
+
readonly intent?: 'read' | 'write' | 'destroy' | undefined;
|
|
27
|
+
readonly startedAt: number;
|
|
28
|
+
readonly endedAt?: number | undefined;
|
|
29
|
+
readonly status: 'ok' | 'err' | 'cancelled';
|
|
30
|
+
readonly errorCategory?: string | undefined;
|
|
31
|
+
readonly permit?:
|
|
32
|
+
| { readonly id: string; readonly tenantId?: string }
|
|
33
|
+
| undefined;
|
|
34
|
+
readonly attrs: Readonly<Record<string, unknown>>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Minimal shape a tracing sink must satisfy.
|
|
39
|
+
*
|
|
40
|
+
* Kept intentionally tiny so adapters in `@ontrails/tracing`,
|
|
41
|
+
* `@ontrails/observe`, and user code can all satisfy it without
|
|
42
|
+
* additional dependencies.
|
|
43
|
+
*/
|
|
44
|
+
export interface TraceSink {
|
|
45
|
+
readonly write: (record: TraceRecord) => void | Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Trace context carried through trail execution via `ctx.extensions`. */
|
|
49
|
+
export interface TraceContext {
|
|
50
|
+
readonly traceId: string;
|
|
51
|
+
readonly spanId: string;
|
|
52
|
+
readonly rootId: string;
|
|
53
|
+
readonly sampled: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Key used to store trace context in `ctx.extensions`. */
|
|
57
|
+
export const TRACE_CONTEXT_KEY = '__trace_context';
|
|
58
|
+
|
|
59
|
+
/** Read trace context from trail context extensions. */
|
|
60
|
+
export const getTraceContext = (ctx: {
|
|
61
|
+
readonly extensions?: Readonly<Record<string, unknown>> | undefined;
|
|
62
|
+
}): TraceContext | undefined =>
|
|
63
|
+
ctx.extensions?.[TRACE_CONTEXT_KEY] as TraceContext | undefined;
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Default no-op sink + sink registry
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/** No-op sink installed by default so core never crashes without configuration. */
|
|
70
|
+
export const NOOP_SINK: TraceSink = {
|
|
71
|
+
// oxlint-disable-next-line no-empty-function -- intentional no-op
|
|
72
|
+
write: () => {},
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// oxlint-disable-next-line eslint-plugin-jest/require-hook -- module-level sink registry, not test setup
|
|
76
|
+
let currentSink: TraceSink = NOOP_SINK;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register a trace sink globally.
|
|
80
|
+
*
|
|
81
|
+
* All trails executed via `executeTrail` will write their completed trace
|
|
82
|
+
* records to this sink, as will every `ctx.trace(label, fn)` child span.
|
|
83
|
+
* Registering `undefined` or calling {@link clearTraceSink} resets back to
|
|
84
|
+
* the default no-op sink.
|
|
85
|
+
*/
|
|
86
|
+
export const registerTraceSink = (sink: TraceSink | undefined): void => {
|
|
87
|
+
currentSink = sink ?? NOOP_SINK;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Retrieve the currently registered sink (never undefined). */
|
|
91
|
+
export const getTraceSink = (): TraceSink => currentSink;
|
|
92
|
+
|
|
93
|
+
/** True when tracing is effectively disabled and executeTrail should skip allocation. */
|
|
94
|
+
export const isTracingDisabled = (sink: TraceSink = currentSink): boolean =>
|
|
95
|
+
sink === NOOP_SINK;
|
|
96
|
+
|
|
97
|
+
/** Reset the sink registry back to the default no-op sink. */
|
|
98
|
+
export const clearTraceSink = (): void => {
|
|
99
|
+
currentSink = NOOP_SINK;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// Record + span helpers
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
|
|
106
|
+
/** Options for creating a trail-scoped {@link TraceRecord}. */
|
|
107
|
+
interface CreateTraceRecordOptions {
|
|
108
|
+
readonly trailId: string;
|
|
109
|
+
readonly traceId?: string | undefined;
|
|
110
|
+
readonly parentId?: string | undefined;
|
|
111
|
+
readonly rootId?: string | undefined;
|
|
112
|
+
readonly trailhead?: TraceRecord['trailhead'];
|
|
113
|
+
readonly intent?: TraceRecord['intent'];
|
|
114
|
+
readonly permit?:
|
|
115
|
+
| { readonly id: string; readonly tenantId?: string }
|
|
116
|
+
| undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Create a fresh trail-kind {@link TraceRecord}. */
|
|
120
|
+
export const createTraceRecord = (
|
|
121
|
+
options: CreateTraceRecordOptions
|
|
122
|
+
): TraceRecord => {
|
|
123
|
+
const id = Bun.randomUUIDv7();
|
|
124
|
+
const traceId = options.traceId ?? Bun.randomUUIDv7();
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
attrs: {},
|
|
128
|
+
endedAt: undefined,
|
|
129
|
+
id,
|
|
130
|
+
intent: options.intent,
|
|
131
|
+
kind: 'trail',
|
|
132
|
+
name: options.trailId,
|
|
133
|
+
parentId: options.parentId,
|
|
134
|
+
permit: options.permit,
|
|
135
|
+
rootId: options.rootId ?? id,
|
|
136
|
+
startedAt: Date.now(),
|
|
137
|
+
status: 'ok',
|
|
138
|
+
traceId,
|
|
139
|
+
trailId: options.trailId,
|
|
140
|
+
trailhead: options.trailhead,
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/** Build a span record from a parent trace context. */
|
|
145
|
+
export const createSpanRecord = (
|
|
146
|
+
parent: TraceContext,
|
|
147
|
+
label: string
|
|
148
|
+
): TraceRecord => ({
|
|
149
|
+
attrs: {},
|
|
150
|
+
endedAt: undefined,
|
|
151
|
+
errorCategory: undefined,
|
|
152
|
+
id: Bun.randomUUIDv7(),
|
|
153
|
+
intent: undefined,
|
|
154
|
+
kind: 'span',
|
|
155
|
+
name: label,
|
|
156
|
+
parentId: parent.spanId,
|
|
157
|
+
rootId: parent.rootId,
|
|
158
|
+
startedAt: Date.now(),
|
|
159
|
+
status: 'ok',
|
|
160
|
+
traceId: parent.traceId,
|
|
161
|
+
trailId: undefined,
|
|
162
|
+
trailhead: undefined,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/** Mark a record as completed with timing and status. */
|
|
166
|
+
export const completeRecord = (
|
|
167
|
+
record: TraceRecord,
|
|
168
|
+
status: TraceRecord['status'],
|
|
169
|
+
errorCategory?: string | undefined
|
|
170
|
+
): TraceRecord => ({
|
|
171
|
+
...record,
|
|
172
|
+
endedAt: Date.now(),
|
|
173
|
+
errorCategory,
|
|
174
|
+
status,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
/** Best-effort sink write that never throws. */
|
|
178
|
+
export const writeToSink = async (
|
|
179
|
+
sink: TraceSink,
|
|
180
|
+
record: TraceRecord
|
|
181
|
+
): Promise<void> => {
|
|
182
|
+
try {
|
|
183
|
+
await Promise.resolve(sink.write(record));
|
|
184
|
+
} catch {
|
|
185
|
+
// Sink failures must never affect trail result delivery.
|
|
186
|
+
}
|
|
187
|
+
};
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { NotFoundError } from '../errors.js';
|
|
6
|
+
|
|
7
|
+
const TRAILS_DIR = '.trails';
|
|
8
|
+
const TRAILS_DB_FILE = 'trails.db';
|
|
9
|
+
const SCHEMA_VERSION_TABLE = 'meta_schema_versions';
|
|
10
|
+
const WORKSPACE_SUBDIRS = ['config', 'dev', 'generated'] as const;
|
|
11
|
+
const REQUIRED_GITIGNORE_LINES = [
|
|
12
|
+
'# Local config overrides',
|
|
13
|
+
'config/',
|
|
14
|
+
'',
|
|
15
|
+
'# Development state',
|
|
16
|
+
'dev/',
|
|
17
|
+
'',
|
|
18
|
+
'# Generated artifacts',
|
|
19
|
+
'generated/',
|
|
20
|
+
'',
|
|
21
|
+
'# Shared Trails database',
|
|
22
|
+
'trails.db',
|
|
23
|
+
'trails.db-shm',
|
|
24
|
+
'trails.db-wal',
|
|
25
|
+
'',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export interface TrailsDbLocationOptions {
|
|
29
|
+
readonly path?: string;
|
|
30
|
+
readonly rootDir?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EnsureSubsystemSchemaOptions {
|
|
34
|
+
readonly migrate: (currentVersion: number) => void;
|
|
35
|
+
readonly subsystem: string;
|
|
36
|
+
readonly version: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface SchemaVersionRow {
|
|
40
|
+
readonly version: number;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const deriveRootDir = (rootDir?: string): string =>
|
|
44
|
+
resolve(rootDir ?? process.cwd());
|
|
45
|
+
|
|
46
|
+
export const deriveTrailsDir = (options?: TrailsDbLocationOptions): string =>
|
|
47
|
+
join(deriveRootDir(options?.rootDir), TRAILS_DIR);
|
|
48
|
+
|
|
49
|
+
export const deriveTrailsDbPath = (options?: TrailsDbLocationOptions): string =>
|
|
50
|
+
options?.path
|
|
51
|
+
? resolve(options.path)
|
|
52
|
+
: join(deriveTrailsDir(options), TRAILS_DB_FILE);
|
|
53
|
+
|
|
54
|
+
const ensureDbParentDir = (dbPath: string): void => {
|
|
55
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const GITIGNORE_TEMPLATE = `${REQUIRED_GITIGNORE_LINES.join('\n').trimEnd()}\n`;
|
|
59
|
+
|
|
60
|
+
const appendMissingGitignoreLines = (
|
|
61
|
+
gitignorePath: string,
|
|
62
|
+
content: string
|
|
63
|
+
): void => {
|
|
64
|
+
const existingLines = new Set(content.split('\n').map((l) => l.trim()));
|
|
65
|
+
const missing = REQUIRED_GITIGNORE_LINES.filter(
|
|
66
|
+
(line) => line !== '' && !existingLines.has(line)
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (missing.length === 0) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const next = `${content.trimEnd()}\n\n${missing.join('\n')}`;
|
|
74
|
+
writeFileSync(gitignorePath, `${next.trimEnd()}\n`);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const ensureWorkspaceGitignore = (trailsDir: string): void => {
|
|
78
|
+
const gitignorePath = join(trailsDir, '.gitignore');
|
|
79
|
+
|
|
80
|
+
if (!existsSync(gitignorePath)) {
|
|
81
|
+
writeFileSync(gitignorePath, GITIGNORE_TEMPLATE);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
appendMissingGitignoreLines(
|
|
86
|
+
gitignorePath,
|
|
87
|
+
readFileSync(gitignorePath, 'utf8')
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const ensureWorkspaceDir = (rootDir: string): void => {
|
|
92
|
+
const trailsDir = deriveTrailsDir({ rootDir });
|
|
93
|
+
mkdirSync(trailsDir, { recursive: true });
|
|
94
|
+
for (const subdir of WORKSPACE_SUBDIRS) {
|
|
95
|
+
mkdirSync(join(trailsDir, subdir), { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
ensureWorkspaceGitignore(trailsDir);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const initializeWritePragmas = (db: Database): void => {
|
|
101
|
+
db.run('PRAGMA journal_mode = WAL');
|
|
102
|
+
db.run('PRAGMA synchronous = NORMAL');
|
|
103
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const ensureSchemaVersionTable = (db: Database): void => {
|
|
107
|
+
db.run(`CREATE TABLE IF NOT EXISTS ${SCHEMA_VERSION_TABLE} (
|
|
108
|
+
subsystem TEXT PRIMARY KEY,
|
|
109
|
+
version INTEGER NOT NULL,
|
|
110
|
+
updated_at TEXT NOT NULL
|
|
111
|
+
)`);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const readSubsystemVersion = (db: Database, subsystem: string): number => {
|
|
115
|
+
const row = db
|
|
116
|
+
.query<SchemaVersionRow, [string]>(
|
|
117
|
+
`SELECT version FROM ${SCHEMA_VERSION_TABLE} WHERE subsystem = ?`
|
|
118
|
+
)
|
|
119
|
+
.get(subsystem);
|
|
120
|
+
return row?.version ?? 0;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const writeSubsystemVersion = (
|
|
124
|
+
db: Database,
|
|
125
|
+
subsystem: string,
|
|
126
|
+
version: number
|
|
127
|
+
): void => {
|
|
128
|
+
db.run(
|
|
129
|
+
`INSERT INTO ${SCHEMA_VERSION_TABLE} (subsystem, version, updated_at)
|
|
130
|
+
VALUES (?, ?, ?)
|
|
131
|
+
ON CONFLICT(subsystem) DO UPDATE SET
|
|
132
|
+
version = excluded.version,
|
|
133
|
+
updated_at = excluded.updated_at`,
|
|
134
|
+
[subsystem, version, new Date().toISOString()]
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const openWriteTrailsDb = (
|
|
139
|
+
options?: TrailsDbLocationOptions
|
|
140
|
+
): Database => {
|
|
141
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
142
|
+
const dbPath = deriveTrailsDbPath(
|
|
143
|
+
options?.path ? { path: options.path, rootDir } : { rootDir }
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
if (options?.path === undefined) {
|
|
147
|
+
ensureWorkspaceDir(rootDir);
|
|
148
|
+
} else {
|
|
149
|
+
ensureDbParentDir(dbPath);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const db = new Database(dbPath, { create: true });
|
|
153
|
+
initializeWritePragmas(db);
|
|
154
|
+
ensureSchemaVersionTable(db);
|
|
155
|
+
return db;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export const openReadTrailsDb = (
|
|
159
|
+
options?: TrailsDbLocationOptions
|
|
160
|
+
): Database => {
|
|
161
|
+
const dbPath = deriveTrailsDbPath(options);
|
|
162
|
+
if (!existsSync(dbPath)) {
|
|
163
|
+
throw new NotFoundError(
|
|
164
|
+
`Trails database not found at "${dbPath}". Run a write operation first to initialize it.`
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const db = new Database(dbPath, { readonly: true });
|
|
168
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
169
|
+
return db;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const ensureSubsystemSchema = (
|
|
173
|
+
db: Database,
|
|
174
|
+
options: EnsureSubsystemSchemaOptions
|
|
175
|
+
): void => {
|
|
176
|
+
ensureSchemaVersionTable(db);
|
|
177
|
+
|
|
178
|
+
db.transaction(() => {
|
|
179
|
+
const currentVersion = readSubsystemVersion(db, options.subsystem);
|
|
180
|
+
if (currentVersion >= options.version) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
options.migrate(currentVersion);
|
|
185
|
+
writeSubsystemVersion(db, options.subsystem, options.version);
|
|
186
|
+
})();
|
|
187
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared Zod wrapper helpers.
|
|
3
|
+
*
|
|
4
|
+
* These helpers walk the wrapper layers of a Zod schema (default, optional,
|
|
5
|
+
* nullable) and rebuild the schema without any default wrappers so that
|
|
6
|
+
* downstream `.partial()` calls do not silently re-materialize defaults during
|
|
7
|
+
* validation. Both `@ontrails/core/trails/derive-trail.ts` and
|
|
8
|
+
* `@ontrails/store/src/store.ts` previously shipped near-identical copies of
|
|
9
|
+
* this walk — extracting them here keeps the wrapper semantics authored in one
|
|
10
|
+
* place so the two call sites cannot drift.
|
|
11
|
+
*
|
|
12
|
+
* Canonical behavior: `optional` wrappers are intentionally dropped. Every
|
|
13
|
+
* current call site applies `.partial()` to the stripped schema, which
|
|
14
|
+
* re-introduces `optional` across the whole shape. Preserving `optional` here
|
|
15
|
+
* would be redundant at best and would produce an `OptionalOptional<T>` shape
|
|
16
|
+
* in edge cases at worst. `nullable` wrappers are preserved because nullability
|
|
17
|
+
* is a semantic constraint that `.partial()` does not reintroduce.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { z } from 'zod';
|
|
23
|
+
|
|
24
|
+
const isWrapperType = (
|
|
25
|
+
type: string
|
|
26
|
+
): type is 'default' | 'nullable' | 'optional' =>
|
|
27
|
+
type === 'default' || type === 'nullable' || type === 'optional';
|
|
28
|
+
|
|
29
|
+
const readInnerType = (schema: z.ZodType): z.ZodType =>
|
|
30
|
+
(schema.def as unknown as { innerType: z.ZodType }).innerType;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Strip `default` wrappers from a Zod type so that partial update schemas do
|
|
34
|
+
* not silently re-materialize defaults. Walks through all wrapper layers
|
|
35
|
+
* (`default`, `optional`, `nullable`), drops defaults, drops `optional` (the
|
|
36
|
+
* downstream `.partial()` reintroduces it across the whole shape), and
|
|
37
|
+
* preserves `nullable` so explicit nullability survives.
|
|
38
|
+
*
|
|
39
|
+
* @internal
|
|
40
|
+
*/
|
|
41
|
+
export const stripDefaultWrappers = (schema: z.ZodType): z.ZodType => {
|
|
42
|
+
let current = schema;
|
|
43
|
+
let isNullable = false;
|
|
44
|
+
|
|
45
|
+
while (isWrapperType(current.def.type)) {
|
|
46
|
+
if (current.def.type === 'nullable') {
|
|
47
|
+
isNullable = true;
|
|
48
|
+
}
|
|
49
|
+
current = readInnerType(current);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return isNullable ? current.nullable() : current;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type AnyObjectSchema = z.ZodObject<Record<string, z.ZodType>>;
|
|
56
|
+
|
|
57
|
+
const asObjectSchema = (schema: z.ZodType): AnyObjectSchema =>
|
|
58
|
+
schema as unknown as AnyObjectSchema;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Apply {@link stripDefaultWrappers} to every field in a Zod object shape and
|
|
62
|
+
* return the resulting shape record. Callers typically feed the result to
|
|
63
|
+
* `.extend()` + `.partial()`.
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
export const stripDefaultsFromShape = (
|
|
68
|
+
schema: z.ZodType
|
|
69
|
+
): Record<string, z.ZodType> => {
|
|
70
|
+
const stripped: Record<string, z.ZodType> = {};
|
|
71
|
+
const objectSchema = asObjectSchema(schema);
|
|
72
|
+
|
|
73
|
+
for (const [field, value] of Object.entries(objectSchema.shape)) {
|
|
74
|
+
stripped[field] = stripDefaultWrappers(value);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return stripped;
|
|
78
|
+
};
|
package/src/layer.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { AnyTrail } from './trail.js';
|
|
2
|
+
import type { Implementation } from './types.js';
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Layer interface
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/** A composable layer that wraps trail implementations. */
|
|
9
|
+
export interface Layer {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly description?: string | undefined;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wrap a trail's implementation, returning a new implementation.
|
|
15
|
+
*
|
|
16
|
+
* The trail is passed for metadata inspection (intent, schema, etc.).
|
|
17
|
+
* The implementation and return type are generic over the input/output
|
|
18
|
+
* types so layers remain type-safe when composed.
|
|
19
|
+
*/
|
|
20
|
+
wrap<I, O>(
|
|
21
|
+
trail: AnyTrail,
|
|
22
|
+
implementation: Implementation<I, O>
|
|
23
|
+
): Implementation<I, O>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Composition
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Apply layers outermost-first: layers[0] wraps layers[1] wraps ... wraps
|
|
32
|
+
* the base implementation.
|
|
33
|
+
*
|
|
34
|
+
* An empty layers array returns the implementation unchanged.
|
|
35
|
+
*/
|
|
36
|
+
export const composeLayers = <I, O>(
|
|
37
|
+
layers: readonly Layer[],
|
|
38
|
+
trail: AnyTrail,
|
|
39
|
+
implementation: Implementation<I, O>
|
|
40
|
+
): Implementation<I, O> => {
|
|
41
|
+
// Fold right so layers[0] is the outermost wrapper.
|
|
42
|
+
let result = implementation;
|
|
43
|
+
for (let i = layers.length - 1; i >= 0; i -= 1) {
|
|
44
|
+
const layer = layers[i];
|
|
45
|
+
if (layer) {
|
|
46
|
+
result = layer.wrap(trail, result);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
};
|
package/src/path-security.ts
CHANGED
|
@@ -70,7 +70,7 @@ export const isPathSafe = (basePath: string, userPath: string): boolean => {
|
|
|
70
70
|
* Joins multiple path segments, resolves them against `basePath`, and
|
|
71
71
|
* validates the result stays within the base directory.
|
|
72
72
|
*/
|
|
73
|
-
export const
|
|
73
|
+
export const deriveSafePath = (
|
|
74
74
|
basePath: string,
|
|
75
75
|
...segments: string[]
|
|
76
76
|
): Result<string, PermissionError> => {
|
package/src/resilience.ts
CHANGED
|
@@ -61,11 +61,11 @@ const sleep = (ms: number, signal?: AbortSignal): Promise<void> =>
|
|
|
61
61
|
export const shouldRetry = (error: Error): boolean => isRetryable(error);
|
|
62
62
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
|
-
//
|
|
64
|
+
// deriveBackoffDelay
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
|
|
67
67
|
/** Compute exponential backoff delay with full jitter. */
|
|
68
|
-
export const
|
|
68
|
+
export const deriveBackoffDelay = (
|
|
69
69
|
attempt: number,
|
|
70
70
|
options?: Pick<RetryOptions, 'baseDelay' | 'maxDelay' | 'backoffFactor'>
|
|
71
71
|
): number => {
|
|
@@ -108,7 +108,7 @@ const tryAttempt = async <T>(
|
|
|
108
108
|
if (isLast || !retryPredicate(result.error)) {
|
|
109
109
|
return { done: true, result };
|
|
110
110
|
}
|
|
111
|
-
const delay =
|
|
111
|
+
const delay = deriveBackoffDelay(attempt, options);
|
|
112
112
|
if (delay > 0) {
|
|
113
113
|
await sleep(delay, options?.signal);
|
|
114
114
|
}
|