@ontrails/core 1.0.0-beta.23 → 1.0.0-beta.29
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 +29 -0
- package/README.md +3 -2
- package/package.json +1 -1
- package/src/derive.ts +194 -0
- package/src/errors.ts +26 -0
- package/src/execute.ts +6 -6
- package/src/fire.ts +1 -1
- package/src/index.ts +22 -2
- package/src/resource-config.ts +1 -1
- package/src/schedule-runtime.ts +1 -1
- package/src/serialization.ts +2 -0
- package/src/trail.ts +7 -1
- package/src/trails-db.ts +74 -76
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# @ontrails/core
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.29
|
|
4
|
+
|
|
5
|
+
## 1.0.0-beta.28
|
|
6
|
+
|
|
7
|
+
## 1.0.0-beta.27
|
|
8
|
+
|
|
9
|
+
## 1.0.0-beta.26
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 1307568: Centralize Trails config module path conventions, move local config overrides to root `trails.config.local.*`, scaffold the matching gitignore entries, and load project-local Warden rules from `.trails/rules.ts` or `.trails/rules/`.
|
|
14
|
+
- 371d19e: Move the default `trails.db` location to the per-user Trails state store, expose deterministic state-store path helpers, stop scaffolding disposable `.trails/cache` and `.trails/state` directories, and update topo-store documentation for the global-state substrate.
|
|
15
|
+
|
|
16
|
+
## 1.0.0-beta.25
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- c36aca9: Preserve existing Result error boundaries directly and widen Warden pass-through
|
|
21
|
+
coaching beyond trail blazes.
|
|
22
|
+
- 3befcf1: Configure Trails SQLite read and write connections with a busy timeout so concurrent artifact readers and writers wait through transient lock contention instead of failing immediately.
|
|
23
|
+
- a4f9cf6: Reserve the `shift` error category and `WorkspaceShiftError` before the stable
|
|
24
|
+
cutover so surface mappings can distinguish moved-workspace retry verdicts.
|
|
25
|
+
Update Warden's error-mapping completeness examples to cover the reserved
|
|
26
|
+
category.
|
|
27
|
+
- 9bcf34e: Add trail-owned CLI command projection metadata and serialize resolved command
|
|
28
|
+
route facts for downstream tools.
|
|
29
|
+
|
|
30
|
+
## 1.0.0-beta.24
|
|
31
|
+
|
|
3
32
|
## 1.0.0-beta.23
|
|
4
33
|
|
|
5
34
|
## 1.0.0-beta.22
|
package/README.md
CHANGED
|
@@ -129,6 +129,7 @@ The current taxonomy is generated from the `errorClasses` owner registry and cat
|
|
|
129
129
|
| `timeout` | 5 | 504 | -32603 | Yes | `TimeoutError` |
|
|
130
130
|
| `rate_limit` | 6 | 429 | -32603 | Yes | `RateLimitError` |
|
|
131
131
|
| `network` | 7 | 502 | -32603 | Yes | `NetworkError` |
|
|
132
|
+
| `shift` | 10 | 503 | -32603 | Yes | `WorkspaceShiftError` |
|
|
132
133
|
| `internal` | 8 | 500 | -32603 | No | `AssertionError`, `InternalError`, `DerivationError`, `RecoverableCompletionError` |
|
|
133
134
|
| `auth` | 9 | 401 | -32600 | No | `AuthError` |
|
|
134
135
|
| `cancelled` | 130 | 499 | -32603 | No | `CancelledError` |
|
|
@@ -161,14 +162,14 @@ The developer returns `Result.err(new NotFoundError(...))`. The framework maps i
|
|
|
161
162
|
The root package also exposes a few low-level contracts that other framework packages build on:
|
|
162
163
|
|
|
163
164
|
- **Intrinsic tracing** -- `TraceRecord`, `TraceSink`, `TraceContext`, and the sink registry helpers are the core-owned execution record shape shared by `@ontrails/observe`, `@ontrails/tracing`, and adapters.
|
|
164
|
-
- **Trails DB** -- `deriveTrailsDbPath`, `deriveTrailsDir`, `ensureSubsystemSchema`, `openReadTrailsDb`, and `openWriteTrailsDb` are the generic database primitive used by framework subsystems.
|
|
165
|
+
- **Trails DB** -- `deriveTrailsDbPath`, `deriveTrailsStateDir`, `deriveTrailsStateHome`, `deriveTrailsProjectKey`, `deriveTrailsDir`, `ensureSubsystemSchema`, `openReadTrailsDb`, and `openWriteTrailsDb` are the generic database primitive used by framework subsystems.
|
|
165
166
|
- **Surface projection helpers** -- safe error projection, layer field projection, compose-batch validation, late-bound signal references, and Zod default-wrapper stripping are stable root exports for first-party surfaces, store helpers, and tests.
|
|
166
167
|
|
|
167
168
|
See the [API Reference](../../docs/api-reference.md) for the full list.
|
|
168
169
|
|
|
169
170
|
## Migration: topo-store moved to `@ontrails/topographer`
|
|
170
171
|
|
|
171
|
-
Per [ADR-0042](../../docs/adr/0042-core-topographer-boundary-doctrine.md), the topo-store public API previously exported from `@ontrails/core` now lives in `@ontrails/topographer`. Generic `trails-db` helpers (`openReadTrailsDb`, `openWriteTrailsDb`, `ensureSubsystemSchema`, `deriveTrailsDbPath`, `deriveTrailsDir`) stay in core because tracing and other subsystems share them.
|
|
172
|
+
Per [ADR-0042](../../docs/adr/0042-core-topographer-boundary-doctrine.md), the topo-store public API previously exported from `@ontrails/core` now lives in `@ontrails/topographer`. Generic `trails-db` helpers (`openReadTrailsDb`, `openWriteTrailsDb`, `ensureSubsystemSchema`, `deriveTrailsDbPath`, `deriveTrailsStateDir`, `deriveTrailsStateHome`, `deriveTrailsProjectKey`, `deriveTrailsDir`) stay in core because tracing and other subsystems share them.
|
|
172
173
|
|
|
173
174
|
Update consumer imports:
|
|
174
175
|
|
package/package.json
CHANGED
package/src/derive.ts
CHANGED
|
@@ -50,6 +50,57 @@ export interface FieldOverride {
|
|
|
50
50
|
| undefined;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
// CLI command route projection
|
|
55
|
+
// ---------------------------------------------------------------------------
|
|
56
|
+
|
|
57
|
+
/** Authored CLI command path shape. Strings are split on whitespace. */
|
|
58
|
+
export type CliCommandPathInput = string | readonly string[];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Authored CLI command alias shape.
|
|
62
|
+
*
|
|
63
|
+
* String aliases are sibling leaf aliases. Array aliases are absolute command
|
|
64
|
+
* paths.
|
|
65
|
+
*/
|
|
66
|
+
export type CliCommandAliasInput = string | readonly string[];
|
|
67
|
+
|
|
68
|
+
/** Source that produced a resolved CLI command route. */
|
|
69
|
+
export type CliCommandRouteSource = 'derived' | 'trail' | 'surface';
|
|
70
|
+
|
|
71
|
+
/** Whether a resolved CLI command route is canonical or an alias. */
|
|
72
|
+
export type CliCommandRouteKind = 'alias' | 'canonical';
|
|
73
|
+
|
|
74
|
+
/** Trail-authored CLI projection metadata. */
|
|
75
|
+
export interface TrailCliProjection {
|
|
76
|
+
readonly aliases?: readonly CliCommandAliasInput[] | undefined;
|
|
77
|
+
readonly path?: CliCommandPathInput | undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A resolved command path accepted by a CLI surface for one trail. */
|
|
81
|
+
export interface CliCommandRoute {
|
|
82
|
+
readonly kind: CliCommandRouteKind;
|
|
83
|
+
readonly path: readonly string[];
|
|
84
|
+
readonly source: CliCommandRouteSource;
|
|
85
|
+
readonly target: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Resolved CLI projection for one trail. */
|
|
89
|
+
export interface TrailCliCommandProjection {
|
|
90
|
+
readonly path: readonly string[];
|
|
91
|
+
readonly routes: readonly CliCommandRoute[];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface TrailCliProjectionInput {
|
|
95
|
+
readonly cli?: CliCommandPathInput | TrailCliProjection | undefined;
|
|
96
|
+
readonly id: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface DeriveTrailCliCommandProjectionOptions {
|
|
100
|
+
readonly aliases?: readonly CliCommandAliasInput[] | undefined;
|
|
101
|
+
readonly aliasSource?: Extract<CliCommandRouteSource, 'surface' | 'trail'>;
|
|
102
|
+
}
|
|
103
|
+
|
|
53
104
|
// ---------------------------------------------------------------------------
|
|
54
105
|
// Zod v4 internals accessor
|
|
55
106
|
// ---------------------------------------------------------------------------
|
|
@@ -214,6 +265,149 @@ export const deriveCliPath = (trailId: string): string[] => {
|
|
|
214
265
|
return segments;
|
|
215
266
|
};
|
|
216
267
|
|
|
268
|
+
const hasWhitespace = (value: string): boolean => /\s/.test(value);
|
|
269
|
+
|
|
270
|
+
const validateCliSegment = (segment: string, context: string): string => {
|
|
271
|
+
const normalized = segment.trim();
|
|
272
|
+
if (normalized.length === 0) {
|
|
273
|
+
throw new ValidationError(`${context} cannot contain empty segments`);
|
|
274
|
+
}
|
|
275
|
+
if (hasWhitespace(normalized)) {
|
|
276
|
+
throw new ValidationError(
|
|
277
|
+
`${context} segment "${segment}" cannot contain whitespace`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
return normalized;
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const splitCliPathString = (value: string, context: string): string[] => {
|
|
284
|
+
const segments = value
|
|
285
|
+
.trim()
|
|
286
|
+
.split(/\s+/)
|
|
287
|
+
.filter((segment) => segment.length > 0);
|
|
288
|
+
if (segments.length === 0) {
|
|
289
|
+
throw new ValidationError(`${context} cannot be empty`);
|
|
290
|
+
}
|
|
291
|
+
return segments.map((segment) => validateCliSegment(segment, context));
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
/** Normalize an authored CLI command path. */
|
|
295
|
+
export const normalizeCliCommandPath = (
|
|
296
|
+
value: CliCommandPathInput,
|
|
297
|
+
context = 'CLI command path'
|
|
298
|
+
): readonly string[] =>
|
|
299
|
+
typeof value === 'string'
|
|
300
|
+
? splitCliPathString(value, context)
|
|
301
|
+
: value.map((segment) => validateCliSegment(segment, context));
|
|
302
|
+
|
|
303
|
+
const isTrailCliProjection = (
|
|
304
|
+
value: CliCommandPathInput | TrailCliProjection
|
|
305
|
+
): value is TrailCliProjection =>
|
|
306
|
+
typeof value !== 'string' &&
|
|
307
|
+
!Array.isArray(value) &&
|
|
308
|
+
value !== null &&
|
|
309
|
+
typeof value === 'object';
|
|
310
|
+
|
|
311
|
+
const trailCliProjectionFor = (
|
|
312
|
+
trail: TrailCliProjectionInput
|
|
313
|
+
): TrailCliProjection | undefined => {
|
|
314
|
+
if (trail.cli === undefined) {
|
|
315
|
+
return undefined;
|
|
316
|
+
}
|
|
317
|
+
return isTrailCliProjection(trail.cli) ? trail.cli : { path: trail.cli };
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const deriveCanonicalCliRoute = (
|
|
321
|
+
trail: TrailCliProjectionInput
|
|
322
|
+
): CliCommandRoute => {
|
|
323
|
+
const projection = trailCliProjectionFor(trail);
|
|
324
|
+
const path =
|
|
325
|
+
projection?.path === undefined
|
|
326
|
+
? deriveCliPath(trail.id)
|
|
327
|
+
: normalizeCliCommandPath(
|
|
328
|
+
projection.path,
|
|
329
|
+
`CLI command path for trail "${trail.id}"`
|
|
330
|
+
);
|
|
331
|
+
return {
|
|
332
|
+
kind: 'canonical',
|
|
333
|
+
path,
|
|
334
|
+
source: projection?.path === undefined ? 'derived' : 'trail',
|
|
335
|
+
target: trail.id,
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const normalizeCliAlias = ({
|
|
340
|
+
alias,
|
|
341
|
+
canonicalPath,
|
|
342
|
+
source,
|
|
343
|
+
target,
|
|
344
|
+
}: {
|
|
345
|
+
readonly alias: CliCommandAliasInput;
|
|
346
|
+
readonly canonicalPath: readonly string[];
|
|
347
|
+
readonly source: Extract<CliCommandRouteSource, 'surface' | 'trail'>;
|
|
348
|
+
readonly target: string;
|
|
349
|
+
}): CliCommandRoute => {
|
|
350
|
+
const context = `CLI command alias for trail "${target}"`;
|
|
351
|
+
if (typeof alias === 'string') {
|
|
352
|
+
const segment = alias.trim();
|
|
353
|
+
if (segment.length === 0) {
|
|
354
|
+
throw new ValidationError(`${context} cannot be empty`);
|
|
355
|
+
}
|
|
356
|
+
if (hasWhitespace(segment)) {
|
|
357
|
+
throw new ValidationError(
|
|
358
|
+
`${context} must be a single command segment; use a string array for absolute paths`
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
return {
|
|
362
|
+
kind: 'alias',
|
|
363
|
+
path: [
|
|
364
|
+
...canonicalPath.slice(0, -1),
|
|
365
|
+
validateCliSegment(segment, context),
|
|
366
|
+
],
|
|
367
|
+
source,
|
|
368
|
+
target,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
return {
|
|
372
|
+
kind: 'alias',
|
|
373
|
+
path: normalizeCliCommandPath(alias, context),
|
|
374
|
+
source,
|
|
375
|
+
target,
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
/** Derive resolved CLI command routes for one trail. */
|
|
380
|
+
export const deriveTrailCliCommandProjection = (
|
|
381
|
+
trail: TrailCliProjectionInput,
|
|
382
|
+
options?: DeriveTrailCliCommandProjectionOptions
|
|
383
|
+
): TrailCliCommandProjection => {
|
|
384
|
+
const canonical = deriveCanonicalCliRoute(trail);
|
|
385
|
+
const projection = trailCliProjectionFor(trail);
|
|
386
|
+
const trailAliases =
|
|
387
|
+
projection?.aliases?.map((alias) =>
|
|
388
|
+
normalizeCliAlias({
|
|
389
|
+
alias,
|
|
390
|
+
canonicalPath: canonical.path,
|
|
391
|
+
source: 'trail',
|
|
392
|
+
target: trail.id,
|
|
393
|
+
})
|
|
394
|
+
) ?? [];
|
|
395
|
+
const surfaceAliases =
|
|
396
|
+
options?.aliases?.map((alias) =>
|
|
397
|
+
normalizeCliAlias({
|
|
398
|
+
alias,
|
|
399
|
+
canonicalPath: canonical.path,
|
|
400
|
+
source: options.aliasSource ?? 'surface',
|
|
401
|
+
target: trail.id,
|
|
402
|
+
})
|
|
403
|
+
) ?? [];
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
path: canonical.path,
|
|
407
|
+
routes: [canonical, ...trailAliases, ...surfaceAliases],
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
|
|
217
411
|
// ---------------------------------------------------------------------------
|
|
218
412
|
// Public API
|
|
219
413
|
// ---------------------------------------------------------------------------
|
package/src/errors.ts
CHANGED
|
@@ -18,6 +18,7 @@ export const errorCategories = [
|
|
|
18
18
|
'timeout',
|
|
19
19
|
'rate_limit',
|
|
20
20
|
'network',
|
|
21
|
+
'shift',
|
|
21
22
|
'internal',
|
|
22
23
|
'auth',
|
|
23
24
|
'cancelled',
|
|
@@ -179,6 +180,22 @@ export class NetworkError extends TrailsError {
|
|
|
179
180
|
readonly retryable = true as const;
|
|
180
181
|
}
|
|
181
182
|
|
|
183
|
+
/**
|
|
184
|
+
* @example
|
|
185
|
+
* ```ts
|
|
186
|
+
* return Result.err(new WorkspaceShiftError('workspace changed during check'));
|
|
187
|
+
* ```
|
|
188
|
+
*
|
|
189
|
+
* Raised when the observed workspace substrate moves during one run.
|
|
190
|
+
*
|
|
191
|
+
* A shift voids the run's verdict, including passes. Callers can retry on
|
|
192
|
+
* stable ground without changing their request.
|
|
193
|
+
*/
|
|
194
|
+
export class WorkspaceShiftError extends TrailsError {
|
|
195
|
+
readonly category = 'shift' as const;
|
|
196
|
+
readonly retryable = true as const;
|
|
197
|
+
}
|
|
198
|
+
|
|
182
199
|
export class InternalError extends TrailsError {
|
|
183
200
|
readonly category: ErrorCategory = 'internal';
|
|
184
201
|
readonly retryable = false as const;
|
|
@@ -344,6 +361,12 @@ export const errorClasses = [
|
|
|
344
361
|
name: 'NetworkError',
|
|
345
362
|
retryable: true,
|
|
346
363
|
},
|
|
364
|
+
{
|
|
365
|
+
category: 'shift',
|
|
366
|
+
ctor: WorkspaceShiftError,
|
|
367
|
+
name: 'WorkspaceShiftError',
|
|
368
|
+
retryable: true,
|
|
369
|
+
},
|
|
347
370
|
{
|
|
348
371
|
category: 'internal',
|
|
349
372
|
ctor: InternalError,
|
|
@@ -397,6 +420,7 @@ export const codesByCategory = {
|
|
|
397
420
|
not_found: { exit: 2, http: 404, jsonRpc: -32_601 },
|
|
398
421
|
permission: { exit: 4, http: 403, jsonRpc: -32_600 },
|
|
399
422
|
rate_limit: { exit: 6, http: 429, jsonRpc: -32_603 },
|
|
423
|
+
shift: { exit: 10, http: 503, jsonRpc: -32_603 },
|
|
400
424
|
timeout: { exit: 5, http: 504, jsonRpc: -32_603 },
|
|
401
425
|
validation: { exit: 1, http: 400, jsonRpc: -32_602 },
|
|
402
426
|
} as const satisfies Record<ErrorCategory, ErrorCategoryCodes>;
|
|
@@ -414,6 +438,7 @@ const deriveCodeMap = <TCode extends keyof ErrorCategoryCodes>(
|
|
|
414
438
|
not_found: codesByCategory.not_found[code],
|
|
415
439
|
permission: codesByCategory.permission[code],
|
|
416
440
|
rate_limit: codesByCategory.rate_limit[code],
|
|
441
|
+
shift: codesByCategory.shift[code],
|
|
417
442
|
timeout: codesByCategory.timeout[code],
|
|
418
443
|
validation: codesByCategory.validation[code],
|
|
419
444
|
});
|
|
@@ -436,6 +461,7 @@ export const retryableMap: Record<ErrorCategory, boolean> = {
|
|
|
436
461
|
not_found: false,
|
|
437
462
|
permission: false,
|
|
438
463
|
rate_limit: true,
|
|
464
|
+
shift: true,
|
|
439
465
|
timeout: true,
|
|
440
466
|
validation: false,
|
|
441
467
|
} as const;
|
package/src/execute.ts
CHANGED
|
@@ -388,7 +388,7 @@ const prepareContext = async (
|
|
|
388
388
|
);
|
|
389
389
|
const permitted = enforcePermitRequirement(trail, baseCtx);
|
|
390
390
|
if (permitted.isErr()) {
|
|
391
|
-
return
|
|
391
|
+
return permitted;
|
|
392
392
|
}
|
|
393
393
|
|
|
394
394
|
const resources = await createResources(
|
|
@@ -1412,7 +1412,7 @@ const executeRequestedTrailVersion = async (
|
|
|
1412
1412
|
|
|
1413
1413
|
const resolved = resolveTrailVersion(trail, reference);
|
|
1414
1414
|
if (resolved.isErr()) {
|
|
1415
|
-
return
|
|
1415
|
+
return resolved;
|
|
1416
1416
|
}
|
|
1417
1417
|
|
|
1418
1418
|
if (resolved.value.current) {
|
|
@@ -1496,14 +1496,14 @@ const validateContextLayerInputs = (
|
|
|
1496
1496
|
): Result<TrailContext, ValidationError> => {
|
|
1497
1497
|
const layerInputs = readContextLayerInputs(ctx);
|
|
1498
1498
|
if (layerInputs.isErr()) {
|
|
1499
|
-
return
|
|
1499
|
+
return layerInputs;
|
|
1500
1500
|
}
|
|
1501
1501
|
if (layerInputs.value === undefined) {
|
|
1502
1502
|
return Result.ok(ctx);
|
|
1503
1503
|
}
|
|
1504
1504
|
const validated = validateLayerInputs(layers, layerInputs.value);
|
|
1505
1505
|
if (validated.isErr()) {
|
|
1506
|
-
return
|
|
1506
|
+
return validated;
|
|
1507
1507
|
}
|
|
1508
1508
|
return Result.ok({
|
|
1509
1509
|
...ctx,
|
|
@@ -1540,7 +1540,7 @@ const executeTrailInternal = async (
|
|
|
1540
1540
|
|
|
1541
1541
|
const resolvedCtx = await prepareContext(trail, options);
|
|
1542
1542
|
if (resolvedCtx.isErr()) {
|
|
1543
|
-
return
|
|
1543
|
+
return resolvedCtx;
|
|
1544
1544
|
}
|
|
1545
1545
|
|
|
1546
1546
|
const layers = composeAttachedLayers(trail, options);
|
|
@@ -1550,7 +1550,7 @@ const executeTrailInternal = async (
|
|
|
1550
1550
|
layers
|
|
1551
1551
|
);
|
|
1552
1552
|
if (layerCtx.isErr()) {
|
|
1553
|
-
return
|
|
1553
|
+
return layerCtx;
|
|
1554
1554
|
}
|
|
1555
1555
|
return await runTrail(
|
|
1556
1556
|
trail,
|
package/src/fire.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
TimeoutError,
|
|
18
18
|
RateLimitError,
|
|
19
19
|
NetworkError,
|
|
20
|
+
WorkspaceShiftError,
|
|
20
21
|
InternalError,
|
|
21
22
|
AuthError,
|
|
22
23
|
CancelledError,
|
|
@@ -391,6 +392,9 @@ export type { Topo, TopoIdentity } from './topo.js';
|
|
|
391
392
|
export {
|
|
392
393
|
deriveTrailsDbPath,
|
|
393
394
|
deriveTrailsDir,
|
|
395
|
+
deriveTrailsProjectKey,
|
|
396
|
+
deriveTrailsStateDir,
|
|
397
|
+
deriveTrailsStateHome,
|
|
394
398
|
ensureSubsystemSchema,
|
|
395
399
|
ensureTrailsWorkspace,
|
|
396
400
|
openReadTrailsDb,
|
|
@@ -441,8 +445,24 @@ export type {
|
|
|
441
445
|
} from './layer-projection.js';
|
|
442
446
|
|
|
443
447
|
// Derive
|
|
444
|
-
export {
|
|
445
|
-
|
|
448
|
+
export {
|
|
449
|
+
deriveCliPath,
|
|
450
|
+
deriveFields,
|
|
451
|
+
deriveTrailCliCommandProjection,
|
|
452
|
+
normalizeCliCommandPath,
|
|
453
|
+
} from './derive.js';
|
|
454
|
+
export type {
|
|
455
|
+
CliCommandAliasInput,
|
|
456
|
+
CliCommandPathInput,
|
|
457
|
+
CliCommandRoute,
|
|
458
|
+
CliCommandRouteKind,
|
|
459
|
+
CliCommandRouteSource,
|
|
460
|
+
DeriveTrailCliCommandProjectionOptions,
|
|
461
|
+
Field,
|
|
462
|
+
FieldOverride,
|
|
463
|
+
TrailCliCommandProjection,
|
|
464
|
+
TrailCliProjection,
|
|
465
|
+
} from './derive.js';
|
|
446
466
|
|
|
447
467
|
// Compose schema
|
|
448
468
|
export { buildComposeValidationSchema } from './compose-schema.js';
|
package/src/resource-config.ts
CHANGED
|
@@ -401,7 +401,7 @@ const doCreateResourceInstance = async (
|
|
|
401
401
|
try {
|
|
402
402
|
const created = await declaredResource.create(resourceContext);
|
|
403
403
|
if (created.isErr()) {
|
|
404
|
-
return
|
|
404
|
+
return created;
|
|
405
405
|
}
|
|
406
406
|
|
|
407
407
|
const instance = created.unwrap();
|
package/src/schedule-runtime.ts
CHANGED
package/src/serialization.ts
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
RateLimitError,
|
|
19
19
|
RetryExhaustedError,
|
|
20
20
|
TimeoutError,
|
|
21
|
+
WorkspaceShiftError,
|
|
21
22
|
errorClasses,
|
|
22
23
|
isTrailsError,
|
|
23
24
|
} from './errors.js';
|
|
@@ -89,6 +90,7 @@ const errorFactories: Record<ErrorCategory, ErrorFactory> = {
|
|
|
89
90
|
}
|
|
90
91
|
return new RateLimitError(msg, rlOpts);
|
|
91
92
|
},
|
|
93
|
+
shift: (msg, opts) => new WorkspaceShiftError(msg, opts),
|
|
92
94
|
timeout: (msg, opts) => new TimeoutError(msg, opts),
|
|
93
95
|
validation: (msg, opts) => new ValidationError(msg, opts),
|
|
94
96
|
};
|
package/src/trail.ts
CHANGED
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
isActivationSource,
|
|
13
13
|
} from './activation-source.js';
|
|
14
14
|
import type { AnyContour } from './contour.js';
|
|
15
|
-
import type {
|
|
15
|
+
import type {
|
|
16
|
+
FieldOverride,
|
|
17
|
+
CliCommandPathInput,
|
|
18
|
+
TrailCliProjection,
|
|
19
|
+
} from './derive.js';
|
|
16
20
|
import type { Layer } from './layer.js';
|
|
17
21
|
import type { Result } from './result.js';
|
|
18
22
|
import type { AnyResource } from './resource.js';
|
|
@@ -413,6 +417,8 @@ export interface TrailSpec<
|
|
|
413
417
|
readonly layers?: readonly Layer[] | undefined;
|
|
414
418
|
/** Per-field overrides for deriveFields() (labels, hints, options) */
|
|
415
419
|
readonly fields?: Readonly<Record<string, FieldOverride>> | undefined;
|
|
420
|
+
/** CLI projection metadata for canonical command path overrides and aliases. */
|
|
421
|
+
readonly cli?: CliCommandPathInput | TrailCliProjection | undefined;
|
|
416
422
|
/** Contours this trail operates on. */
|
|
417
423
|
readonly contours?: readonly AnyContour[] | undefined;
|
|
418
424
|
/** IDs or trail objects of downstream trails this trail may invoke via ctx.compose() */
|
package/src/trails-db.ts
CHANGED
|
@@ -1,44 +1,36 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, mkdirSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
4
6
|
|
|
5
7
|
import { NotFoundError } from './errors.js';
|
|
6
8
|
|
|
7
9
|
const TRAILS_DIR = '.trails';
|
|
8
10
|
const TRAILS_DB_FILE = 'trails.db';
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
+
const TRAILS_STORE_DIR = 'trails';
|
|
12
|
+
const TRAILS_PROJECTS_DIR = 'projects';
|
|
11
13
|
const SCHEMA_VERSION_TABLE = 'meta_schema_versions';
|
|
12
|
-
const
|
|
14
|
+
const SQLITE_BUSY_TIMEOUT_MS = 5000;
|
|
15
|
+
const PROJECT_KEY_HASH_LENGTH = 16;
|
|
16
|
+
const PROJECT_KEY_NAME_FALLBACK = 'project';
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
|
-
*
|
|
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).
|
|
19
|
+
* Legacy no-op compatibility export.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
21
|
+
* `.trails/` is committed project control, not disposable cache/state. New
|
|
22
|
+
* code should not write a `.trails/.gitignore`; keep this export available for
|
|
23
|
+
* older callers during the pre-1.0 cutover.
|
|
20
24
|
*/
|
|
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;
|
|
25
|
+
export const WORKSPACE_GITIGNORE_LINES = [] as const;
|
|
33
26
|
|
|
34
27
|
/**
|
|
35
|
-
*
|
|
36
|
-
* the file eagerly (e.g. during `trails create` scaffolding) or when asserting
|
|
37
|
-
* on the workspace bootstrap output.
|
|
28
|
+
* Legacy no-op compatibility export. See {@link WORKSPACE_GITIGNORE_LINES}.
|
|
38
29
|
*/
|
|
39
|
-
export const WORKSPACE_GITIGNORE_CONTENT =
|
|
30
|
+
export const WORKSPACE_GITIGNORE_CONTENT = '';
|
|
40
31
|
|
|
41
32
|
export interface TrailsDbLocationOptions {
|
|
33
|
+
readonly env?: Record<string, string | undefined>;
|
|
42
34
|
readonly path?: string;
|
|
43
35
|
readonly rootDir?: string;
|
|
44
36
|
}
|
|
@@ -56,74 +48,81 @@ interface SchemaVersionRow {
|
|
|
56
48
|
const deriveRootDir = (rootDir?: string): string =>
|
|
57
49
|
resolve(rootDir ?? process.cwd());
|
|
58
50
|
|
|
59
|
-
|
|
60
|
-
|
|
51
|
+
const sanitizeProjectKeyName = (name: string): string => {
|
|
52
|
+
const normalized = name.replaceAll(/[^a-zA-Z0-9._-]+/g, '-');
|
|
53
|
+
return normalized.length > 0 ? normalized : PROJECT_KEY_NAME_FALLBACK;
|
|
54
|
+
};
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
56
|
+
const projectHash = (rootDir: string): string =>
|
|
57
|
+
createHash('sha256')
|
|
58
|
+
.update(rootDir)
|
|
59
|
+
.digest('hex')
|
|
60
|
+
.slice(0, PROJECT_KEY_HASH_LENGTH);
|
|
66
61
|
|
|
67
|
-
const
|
|
68
|
-
|
|
62
|
+
export const deriveTrailsProjectKey = (
|
|
63
|
+
options?: TrailsDbLocationOptions
|
|
64
|
+
): string => {
|
|
65
|
+
const rootDir = deriveRootDir(options?.rootDir);
|
|
66
|
+
return `${sanitizeProjectKeyName(basename(rootDir))}-${projectHash(rootDir)}`;
|
|
69
67
|
};
|
|
70
68
|
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
69
|
+
export const deriveTrailsStateHome = (
|
|
70
|
+
options?: TrailsDbLocationOptions
|
|
71
|
+
): string => {
|
|
72
|
+
const env = options?.env ?? process.env;
|
|
73
|
+
return resolve(
|
|
74
|
+
env['TRAILS_STATE_HOME'] ??
|
|
75
|
+
env['XDG_STATE_HOME'] ??
|
|
76
|
+
join(homedir(), '.local', 'state')
|
|
78
77
|
);
|
|
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
78
|
};
|
|
87
79
|
|
|
88
|
-
const
|
|
89
|
-
|
|
80
|
+
export const deriveTrailsStateDir = (
|
|
81
|
+
options?: TrailsDbLocationOptions
|
|
82
|
+
): string =>
|
|
83
|
+
join(
|
|
84
|
+
deriveTrailsStateHome(options),
|
|
85
|
+
TRAILS_STORE_DIR,
|
|
86
|
+
TRAILS_PROJECTS_DIR,
|
|
87
|
+
deriveTrailsProjectKey(options)
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export const deriveTrailsDir = (options?: TrailsDbLocationOptions): string =>
|
|
91
|
+
join(deriveRootDir(options?.rootDir), TRAILS_DIR);
|
|
90
92
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
93
|
+
export const deriveTrailsDbPath = (options?: TrailsDbLocationOptions): string =>
|
|
94
|
+
options?.path
|
|
95
|
+
? resolve(options.path)
|
|
96
|
+
: join(deriveTrailsStateDir(options), TRAILS_DB_FILE);
|
|
95
97
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
readFileSync(gitignorePath, 'utf8')
|
|
99
|
-
);
|
|
98
|
+
const ensureDbParentDir = (dbPath: string): void => {
|
|
99
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
100
100
|
};
|
|
101
101
|
|
|
102
102
|
/**
|
|
103
103
|
* Bootstrap the `.trails/` workspace at `rootDir`.
|
|
104
104
|
*
|
|
105
|
-
* Creates the
|
|
106
|
-
*
|
|
107
|
-
*
|
|
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.
|
|
105
|
+
* Creates only the committed-control directory. Derived cache and observed
|
|
106
|
+
* state live in the per-user Trails store, so this helper intentionally does
|
|
107
|
+
* not create `.trails/cache`, `.trails/state`, or `.trails/.gitignore`.
|
|
111
108
|
*/
|
|
112
109
|
export const ensureTrailsWorkspace = (rootDir: string): void => {
|
|
113
110
|
const trailsDir = deriveTrailsDir({ rootDir });
|
|
114
111
|
mkdirSync(trailsDir, { recursive: true });
|
|
115
|
-
for (const subdir of WORKSPACE_SUBDIRS) {
|
|
116
|
-
mkdirSync(join(trailsDir, subdir), { recursive: true });
|
|
117
|
-
}
|
|
118
|
-
ensureWorkspaceGitignore(trailsDir);
|
|
119
112
|
};
|
|
120
113
|
|
|
121
114
|
const initializeWritePragmas = (db: Database): void => {
|
|
115
|
+
db.run(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS.toString()}`);
|
|
122
116
|
db.run('PRAGMA journal_mode = WAL');
|
|
123
117
|
db.run('PRAGMA synchronous = NORMAL');
|
|
124
118
|
db.run('PRAGMA foreign_keys = ON');
|
|
125
119
|
};
|
|
126
120
|
|
|
121
|
+
const initializeReadPragmas = (db: Database): void => {
|
|
122
|
+
db.run(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS.toString()}`);
|
|
123
|
+
db.run('PRAGMA foreign_keys = ON');
|
|
124
|
+
};
|
|
125
|
+
|
|
127
126
|
const ensureSchemaVersionTable = (db: Database): void => {
|
|
128
127
|
db.run(`CREATE TABLE IF NOT EXISTS ${SCHEMA_VERSION_TABLE} (
|
|
129
128
|
subsystem TEXT PRIMARY KEY,
|
|
@@ -160,15 +159,14 @@ export const openWriteTrailsDb = (
|
|
|
160
159
|
options?: TrailsDbLocationOptions
|
|
161
160
|
): Database => {
|
|
162
161
|
const rootDir = deriveRootDir(options?.rootDir);
|
|
163
|
-
const
|
|
164
|
-
options?.
|
|
165
|
-
|
|
162
|
+
const locationOptions: TrailsDbLocationOptions = {
|
|
163
|
+
...(options?.env === undefined ? {} : { env: options.env }),
|
|
164
|
+
...(options?.path === undefined ? {} : { path: options.path }),
|
|
165
|
+
rootDir,
|
|
166
|
+
};
|
|
167
|
+
const dbPath = deriveTrailsDbPath(locationOptions);
|
|
166
168
|
|
|
167
|
-
|
|
168
|
-
ensureTrailsWorkspace(rootDir);
|
|
169
|
-
} else {
|
|
170
|
-
ensureDbParentDir(dbPath);
|
|
171
|
-
}
|
|
169
|
+
ensureDbParentDir(dbPath);
|
|
172
170
|
|
|
173
171
|
const db = new Database(dbPath, { create: true });
|
|
174
172
|
initializeWritePragmas(db);
|
|
@@ -186,7 +184,7 @@ export const openReadTrailsDb = (
|
|
|
186
184
|
);
|
|
187
185
|
}
|
|
188
186
|
const db = new Database(dbPath, { readonly: true });
|
|
189
|
-
db
|
|
187
|
+
initializeReadPragmas(db);
|
|
190
188
|
return db;
|
|
191
189
|
};
|
|
192
190
|
|