@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
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { deriveTrail } from './derive-trail.js';
|
|
2
|
+
export type {
|
|
3
|
+
DeriveTrailInput,
|
|
4
|
+
DeriveTrailOperation,
|
|
5
|
+
DeriveTrailOutput,
|
|
6
|
+
DeriveTrailSpec,
|
|
7
|
+
} from './derive-trail.js';
|
|
8
|
+
export { ingest } from './ingest.js';
|
|
9
|
+
export type { IngestOptions, IngestTransform } from './ingest.js';
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
import { InternalError } from '../errors.js';
|
|
4
|
+
import { composeLayers } from '../layer.js';
|
|
5
|
+
import type { Layer } from '../layer.js';
|
|
6
|
+
import { Result } from '../result.js';
|
|
7
|
+
import type { Signal } from '../signal.js';
|
|
8
|
+
import { trail } from '../trail.js';
|
|
9
|
+
import type { Trail, TrailExample, TrailSpec } from '../trail.js';
|
|
10
|
+
import type { TrailContext } from '../types.js';
|
|
11
|
+
|
|
12
|
+
type SchemaValue<TSchema extends z.ZodType> = z.output<TSchema>;
|
|
13
|
+
|
|
14
|
+
type ExampleBearingSchema<TSchema extends z.ZodType> = TSchema & {
|
|
15
|
+
readonly examples?: readonly Partial<SchemaValue<TSchema>>[] | undefined;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface IngestBaseOptions<TSchema extends z.ZodType, TSignal> extends Omit<
|
|
19
|
+
TrailSpec<SchemaValue<TSchema>, void>,
|
|
20
|
+
'blaze' | 'examples' | 'fires' | 'input' | 'intent' | 'output' | 'pattern'
|
|
21
|
+
> {
|
|
22
|
+
/** Override the derived trail id. Defaults to `${signal}.ingest`. */
|
|
23
|
+
readonly id?: string | undefined;
|
|
24
|
+
/** Validated external payload shape. */
|
|
25
|
+
readonly schema: TSchema;
|
|
26
|
+
/** Signal to fire after verification and optional transformation. */
|
|
27
|
+
readonly signal: Signal<TSignal>;
|
|
28
|
+
/** Optional per-trail verification layer, e.g. HMAC signature checks. */
|
|
29
|
+
readonly verify?: Layer | undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type IngestTransform<TInput, TSignal> = (
|
|
33
|
+
payload: TInput,
|
|
34
|
+
ctx: TrailContext
|
|
35
|
+
) => TSignal | Promise<TSignal>;
|
|
36
|
+
|
|
37
|
+
export interface IngestOptions<
|
|
38
|
+
TSchema extends z.ZodType,
|
|
39
|
+
TSignal,
|
|
40
|
+
> extends IngestBaseOptions<TSchema, TSignal> {
|
|
41
|
+
readonly transform?:
|
|
42
|
+
| IngestTransform<SchemaValue<TSchema>, TSignal>
|
|
43
|
+
| undefined;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const deriveExampleName = (signalId: string, index: number): string =>
|
|
47
|
+
`Ingest ${signalId} ${index + 1}`;
|
|
48
|
+
|
|
49
|
+
const deriveExamples = <TSchema extends z.ZodType>(
|
|
50
|
+
schema: ExampleBearingSchema<TSchema>,
|
|
51
|
+
signalId: string
|
|
52
|
+
): readonly TrailExample<SchemaValue<TSchema>, void>[] | undefined => {
|
|
53
|
+
const { examples } = schema;
|
|
54
|
+
if (examples === undefined || examples.length === 0) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return Object.freeze(
|
|
59
|
+
examples.map((example, index) => ({
|
|
60
|
+
input: example,
|
|
61
|
+
name: deriveExampleName(signalId, index),
|
|
62
|
+
}))
|
|
63
|
+
);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const createIngestBlaze =
|
|
67
|
+
<TSchema extends z.ZodType, TSignal>(
|
|
68
|
+
signalRef: Signal<TSignal>,
|
|
69
|
+
signalId: string,
|
|
70
|
+
trailId: string,
|
|
71
|
+
transform: IngestTransform<SchemaValue<TSchema>, TSignal> | undefined
|
|
72
|
+
) =>
|
|
73
|
+
async (
|
|
74
|
+
input: SchemaValue<TSchema>,
|
|
75
|
+
ctx: TrailContext
|
|
76
|
+
): Promise<Result<void, Error>> => {
|
|
77
|
+
if (ctx.fire === undefined) {
|
|
78
|
+
return Result.err(
|
|
79
|
+
new InternalError(
|
|
80
|
+
`ingest("${trailId}") requires topo-backed execution to fire "${signalId}"`
|
|
81
|
+
)
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const payload =
|
|
87
|
+
transform === undefined
|
|
88
|
+
? (input as TSignal)
|
|
89
|
+
: await transform(input, ctx);
|
|
90
|
+
await ctx.fire(signalRef, payload);
|
|
91
|
+
return Result.ok();
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const message = `ingest("${trailId}"): ${error instanceof Error ? error.message : String(error)}`;
|
|
94
|
+
return Result.err(
|
|
95
|
+
error instanceof Error
|
|
96
|
+
? new InternalError(message, { cause: error })
|
|
97
|
+
: new InternalError(message)
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const ingest = <
|
|
103
|
+
TSchema extends z.ZodType,
|
|
104
|
+
TSignal = SchemaValue<TSchema>,
|
|
105
|
+
>(
|
|
106
|
+
options: IngestOptions<TSchema, TSignal>
|
|
107
|
+
): Trail<SchemaValue<TSchema>, void> => {
|
|
108
|
+
const signalId = options.signal.id;
|
|
109
|
+
const id = options.id ?? `${signalId}.ingest`;
|
|
110
|
+
const { id: _id, schema, signal, transform, verify, ...trailSpec } = options;
|
|
111
|
+
const baseBlaze = createIngestBlaze<TSchema, TSignal>(
|
|
112
|
+
signal,
|
|
113
|
+
signalId,
|
|
114
|
+
id,
|
|
115
|
+
transform
|
|
116
|
+
);
|
|
117
|
+
const baseSpec = {
|
|
118
|
+
...trailSpec,
|
|
119
|
+
blaze: baseBlaze as TrailSpec<unknown, unknown>['blaze'],
|
|
120
|
+
examples: deriveExamples(schema as ExampleBearingSchema<TSchema>, signalId),
|
|
121
|
+
fires: [signal],
|
|
122
|
+
input: schema as z.ZodType<unknown>,
|
|
123
|
+
intent: 'write',
|
|
124
|
+
output: z.void(),
|
|
125
|
+
pattern: 'ingest',
|
|
126
|
+
} as TrailSpec<unknown, unknown>;
|
|
127
|
+
const baseTrail = trail(id, baseSpec) as Trail<SchemaValue<TSchema>, void>;
|
|
128
|
+
|
|
129
|
+
if (verify === undefined) {
|
|
130
|
+
return baseTrail;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Verification is a per-factory concern, so compose it locally instead of
|
|
134
|
+
// mutating runner-wide layer configuration.
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
...baseTrail,
|
|
137
|
+
blaze: composeLayers([verify], baseTrail, baseTrail.blaze),
|
|
138
|
+
}) as Trail<SchemaValue<TSchema>, void>;
|
|
139
|
+
};
|
package/src/trails-db.ts
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
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 TRAILS_CACHE_DIR = 'cache';
|
|
10
|
+
const TRAILS_STATE_DIR = 'state';
|
|
11
|
+
const SCHEMA_VERSION_TABLE = 'meta_schema_versions';
|
|
12
|
+
const WORKSPACE_SUBDIRS = [TRAILS_CACHE_DIR, TRAILS_STATE_DIR] as const;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The canonical lines written to a freshly-bootstrapped
|
|
16
|
+
* `.trails/.gitignore`. Kept as the source of truth for every consumer that
|
|
17
|
+
* needs to either write the file (scaffold) or audit its content (tests).
|
|
18
|
+
*
|
|
19
|
+
* @see {@link WORKSPACE_GITIGNORE_CONTENT} for the rendered string form.
|
|
20
|
+
*/
|
|
21
|
+
export const WORKSPACE_GITIGNORE_LINES = [
|
|
22
|
+
'# Local config overrides',
|
|
23
|
+
'config.local.js',
|
|
24
|
+
'config.local.ts',
|
|
25
|
+
'',
|
|
26
|
+
'# Rebuildable cache',
|
|
27
|
+
'cache/',
|
|
28
|
+
'',
|
|
29
|
+
'# Mutable runtime state',
|
|
30
|
+
'state/',
|
|
31
|
+
'',
|
|
32
|
+
] as const;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The canonical rendered `.trails/.gitignore` content. Use this when writing
|
|
36
|
+
* the file eagerly (e.g. during `trails create` scaffolding) or when asserting
|
|
37
|
+
* on the workspace bootstrap output.
|
|
38
|
+
*/
|
|
39
|
+
export const WORKSPACE_GITIGNORE_CONTENT = `${WORKSPACE_GITIGNORE_LINES.join('\n').trimEnd()}\n`;
|
|
40
|
+
|
|
41
|
+
export interface TrailsDbLocationOptions {
|
|
42
|
+
readonly path?: string;
|
|
43
|
+
readonly rootDir?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface EnsureSubsystemSchemaOptions {
|
|
47
|
+
readonly migrate: (currentVersion: number) => void;
|
|
48
|
+
readonly subsystem: string;
|
|
49
|
+
readonly version: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface SchemaVersionRow {
|
|
53
|
+
readonly version: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const deriveRootDir = (rootDir?: string): string =>
|
|
57
|
+
resolve(rootDir ?? process.cwd());
|
|
58
|
+
|
|
59
|
+
export const deriveTrailsDir = (options?: TrailsDbLocationOptions): string =>
|
|
60
|
+
join(deriveRootDir(options?.rootDir), TRAILS_DIR);
|
|
61
|
+
|
|
62
|
+
export const deriveTrailsDbPath = (options?: TrailsDbLocationOptions): string =>
|
|
63
|
+
options?.path
|
|
64
|
+
? resolve(options.path)
|
|
65
|
+
: join(deriveTrailsDir(options), TRAILS_STATE_DIR, TRAILS_DB_FILE);
|
|
66
|
+
|
|
67
|
+
const ensureDbParentDir = (dbPath: string): void => {
|
|
68
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const appendMissingGitignoreLines = (
|
|
72
|
+
gitignorePath: string,
|
|
73
|
+
content: string
|
|
74
|
+
): void => {
|
|
75
|
+
const existingLines = new Set(content.split('\n').map((l) => l.trim()));
|
|
76
|
+
const missing = WORKSPACE_GITIGNORE_LINES.filter(
|
|
77
|
+
(line) => line !== '' && !existingLines.has(line)
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
if (missing.length === 0) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const next = `${content.trimEnd()}\n\n${missing.join('\n')}`;
|
|
85
|
+
writeFileSync(gitignorePath, `${next.trimEnd()}\n`);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const ensureWorkspaceGitignore = (trailsDir: string): void => {
|
|
89
|
+
const gitignorePath = join(trailsDir, '.gitignore');
|
|
90
|
+
|
|
91
|
+
if (!existsSync(gitignorePath)) {
|
|
92
|
+
writeFileSync(gitignorePath, WORKSPACE_GITIGNORE_CONTENT);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
appendMissingGitignoreLines(
|
|
97
|
+
gitignorePath,
|
|
98
|
+
readFileSync(gitignorePath, 'utf8')
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Bootstrap the `.trails/` workspace at `rootDir`.
|
|
104
|
+
*
|
|
105
|
+
* Creates the workspace directory plus the canonical `cache/` and `state/`
|
|
106
|
+
* subdirectories, then either writes a fresh `.gitignore` matching
|
|
107
|
+
* {@link WORKSPACE_GITIGNORE_CONTENT} or appends any missing canonical lines
|
|
108
|
+
* to an existing one. Safe to call repeatedly. This is the single canonical
|
|
109
|
+
* source of truth for workspace layout — scaffolding, configuration loading,
|
|
110
|
+
* and runtime DB initialization all flow through here.
|
|
111
|
+
*/
|
|
112
|
+
export const ensureTrailsWorkspace = (rootDir: string): void => {
|
|
113
|
+
const trailsDir = deriveTrailsDir({ rootDir });
|
|
114
|
+
mkdirSync(trailsDir, { recursive: true });
|
|
115
|
+
for (const subdir of WORKSPACE_SUBDIRS) {
|
|
116
|
+
mkdirSync(join(trailsDir, subdir), { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
ensureWorkspaceGitignore(trailsDir);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const initializeWritePragmas = (db: Database): void => {
|
|
122
|
+
db.run('PRAGMA journal_mode = WAL');
|
|
123
|
+
db.run('PRAGMA synchronous = NORMAL');
|
|
124
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const ensureSchemaVersionTable = (db: Database): void => {
|
|
128
|
+
db.run(`CREATE TABLE IF NOT EXISTS ${SCHEMA_VERSION_TABLE} (
|
|
129
|
+
subsystem TEXT PRIMARY KEY,
|
|
130
|
+
version INTEGER NOT NULL,
|
|
131
|
+
updated_at TEXT NOT NULL
|
|
132
|
+
)`);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const readSubsystemVersion = (db: Database, subsystem: string): number => {
|
|
136
|
+
const row = db
|
|
137
|
+
.query<SchemaVersionRow, [string]>(
|
|
138
|
+
`SELECT version FROM ${SCHEMA_VERSION_TABLE} WHERE subsystem = ?`
|
|
139
|
+
)
|
|
140
|
+
.get(subsystem);
|
|
141
|
+
return row?.version ?? 0;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const writeSubsystemVersion = (
|
|
145
|
+
db: Database,
|
|
146
|
+
subsystem: string,
|
|
147
|
+
version: number
|
|
148
|
+
): void => {
|
|
149
|
+
db.run(
|
|
150
|
+
`INSERT INTO ${SCHEMA_VERSION_TABLE} (subsystem, version, updated_at)
|
|
151
|
+
VALUES (?, ?, ?)
|
|
152
|
+
ON CONFLICT(subsystem) DO UPDATE SET
|
|
153
|
+
version = excluded.version,
|
|
154
|
+
updated_at = excluded.updated_at`,
|
|
155
|
+
[subsystem, version, new Date().toISOString()]
|
|
156
|
+
);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const openWriteTrailsDb = (
|
|
160
|
+
options?: TrailsDbLocationOptions
|
|
161
|
+
): Database => {
|
|
162
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
163
|
+
const dbPath = deriveTrailsDbPath(
|
|
164
|
+
options?.path ? { path: options.path, rootDir } : { rootDir }
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (options?.path === undefined) {
|
|
168
|
+
ensureTrailsWorkspace(rootDir);
|
|
169
|
+
} else {
|
|
170
|
+
ensureDbParentDir(dbPath);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const db = new Database(dbPath, { create: true });
|
|
174
|
+
initializeWritePragmas(db);
|
|
175
|
+
ensureSchemaVersionTable(db);
|
|
176
|
+
return db;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
export const openReadTrailsDb = (
|
|
180
|
+
options?: TrailsDbLocationOptions
|
|
181
|
+
): Database => {
|
|
182
|
+
const dbPath = deriveTrailsDbPath(options);
|
|
183
|
+
if (!existsSync(dbPath)) {
|
|
184
|
+
throw new NotFoundError(
|
|
185
|
+
`Trails database not found at "${dbPath}". Run a write operation first to initialize it.`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
const db = new Database(dbPath, { readonly: true });
|
|
189
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
190
|
+
return db;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export const ensureSubsystemSchema = (
|
|
194
|
+
db: Database,
|
|
195
|
+
options: EnsureSubsystemSchemaOptions
|
|
196
|
+
): void => {
|
|
197
|
+
ensureSchemaVersionTable(db);
|
|
198
|
+
|
|
199
|
+
db.transaction(() => {
|
|
200
|
+
const currentVersion = readSubsystemVersion(db, options.subsystem);
|
|
201
|
+
if (currentVersion >= options.version) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
options.migrate(currentVersion);
|
|
206
|
+
writeSubsystemVersion(db, options.subsystem, options.version);
|
|
207
|
+
})();
|
|
208
|
+
};
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ErrorCategory,
|
|
3
|
+
ErrorCategoryCodes,
|
|
4
|
+
ErrorClassRegistryEntry,
|
|
5
|
+
FixedErrorClassRegistryEntry,
|
|
6
|
+
TrailsError,
|
|
7
|
+
} from './errors.js';
|
|
8
|
+
import {
|
|
9
|
+
codesByCategory,
|
|
10
|
+
errorClasses,
|
|
11
|
+
exitCodeMap,
|
|
12
|
+
isTrailsError,
|
|
13
|
+
jsonRpcCodeMap,
|
|
14
|
+
statusCodeMap,
|
|
15
|
+
} from './errors.js';
|
|
16
|
+
import {
|
|
17
|
+
INTERNAL_ERROR_PUBLIC_MESSAGE,
|
|
18
|
+
redactErrorString,
|
|
19
|
+
} from './error-projection.js';
|
|
20
|
+
|
|
21
|
+
export const surfaceNames = ['cli', 'http', 'jsonRpc', 'mcp'] as const;
|
|
22
|
+
|
|
23
|
+
export type SurfaceName = (typeof surfaceNames)[number];
|
|
24
|
+
|
|
25
|
+
const surfaceCodeKeys = {
|
|
26
|
+
cli: 'exit',
|
|
27
|
+
http: 'http',
|
|
28
|
+
jsonRpc: 'jsonRpc',
|
|
29
|
+
mcp: 'jsonRpc',
|
|
30
|
+
} as const satisfies Record<SurfaceName, keyof ErrorCategoryCodes>;
|
|
31
|
+
|
|
32
|
+
export type SurfaceErrorMapper<T> = (error: TrailsError) => T;
|
|
33
|
+
|
|
34
|
+
export type SurfaceErrorMappings<T> = Record<ErrorCategory, T>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Union of every surface-specific error code emitted by {@link surfaceErrorMap}.
|
|
38
|
+
*
|
|
39
|
+
* @remarks
|
|
40
|
+
* Previously parameterized by surface (`SurfaceErrorCode<'cli'>` etc.), but
|
|
41
|
+
* the generic collapsed to `number` because the underlying maps were typed as
|
|
42
|
+
* `Record<ErrorCategory, number>`. With `as const satisfies` on the maps the
|
|
43
|
+
* per-surface literals are now observable, but TypeScript cannot narrow
|
|
44
|
+
* `surfaceErrorMap[surface][error.category]` through a generic `TSurface`
|
|
45
|
+
* without an unsound cast. The non-generic union honestly reflects what
|
|
46
|
+
* `mapSurfaceError` returns at the call site.
|
|
47
|
+
*/
|
|
48
|
+
export type SurfaceErrorCode =
|
|
49
|
+
(typeof codesByCategory)[ErrorCategory][(typeof surfaceCodeKeys)[SurfaceName]];
|
|
50
|
+
|
|
51
|
+
export interface SurfaceErrorProjection {
|
|
52
|
+
readonly category: ErrorCategory;
|
|
53
|
+
readonly code: SurfaceErrorCode;
|
|
54
|
+
readonly message: string;
|
|
55
|
+
readonly name: string;
|
|
56
|
+
readonly retryable: boolean;
|
|
57
|
+
readonly surface: SurfaceName;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface ErrorClassSurfaceProjection {
|
|
61
|
+
readonly category: ErrorCategory;
|
|
62
|
+
readonly code: SurfaceErrorCode;
|
|
63
|
+
readonly name: string;
|
|
64
|
+
readonly retryable: boolean;
|
|
65
|
+
readonly surface: SurfaceName;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const createSurfaceErrorMapper =
|
|
69
|
+
<T>(mappings: SurfaceErrorMappings<T>): SurfaceErrorMapper<T> =>
|
|
70
|
+
(error) =>
|
|
71
|
+
mappings[error.category];
|
|
72
|
+
|
|
73
|
+
export const surfaceErrorMap = {
|
|
74
|
+
cli: exitCodeMap,
|
|
75
|
+
http: statusCodeMap,
|
|
76
|
+
jsonRpc: jsonRpcCodeMap,
|
|
77
|
+
mcp: jsonRpcCodeMap,
|
|
78
|
+
} as const satisfies Record<SurfaceName, SurfaceErrorMappings<number>>;
|
|
79
|
+
|
|
80
|
+
export const surfaceErrorRegistry = {
|
|
81
|
+
cli: {
|
|
82
|
+
map: createSurfaceErrorMapper(surfaceErrorMap.cli),
|
|
83
|
+
values: surfaceErrorMap.cli,
|
|
84
|
+
},
|
|
85
|
+
http: {
|
|
86
|
+
map: createSurfaceErrorMapper(surfaceErrorMap.http),
|
|
87
|
+
values: surfaceErrorMap.http,
|
|
88
|
+
},
|
|
89
|
+
jsonRpc: {
|
|
90
|
+
map: createSurfaceErrorMapper(surfaceErrorMap.jsonRpc),
|
|
91
|
+
values: surfaceErrorMap.jsonRpc,
|
|
92
|
+
},
|
|
93
|
+
mcp: {
|
|
94
|
+
map: createSurfaceErrorMapper(surfaceErrorMap.mcp),
|
|
95
|
+
values: surfaceErrorMap.mcp,
|
|
96
|
+
},
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
export const mapSurfaceError = (
|
|
100
|
+
surface: SurfaceName,
|
|
101
|
+
error: TrailsError
|
|
102
|
+
): SurfaceErrorCode =>
|
|
103
|
+
codesByCategory[error.category][surfaceCodeKeys[surface]];
|
|
104
|
+
|
|
105
|
+
export const projectSurfaceError = (
|
|
106
|
+
surface: SurfaceName,
|
|
107
|
+
error: TrailsError
|
|
108
|
+
): SurfaceErrorProjection => ({
|
|
109
|
+
category: error.category,
|
|
110
|
+
code: mapSurfaceError(surface, error),
|
|
111
|
+
message: error.message,
|
|
112
|
+
name: error.name,
|
|
113
|
+
retryable: error.retryable,
|
|
114
|
+
surface,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export const projectPublicSurfaceError = (
|
|
118
|
+
surface: SurfaceName,
|
|
119
|
+
error: Error
|
|
120
|
+
): SurfaceErrorProjection => {
|
|
121
|
+
if (isTrailsError(error)) {
|
|
122
|
+
const projection = projectSurfaceError(surface, error);
|
|
123
|
+
return {
|
|
124
|
+
...projection,
|
|
125
|
+
message:
|
|
126
|
+
projection.category === 'internal'
|
|
127
|
+
? INTERNAL_ERROR_PUBLIC_MESSAGE
|
|
128
|
+
: redactErrorString(projection.message),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
category: 'internal',
|
|
134
|
+
code: codesByCategory.internal[surfaceCodeKeys[surface]],
|
|
135
|
+
message: INTERNAL_ERROR_PUBLIC_MESSAGE,
|
|
136
|
+
name: 'InternalError',
|
|
137
|
+
retryable: false,
|
|
138
|
+
surface,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const isFixedErrorClassEntry = (
|
|
143
|
+
entry: ErrorClassRegistryEntry
|
|
144
|
+
): entry is FixedErrorClassRegistryEntry => entry.category !== 'dynamic';
|
|
145
|
+
|
|
146
|
+
const fixedErrorClassByName: ReadonlyMap<string, FixedErrorClassRegistryEntry> =
|
|
147
|
+
new Map(
|
|
148
|
+
errorClasses.flatMap((entry): [string, FixedErrorClassRegistryEntry][] =>
|
|
149
|
+
isFixedErrorClassEntry(entry) ? [[entry.name, entry]] : []
|
|
150
|
+
)
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Project a known error class name onto a surface without constructing it.
|
|
155
|
+
*
|
|
156
|
+
* Dynamic-category errors such as `RetryExhaustedError` return `undefined`
|
|
157
|
+
* because their surface code depends on the wrapped runtime error.
|
|
158
|
+
*/
|
|
159
|
+
export const projectErrorClassSurface = (
|
|
160
|
+
surface: SurfaceName,
|
|
161
|
+
errorName: string
|
|
162
|
+
): ErrorClassSurfaceProjection | undefined => {
|
|
163
|
+
const entry = fixedErrorClassByName.get(errorName);
|
|
164
|
+
if (entry === undefined) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
category: entry.category,
|
|
169
|
+
code: codesByCategory[entry.category][surfaceCodeKeys[surface]],
|
|
170
|
+
name: entry.name,
|
|
171
|
+
retryable: entry.retryable,
|
|
172
|
+
surface,
|
|
173
|
+
};
|
|
174
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type utilities for extracting input/output types from trails.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Result } from './result.js';
|
|
6
|
+
import type { AnyTrail, Trail } from './trail.js';
|
|
7
|
+
import type { Implementation } from './types.js';
|
|
8
|
+
import type { z } from 'zod';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Utility types
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/* oxlint-disable no-explicit-any -- `any` required for conditional type inference; `unknown` breaks inference */
|
|
15
|
+
|
|
16
|
+
type SchemaInputOrFallback<TSchema extends z.ZodType, TFallback> =
|
|
17
|
+
unknown extends z.input<TSchema> ? TFallback : z.input<TSchema>;
|
|
18
|
+
|
|
19
|
+
type ComposeSchemaOf<T extends AnyTrail> = T extends {
|
|
20
|
+
readonly composeInput?: (infer TComposeSchema) | undefined;
|
|
21
|
+
}
|
|
22
|
+
? NonNullable<TComposeSchema>
|
|
23
|
+
: never;
|
|
24
|
+
|
|
25
|
+
type ComposeInputPart<T extends AnyTrail> =
|
|
26
|
+
ComposeSchemaOf<T> extends z.ZodType
|
|
27
|
+
? T extends Trail<any, any, infer CI>
|
|
28
|
+
? SchemaInputOrFallback<ComposeSchemaOf<T>, CI>
|
|
29
|
+
: z.input<ComposeSchemaOf<T>>
|
|
30
|
+
: T extends Trail<any, any, infer CI>
|
|
31
|
+
? CI
|
|
32
|
+
: never;
|
|
33
|
+
|
|
34
|
+
/** Extract the input type from a Trail. */
|
|
35
|
+
export type TrailInput<T extends AnyTrail> = T extends {
|
|
36
|
+
readonly input: infer TInputSchema extends z.ZodType;
|
|
37
|
+
}
|
|
38
|
+
? T extends Trail<infer I, any, any>
|
|
39
|
+
? SchemaInputOrFallback<TInputSchema, I>
|
|
40
|
+
: z.input<TInputSchema>
|
|
41
|
+
: never;
|
|
42
|
+
|
|
43
|
+
/** Extract the output type from a Trail. */
|
|
44
|
+
export type TrailOutput<T extends AnyTrail> = T extends {
|
|
45
|
+
readonly blaze: Implementation<any, infer O>;
|
|
46
|
+
}
|
|
47
|
+
? O
|
|
48
|
+
: never;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Extract the compose-callable input type from a trail.
|
|
52
|
+
*
|
|
53
|
+
* When a trail declares `composeInput`, callers via `ctx.compose()` must pass
|
|
54
|
+
* both the public input fields and the composition-only fields. This type
|
|
55
|
+
* merges both schemas so the compiler enforces the full shape at the call
|
|
56
|
+
* site. Falls back to plain `TrailInput<T>` when no `composeInput` exists.
|
|
57
|
+
*/
|
|
58
|
+
export type ComposeInput<T extends AnyTrail> = [ComposeInputPart<T>] extends [
|
|
59
|
+
never,
|
|
60
|
+
]
|
|
61
|
+
? TrailInput<T>
|
|
62
|
+
: TrailInput<T> & ComposeInputPart<T>;
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Extracts the full `Result<Output, Error>` type from a trail definition.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```typescript
|
|
69
|
+
* type SearchResult = TrailResult<typeof searchTrail>;
|
|
70
|
+
* // Result<{ results: Item[]; count: number }, Error>
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export type TrailResult<T extends AnyTrail> = Result<TrailOutput<T>, Error>;
|
|
74
|
+
|
|
75
|
+
/* oxlint-enable no-explicit-any */
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Runtime schema accessors
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
/** Get the input Zod schema from a trail, preserving the specific schema type. */
|
|
82
|
+
export const inputOf = <T extends AnyTrail>(trail: T): T['input'] =>
|
|
83
|
+
trail.input;
|
|
84
|
+
|
|
85
|
+
/** Get the output Zod schema from a trail, if defined, preserving the specific schema type. */
|
|
86
|
+
export const outputOf = <T extends AnyTrail>(trail: T): T['output'] =>
|
|
87
|
+
trail.output;
|