@ontrails/core 1.0.0-beta.30 → 1.0.0-beta.39

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 CHANGED
@@ -1,5 +1,68 @@
1
1
  # @ontrails/core
2
2
 
3
+ ## 1.0.0-beta.39
4
+
5
+ ### Patch Changes
6
+
7
+ - [`f42ca6e`](https://github.com/outfitter-dev/trails/commit/f42ca6e40b29155acec446e5bf44e52e014466bd): Hard cutover: the CLI consumes `cli` bindings from the app-authored surfaces overlay. Scalar bindings behave identically to the removed cliAliases (parity-tested) — the binding name splits on `.` into a transparent synonym command path for exactly one trail. List bindings arrive as command groups: each expanded member trail gets a group-prefixed route that dispatches the member trail with its identity preserved, and a singleton list stays a group. Expansion is fail-fast boundary validation: a scalar binding resolving to zero or multiple trails, or a group with an empty member union, is a `ValidationError` naming the binding. `DeriveTopoGraphOptions.cliAliases`, the `cliAliases`/`trailsCliAliases` app-module export convention, and the per-kind compile lift are deleted; `deriveCliCommands`/`createProgram` take `overlays` instead of `aliases`, and both topo-graph derivation pipelines expand the same bindings through one shared helper so runtime CLI routes and lock routes come from one semantic. A leftover legacy export is now a Warden error (`no-legacy-cli-alias-export`) naming the `surfaceOverlay({ cli: { ... } })` rewrite.
8
+
9
+ This is a breaking API removal shipped under the lockstep beta patch convention (pre-1.0 hard-cutover posture, zero external adoption); the removed options have no deprecation window by design.
10
+
11
+ - [`6b75a46`](https://github.com/outfitter-dev/trails/commit/6b75a46ab6210237d306cceade833bf9ce6e7431): The core barrel is now execution-portable: no eager `bun:`/`node:` builtin imports remain on its module graph (TRL-1198). `trails-db`, workspace discovery, and path security load `bun:sqlite`, `node:fs`, `node:os`, and `node:path` lazily through `process.getBuiltinModule` at first use, and signal payload summaries plus per-project store keys use a pure SHA-256 (output-identical to `node:crypto`). A Worker bundle no longer needs a `bun:sqlite` stub plugin or the `nodejs_compat` flag to serve trails; the Cloudflare adapter's miniflare lane now bundles without externals and boots workerd without `nodejs_compat` as the structural regression gate, and its README stub instructions are replaced with the portable posture. Tooling helpers throw a clear `InternalError` naming the missing builtin when called on runtimes without it.
12
+ - [`81373bc`](https://github.com/outfitter-dev/trails/commit/81373bc5e980bb06d56fb06af4f0986f72e318c7): Wave-2 MCP cutover to the app-authored `surfaces` overlay. The overlay's `mcp` bindings are now the authored, lockable default for the MCP surface: a list binding derives one grouped trailhead tool (member selection in `{ trail, input }`, member identity preserved in `{ trail, output }`, deterministic derived description), and a scalar binding derives an additional tool synonym whose MCP-safe name is published verbatim and must expand to exactly one trail. `deriveMcpTools`/`createServer` accept the new `overlays` option; `@ontrails/core` gains `expandMcpSurfaceBindings` and `deriveMcpTrailheadDescription`.
13
+
14
+ The call-site `CreateServerOptions.trailheads` map survives as permanent override-in-context design, not a compatibility bridge: when both channels are present, the call-site map wins at runtime. Warden's new `trailhead-override-divergence` rule (warn) names both sides when a call-site map's binding names or member selectors diverge from the authored overlay default.
15
+
16
+ Topographer now derives `graph.trailheads` from the overlay's `mcp` list bindings in both `deriveTopoGraph` and the store-side graph build, so trailhead facts flow from compiled locks into Wayfinder reads for the first time. The never-wired `DeriveTopoGraphOptions.trailheads` option and the `TopoGraphTrailheadDeclaration`/`TopoGraphTrailheadTrailSelector` types are removed — a beta-window hard cutover of an option no caller could reach; author the equivalent `mcp` list binding in `surfaceOverlay({ mcp })` instead.
17
+
18
+ - [`820b4ad`](https://github.com/outfitter-dev/trails/commit/820b4ad9c40ea383b3c489a05fe7e4b2328e324f): Add `surfaceOverlay` — the shared surface-naming schema (scalar binding = synonym, list binding = grouped entry, singleton list stays a group) with app-authored/adapter-derived overlay provenance enforced at collection and consumption, and the `surface-overlay-coherence` Warden rule. MCP tool-name derivation moves to `@ontrails/core` (`deriveMcpToolName`) so the surface and governance read one projection; `@ontrails/mcp`'s `deriveToolName` now delegates to it. The coherence rule activates on standard warden runs once fresh derivations collect app-module overlays through the shared compile channel (TRL-1209, next in this stack).
19
+ - [`28d75fb`](https://github.com/outfitter-dev/trails/commit/28d75fbadecc62794f43957bc3aca11a4cf39c51): Add `forkVersion()` so fork version entries get typed blazes (TRL-1180). `TrailVersions` fixes every entry's generics to `unknown`, which left fork blazes with `unknown` input and forced authors to re-parse the already-validated value just to narrow it. `forkVersion({ input, output, blaze, ... })` threads the entry's own schemas into the blaze signature (including merged `composeInput` fields) and enforces the entry's output shape at compile time; the erasure back to `TrailVersionEntry` is sound because the fork pipeline validates raw input against the entry's own schema before dispatch. `TrailVersionForkSpec` is exported alongside it.
20
+ - [`b9e82a3`](https://github.com/outfitter-dev/trails/commit/b9e82a33546356c93fbc302fb934a83f19f1c2c5): Webhook ingress v2 (TRL-1194, absorbing TRL-1174 and TRL-1175): store-verified, per-endpoint webhook ingress becomes framework-expressible. `webhook()` accepts dynamic path segments (`path: '/hooks/:endpoint'`) whose values are delivered as envelope fields, opt-in `rawBody: true` delivery (a non-JSON body is no longer a surface-level failure — the trail owns payload interpretation), an allowlisted `headers` list delivered lowercased, and `resources` that make `verify` resource-capable: the HTTP surface resolves the declared resources into a context for the verifier and releases them afterwards, so signature checks can reach stores holding per-endpoint secrets. Envelope-mode ingress responds 202 Accepted; classic static webhooks keep their exact-match, JSON-gated, 200 behavior. Core exports `parseWebhookPathParams`, `matchWebhookPath`, `webhookPathPatternsOverlap`, and `createResources`. The `webhook-route-collision` Warden rule now also flags dynamic patterns that overlap other webhook or derived routes, not just exact method/path duplicates.
21
+
22
+ ## 1.0.0-beta.38
23
+
24
+ ### Patch Changes
25
+
26
+ - [`a105127`](https://github.com/outfitter-dev/trails/commit/a105127e5662ed9a6c245125f791fb0182da3f5e): Add the `@ontrails/cloudflare` adapter collection with its first two service subpaths. `@ontrails/cloudflare/workers` exports `createWorkersHandler`, a materializer producing the `{ fetch(request, env, ctx) }` Worker export on the shared HTTP fetch kernel, with an env bridge that re-resolves env-bound resources whenever a new Worker `env` arrives so no resource instance serves a request with a stale env. `@ontrails/cloudflare/kv` exports `cloudflareKv`, a resource definition wrapping a KV namespace binding (`get`/`put`/`delete`/`list` with TTL options) plus an in-memory `createMemoryKv` mock so `testAll` runs configuration-free.
27
+
28
+ `@ontrails/core` now guards the default trail context fields: `requestId` falls back to `crypto.randomUUID()` when the `Bun` global is absent, and `cwd`/`env` fall back to `'/'`/`{}` when `process` is absent, so trail execution works on runtimes like Cloudflare Workers.
29
+
30
+ `@ontrails/warden` registers the `@ontrails/cloudflare` public barrel in the repo-local `public-export-example-coverage` policy, requiring `@example` TSDoc coverage on `createWorkersHandler` and `cloudflareKv`.
31
+
32
+ ## 1.0.0-beta.37
33
+
34
+ ## 1.0.0-beta.36
35
+
36
+ ## 1.0.0-beta.35
37
+
38
+ ### Patch Changes
39
+
40
+ - [`417bd84`](https://github.com/outfitter-dev/trails/commit/417bd8471d0f0f47ad5f33cd2ac1c606eccd72f8): Promote signal trace helpers from tracing compatibility code to core exports, and make tracing's memory sink wrapper use the observe-owned implementation.
41
+ - [`a88114b`](https://github.com/outfitter-dev/trails/commit/a88114b4dd0772db6b58ecdb7671e4169e6bdca5): Expose stable typed topo diagnostics for missing references so downstream
42
+ migration tooling can consume validation results without parsing human messages.
43
+
44
+ ## 1.0.0-beta.34
45
+
46
+ ## 1.0.0-beta.33
47
+
48
+ ## 1.0.0-beta.32
49
+
50
+ ### Patch Changes
51
+
52
+ - 3e5c0fc: Export shared diagnostic base types from core and align governance diagnostic
53
+ severity vocabulary across adapter checks, permits, and Warden.
54
+ - f3c4fef: Export a shared `escapeRegExp` helper from core and migrate first-party callers off local copies.
55
+ - cb0a9d8: Export shared workspace package discovery helpers from core and migrate first-party discovery callers.
56
+ - 21c6dda: Rename topo and draft report types to `TopoDiagnostic` and `DraftDiagnostic`, with deprecated `TopoIssue` and `DraftFinding` aliases preserved for source compatibility.
57
+ - fe72b84: Fold remaining Regrade and Warden scan-target surfaces onto the shared path-scope vocabulary.
58
+
59
+ ## 1.0.0-beta.31
60
+
61
+ ### Patch Changes
62
+
63
+ - 4cd5d4e: Add shared glob, path-scope, and trail-id glob contracts for downstream Trails tooling.
64
+ - 38907cc: Adopt the shared trail-id glob engine for surface filtering and Wayfinder entity filters so dotted `*`, `**`, and `?` patterns behave consistently across graph inspection and surface selection.
65
+
3
66
  ## 1.0.0-beta.30
4
67
 
5
68
  ## 1.0.0-beta.29
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ontrails/core",
3
- "version": "1.0.0-beta.30",
3
+ "version": "1.0.0-beta.39",
4
4
  "files": [
5
5
  "src/**/*.ts",
6
6
  "!src/**/__tests__/**",
package/src/context.ts CHANGED
@@ -21,11 +21,25 @@ export const passthroughTrace: TraceFn = async <T>(
21
21
  fn: () => T | Promise<T>
22
22
  ): Promise<T> => await fn();
23
23
 
24
+ const defaultCwd = (): string =>
25
+ typeof process === 'undefined' ? '/' : process.cwd();
26
+
27
+ const defaultEnv = (): Record<string, string | undefined> =>
28
+ typeof process === 'undefined'
29
+ ? {}
30
+ : (process.env as Record<string, string | undefined>);
31
+
32
+ const defaultRequestId = (): string =>
33
+ typeof Bun === 'undefined' ? crypto.randomUUID() : Bun.randomUUIDv7();
34
+
24
35
  /**
25
36
  * Create a TrailContext with sensible defaults.
26
37
  *
27
- * - `requestId` defaults to `Bun.randomUUIDv7()` (sortable v7 UUID)
38
+ * - `requestId` defaults to `Bun.randomUUIDv7()` (sortable v7 UUID), falling
39
+ * back to `crypto.randomUUID()` on runtimes without the `Bun` global
28
40
  * - `abortSignal` defaults to a fresh, non-aborted `AbortSignal`
41
+ * - `cwd`/`env` default to the `process` globals when available, and to
42
+ * `'/'`/`{}` on runtimes without `process` (for example Cloudflare Workers)
29
43
  * - All other fields come from `overrides`
30
44
  */
31
45
  export const createTrailContext = (
@@ -33,10 +47,10 @@ export const createTrailContext = (
33
47
  ): TrailContext => {
34
48
  const ctx = {
35
49
  abortSignal: new AbortController().signal,
36
- cwd: process.cwd(),
50
+ cwd: defaultCwd(),
37
51
  dryRun: false,
38
- env: process.env as Record<string, string | undefined>,
39
- requestId: Bun.randomUUIDv7(),
52
+ env: defaultEnv(),
53
+ requestId: defaultRequestId(),
40
54
  trace: passthroughTrace,
41
55
  ...overrides,
42
56
  } as MutableTrailContext;
package/src/derive.ts CHANGED
@@ -145,7 +145,7 @@ const propagateDescription = (
145
145
  }
146
146
  };
147
147
 
148
- /** Step one level of optional/default unwrapping. Returns null if not a wrapper type. */
148
+ /** Step one level of transparent wrapper unwrapping. Returns null if not a wrapper type. */
149
149
  const unwrapStep = (
150
150
  current: ZodInternals,
151
151
  state: {
@@ -155,10 +155,16 @@ const unwrapStep = (
155
155
  }
156
156
  ): ZodInternals | null => {
157
157
  const defType = current._zod.def['type'] as string;
158
- if (defType !== 'optional' && defType !== 'default') {
158
+ if (
159
+ defType !== 'optional' &&
160
+ defType !== 'default' &&
161
+ defType !== 'readonly'
162
+ ) {
159
163
  return null;
160
164
  }
161
- state.required = false;
165
+ if (defType !== 'readonly') {
166
+ state.required = false;
167
+ }
162
168
  if (defType === 'default') {
163
169
  state.defaultValue = current._zod.def['defaultValue'];
164
170
  }
@@ -376,6 +382,27 @@ const normalizeCliAlias = ({
376
382
  };
377
383
  };
378
384
 
385
+ /**
386
+ * Convert app name + trail ID to an MCP-safe tool name.
387
+ *
388
+ * MCP tool names must be `[a-z0-9_]+`: the app name prefixes the trail id,
389
+ * dots and hyphens collapse to underscores, and everything lowercases. This
390
+ * is the one owner for the projection — the MCP surface renders tools with
391
+ * it and Warden checks binding-name collisions against it, so the two
392
+ * readers cannot drift.
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * deriveMcpToolName('myapp', 'entity.show'); // "myapp_entity_show"
397
+ * deriveMcpToolName('dispatch', 'patch.search'); // "dispatch_patch_search"
398
+ * ```
399
+ */
400
+ export const deriveMcpToolName = (appName: string, trailId: string): string => {
401
+ const prefix = appName.toLowerCase().replaceAll(/[.-]/g, '_');
402
+ const suffix = trailId.toLowerCase().replaceAll(/[.-]/g, '_');
403
+ return `${prefix}_${suffix}`;
404
+ };
405
+
379
406
  /** Derive resolved CLI command routes for one trail. */
380
407
  export const deriveTrailCliCommandProjection = (
381
408
  trail: TrailCliProjectionInput,
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shared diagnostic vocabulary for governance-style findings.
3
+ *
4
+ * Runtime side-channel records and field-state reports can still define their
5
+ * own shapes. This base exists for tools that report rule or check failures.
6
+ */
7
+
8
+ export type DiagnosticSeverity = 'error' | 'warn';
9
+
10
+ export interface DiagnosticBase<TCode extends string = string> {
11
+ readonly code?: TCode | undefined;
12
+ readonly message: string;
13
+ readonly severity: DiagnosticSeverity;
14
+ }
15
+
16
+ export interface RuleDiagnosticBase<
17
+ TCode extends string = string,
18
+ TRule extends string = string,
19
+ > extends DiagnosticBase<TCode> {
20
+ readonly rule: TRule;
21
+ }
package/src/draft.ts CHANGED
@@ -25,7 +25,7 @@ export interface DraftDependency {
25
25
  readonly toId: string;
26
26
  }
27
27
 
28
- export interface DraftFinding {
28
+ export interface DraftDiagnostic {
29
29
  readonly id: string;
30
30
  readonly kind: 'contour' | 'resource' | 'signal' | 'trail' | 'unknown';
31
31
  readonly message: string;
@@ -34,11 +34,19 @@ export interface DraftFinding {
34
34
  readonly dependsOn?: string | undefined;
35
35
  }
36
36
 
37
+ /**
38
+ * @deprecated Use {@link DraftDiagnostic}. Kept as a source-compatible alias
39
+ * during the v1 vocabulary cutover.
40
+ */
41
+ export interface DraftFinding extends DraftDiagnostic {
42
+ readonly id: DraftDiagnostic['id'];
43
+ }
44
+
37
45
  export interface DraftReport {
38
46
  readonly contaminatedIds: ReadonlySet<string>;
39
47
  readonly declaredDraftIds: ReadonlySet<string>;
40
48
  readonly dependencies: readonly DraftDependency[];
41
- readonly findings: readonly DraftFinding[];
49
+ readonly findings: readonly DraftDiagnostic[];
42
50
  }
43
51
 
44
52
  interface DraftReason {
@@ -120,7 +128,7 @@ const nodeKind = (
120
128
  trails: ReadonlyMap<string, AnyTrail>,
121
129
  signals: ReadonlyMap<string, AnySignal>,
122
130
  resources: ReadonlyMap<string, AnyResource>
123
- ): DraftFinding['kind'] => {
131
+ ): DraftDiagnostic['kind'] => {
124
132
  if (contours.has(id)) {
125
133
  return 'contour';
126
134
  }
@@ -136,7 +144,7 @@ const nodeKind = (
136
144
  return 'unknown';
137
145
  };
138
146
 
139
- const displayKind = (kind: DraftFinding['kind']): string =>
147
+ const displayKind = (kind: DraftDiagnostic['kind']): string =>
140
148
  kind === 'unknown' ? 'Node' : kind[0]?.toUpperCase() + kind.slice(1);
141
149
 
142
150
  const draftIdsFromKeys = (keys: Iterable<string>): string[] =>
@@ -152,8 +160,8 @@ const collectDeclaredDraftIds = (topo: Topo): ReadonlySet<string> =>
152
160
 
153
161
  const findingForDraftId = (
154
162
  id: string,
155
- kind: DraftFinding['kind']
156
- ): DraftFinding => ({
163
+ kind: DraftDiagnostic['kind']
164
+ ): DraftDiagnostic => ({
157
165
  id,
158
166
  kind,
159
167
  message: `${displayKind(id ? kind : 'unknown')} "${id}" is draft and cannot appear in the established graph.`,
@@ -162,9 +170,9 @@ const findingForDraftId = (
162
170
 
163
171
  const findingForContamination = (
164
172
  id: string,
165
- kind: DraftFinding['kind'],
173
+ kind: DraftDiagnostic['kind'],
166
174
  reason: DraftReason
167
- ): DraftFinding => {
175
+ ): DraftDiagnostic => {
168
176
  const dependencyLabel = isDraftId(reason.dependsOn)
169
177
  ? `draft "${reason.dependsOn}"`
170
178
  : `draft-contaminated "${reason.dependsOn}"`;
@@ -244,7 +252,7 @@ const contaminationFindingForId = (
244
252
  trails: ReadonlyMap<string, AnyTrail>,
245
253
  signals: ReadonlyMap<string, AnySignal>,
246
254
  resources: ReadonlyMap<string, AnyResource>
247
- ): DraftFinding | undefined => {
255
+ ): DraftDiagnostic | undefined => {
248
256
  if (declaredDraftIds.has(id)) {
249
257
  return undefined;
250
258
  }
@@ -269,7 +277,7 @@ const collectFindings = (
269
277
  trails: ReadonlyMap<string, AnyTrail>,
270
278
  signals: ReadonlyMap<string, AnySignal>,
271
279
  resources: ReadonlyMap<string, AnyResource>
272
- ): DraftFinding[] => [
280
+ ): DraftDiagnostic[] => [
273
281
  ...[...declaredDraftIds]
274
282
  .toSorted()
275
283
  .map((id) =>
package/src/glob.ts ADDED
@@ -0,0 +1,81 @@
1
+ export interface GlobConfig {
2
+ readonly separator: '/' | '.';
3
+ }
4
+
5
+ /**
6
+ * Escape a literal string so it can be embedded safely in a RegExp source.
7
+ *
8
+ * @example
9
+ * ```ts
10
+ * const pattern = new RegExp(`^${escapeRegExp('@ontrails/core')}$`);
11
+ * ```
12
+ */
13
+ export const escapeRegExp = (value: string): string =>
14
+ value.replaceAll(/[.*+?^${}()|[\]\\]/g, '\\$&');
15
+
16
+ const separatorRegExp = (separator: GlobConfig['separator']): string =>
17
+ escapeRegExp(separator);
18
+
19
+ export const globToRegExp = (pattern: string, config: GlobConfig): RegExp => {
20
+ const separator = separatorRegExp(config.separator);
21
+ const parts: string[] = ['^'];
22
+
23
+ for (let index = 0; index < pattern.length; index += 1) {
24
+ const char = pattern[index];
25
+ const next = pattern[index + 1];
26
+
27
+ if (char === '*' && next === '*') {
28
+ if (pattern[index + 2] === config.separator) {
29
+ parts.push(`(?:.*${separator})?`);
30
+ index += 2;
31
+ } else {
32
+ parts.push('.*');
33
+ index += 1;
34
+ }
35
+ continue;
36
+ }
37
+
38
+ if (char === '*') {
39
+ parts.push(`[^${separator}]*`);
40
+ continue;
41
+ }
42
+
43
+ if (char === '?') {
44
+ parts.push(`[^${separator}]`);
45
+ continue;
46
+ }
47
+
48
+ parts.push(escapeRegExp(char ?? ''));
49
+ }
50
+
51
+ parts.push('$');
52
+ return new RegExp(parts.join(''));
53
+ };
54
+
55
+ export const matchesGlob = (
56
+ value: string,
57
+ pattern: string,
58
+ config: GlobConfig
59
+ ): boolean => {
60
+ if (value === pattern) {
61
+ return true;
62
+ }
63
+
64
+ const terminalDoubleStar = `${config.separator}**`;
65
+ if (pattern.endsWith(terminalDoubleStar)) {
66
+ const parent = pattern.slice(0, -terminalDoubleStar.length);
67
+ if (value === parent) {
68
+ return true;
69
+ }
70
+ }
71
+
72
+ return globToRegExp(pattern, config).test(value);
73
+ };
74
+
75
+ export const matchesAnyGlob = (
76
+ value: string,
77
+ patterns: readonly string[] | undefined,
78
+ config: GlobConfig
79
+ ): boolean =>
80
+ patterns !== undefined &&
81
+ patterns.some((pattern) => matchesGlob(value, pattern, config));
package/src/index.ts CHANGED
@@ -68,6 +68,11 @@ export {
68
68
  redactErrorString,
69
69
  } from './error-projection.js';
70
70
  export type { ErrorDiagnosticsProjection } from './error-projection.js';
71
+ export type {
72
+ DiagnosticBase,
73
+ DiagnosticSeverity,
74
+ RuleDiagnosticBase,
75
+ } from './diagnostics.js';
71
76
 
72
77
  // Types
73
78
  export type {
@@ -112,6 +117,25 @@ export type {
112
117
  // Context factory
113
118
  export { createTrailContext, passthroughTrace } from './context.js';
114
119
 
120
+ // Glob and path scope
121
+ export {
122
+ escapeRegExp,
123
+ globToRegExp,
124
+ matchesAnyGlob,
125
+ matchesGlob,
126
+ } from './glob.js';
127
+ export type { GlobConfig } from './glob.js';
128
+ export {
129
+ includedByPathScope,
130
+ matchesAnyPathGlob,
131
+ matchesPathGlob,
132
+ normalizePathScopePath,
133
+ pathScopeSchema,
134
+ } from './path-scope.js';
135
+ export type { PathGlob, PathScope, ScanTargets } from './path-scope.js';
136
+ export { matchesAnyTrailIdGlob, matchesTrailIdGlob } from './trail-id-glob.js';
137
+ export type { TrailIdGlob } from './trail-id-glob.js';
138
+
115
139
  // Resource
116
140
  export {
117
141
  createResourceLookup,
@@ -119,8 +143,13 @@ export {
119
143
  isResource,
120
144
  resource,
121
145
  } from './resource.js';
122
- export { drainResources, resolveResourceConfig } from './resource-config.js';
146
+ export {
147
+ createResources,
148
+ drainResources,
149
+ resolveResourceConfig,
150
+ } from './resource-config.js';
123
151
  export type {
152
+ ResolvedResourceScope,
124
153
  ResourceConfigValues,
125
154
  ResourceDrainReport,
126
155
  } from './resource-config.js';
@@ -162,6 +191,7 @@ export {
162
191
  export type { ActivationSourceProjection } from './activation-source-projection.js';
163
192
  export {
164
193
  deriveSupportedTrailVersions,
194
+ forkVersion,
165
195
  getTrailVersionEntryKind,
166
196
  hasDeprecatedTrailVersionGuidance,
167
197
  intentValues,
@@ -200,6 +230,7 @@ export type {
200
230
  TrailVersionArchivedStatus,
201
231
  TrailVersionDeprecatedStatus,
202
232
  TrailVersionForkEntry,
233
+ TrailVersionForkSpec,
203
234
  TrailVersionRevisionEntry,
204
235
  TrailVersions,
205
236
  TrailVersionStatus,
@@ -278,10 +309,13 @@ export type {
278
309
  export {
279
310
  getWebhookHeader,
280
311
  getWebhookHeaders,
312
+ matchWebhookPath,
313
+ parseWebhookPathParams,
281
314
  validateWebhookSource,
282
315
  verifyWebhookRequest,
283
316
  webhook,
284
317
  webhookMethods,
318
+ webhookPathPatternsOverlap,
285
319
  } from './webhook.js';
286
320
  export type {
287
321
  WebhookMethod,
@@ -386,6 +420,31 @@ export type {
386
420
  export { topo } from './topo.js';
387
421
  export type { Topo, TopoIdentity } from './topo.js';
388
422
 
423
+ // Surface overlay
424
+ export {
425
+ SURFACES_OVERLAY_NAMESPACE,
426
+ classifySurfaceBinding,
427
+ deriveMcpTrailheadDescription,
428
+ expandCliSurfaceBindings,
429
+ expandMcpSurfaceBindings,
430
+ resolveSurfaceOverlayBindings,
431
+ surfaceBindingsFromLockOverlays,
432
+ surfaceOverlay,
433
+ surfaceOverlayBindingsSchema,
434
+ } from './surface-overlay.js';
435
+ export type {
436
+ CliSurfaceBindingAliases,
437
+ McpSurfaceBindingExpansion,
438
+ OverlayEnvelopeLike,
439
+ OverlayProvenance,
440
+ SurfaceBindingRef,
441
+ SurfaceBindingShape,
442
+ SurfaceBindingValue,
443
+ SurfaceBindings,
444
+ SurfaceOverlay,
445
+ SurfaceOverlayBindings,
446
+ } from './surface-overlay.js';
447
+
389
448
  // Generic trails-db helpers (shared framework infrastructure per ADR-0014).
390
449
  // The topo-store public API that previously lived here moved to
391
450
  // `@ontrails/topographer` per ADR-0042.
@@ -417,13 +476,21 @@ export {
417
476
  export type {
418
477
  DraftDependency,
419
478
  DraftDependencyKind,
479
+ DraftDiagnostic,
420
480
  DraftFinding,
421
481
  DraftReport,
422
482
  } from './draft.js';
423
483
 
424
484
  // Topo validation
425
- export { validateTopo } from './validate-topo.js';
426
- export type { TopoIssue } from './validate-topo.js';
485
+ export { getTopoDiagnostics, validateTopo } from './validate-topo.js';
486
+ export type {
487
+ TopoDiagnostic,
488
+ TopoDiagnosticCode,
489
+ TopoIssue,
490
+ TopoMissingReference,
491
+ TopoReferenceKind,
492
+ TopoReferenceOwnerKind,
493
+ } from './validate-topo.js';
427
494
  export { validateEstablishedTopo } from './validate-established-topo.js';
428
495
 
429
496
  // Layer
@@ -448,6 +515,7 @@ export type {
448
515
  export {
449
516
  deriveCliPath,
450
517
  deriveFields,
518
+ deriveMcpToolName,
451
519
  deriveTrailCliCommandProjection,
452
520
  normalizeCliCommandPath,
453
521
  } from './derive.js';
@@ -481,6 +549,7 @@ export type { ExecuteTrailOptions } from './execute.js';
481
549
  export {
482
550
  clearTraceSink,
483
551
  createActivationTraceRecord,
552
+ createSignalTraceRecord,
484
553
  createTraceRecord,
485
554
  getTraceContext,
486
555
  getTraceSink,
@@ -489,6 +558,7 @@ export {
489
558
  TRACE_CONTEXT_KEY,
490
559
  traceContextFromRecord,
491
560
  writeActivationTraceRecord,
561
+ writeSignalTraceRecord,
492
562
  } from './tracing.js';
493
563
  export type {
494
564
  ActivationTraceRecordName,
@@ -581,10 +651,15 @@ export { securePath, isPathSafe, deriveSafePath } from './path-security.js';
581
651
 
582
652
  // Workspace
583
653
  export {
654
+ deriveRelativePath,
655
+ findWorkspacePackage,
584
656
  findWorkspaceRoot,
585
657
  isInsideWorkspace,
586
- deriveRelativePath,
658
+ listWorkspacePackageDirs,
659
+ listWorkspacePackages,
660
+ listWorkspacePatterns,
587
661
  } from './workspace.js';
662
+ export type { WorkspacePackage, WorkspaceRootManifest } from './workspace.js';
588
663
 
589
664
  // Blob
590
665
  export {
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+
3
+ import { matchesAnyGlob, matchesGlob } from './glob.js';
4
+
5
+ declare const pathGlobBrand: unique symbol;
6
+
7
+ export type PathGlob = string & {
8
+ readonly [pathGlobBrand]: 'PathGlob';
9
+ };
10
+
11
+ export const normalizePathScopePath = (value: string): string =>
12
+ value.replaceAll('\\', '/').replace(/^\.\//, '');
13
+
14
+ export const matchesPathGlob = (path: string, pattern: string): boolean =>
15
+ matchesGlob(normalizePathScopePath(path), normalizePathScopePath(pattern), {
16
+ separator: '/',
17
+ });
18
+
19
+ export const matchesAnyPathGlob = (
20
+ path: string,
21
+ patterns: readonly string[] | undefined
22
+ ): boolean =>
23
+ matchesAnyGlob(
24
+ normalizePathScopePath(path),
25
+ patterns?.map(normalizePathScopePath),
26
+ { separator: '/' }
27
+ );
28
+
29
+ const pathGlobArraySchema = z.array(z.string()).readonly();
30
+
31
+ export const pathScopeSchema = z
32
+ .object({
33
+ exclude: pathGlobArraySchema.optional(),
34
+ extensions: z.array(z.string()).readonly().optional(),
35
+ include: pathGlobArraySchema.optional(),
36
+ })
37
+ .strict();
38
+
39
+ export type PathScope = z.output<typeof pathScopeSchema>;
40
+
41
+ export type ScanTargets = Pick<PathScope, 'exclude' | 'extensions'>;
42
+
43
+ const extensionOf = (path: string): string => {
44
+ const normalized = normalizePathScopePath(path);
45
+ const name = normalized.slice(normalized.lastIndexOf('/') + 1);
46
+ const dot = name.lastIndexOf('.');
47
+ return dot <= 0 ? '' : name.slice(dot);
48
+ };
49
+
50
+ const normalizeExtension = (extension: string): string =>
51
+ extension === '' || extension.startsWith('.') ? extension : `.${extension}`;
52
+
53
+ const includedByExtension = (
54
+ path: string,
55
+ extensions: readonly string[] | undefined
56
+ ): boolean =>
57
+ extensions === undefined ||
58
+ extensions.length === 0 ||
59
+ extensions.map(normalizeExtension).includes(extensionOf(path));
60
+
61
+ export const includedByPathScope = (path: string, scope?: PathScope): boolean =>
62
+ (scope?.include === undefined ||
63
+ scope.include.length === 0 ||
64
+ matchesAnyPathGlob(path, scope.include)) &&
65
+ !matchesAnyPathGlob(path, scope?.exclude) &&
66
+ includedByExtension(path, scope?.extensions);
@@ -4,10 +4,14 @@
4
4
  * All functions are runtime-agnostic (Node / Bun compatible).
5
5
  */
6
6
 
7
- import { resolve, relative, normalize, isAbsolute } from 'node:path';
8
-
9
7
  import { PermissionError } from './errors.js';
10
8
  import { Result } from './result.js';
9
+ // Path security guards filesystem access on tooling paths: node:path
10
+ // loads lazily at first use so the core barrel's module graph stays
11
+ // execution-portable on runtimes without node: builtins (TRL-1198).
12
+ import { loadRuntimeBuiltin } from './runtime-builtins.js';
13
+
14
+ const nodePath = () => loadRuntimeBuiltin('node:path');
11
15
 
12
16
  // ---------------------------------------------------------------------------
13
17
  // Internal
@@ -15,6 +19,7 @@ import { Result } from './result.js';
15
19
 
16
20
  /** Returns true when `target` is equal to or a descendant of `base`. */
17
21
  const isWithin = (base: string, target: string): boolean => {
22
+ const { isAbsolute, relative } = nodePath();
18
23
  const rel = relative(base, target);
19
24
  // Empty string means they are the same directory.
20
25
  // A relative path starting with ".." means it escapes.
@@ -39,6 +44,7 @@ export const securePath = (
39
44
  basePath: string,
40
45
  userPath: string
41
46
  ): Result<string, PermissionError> => {
47
+ const { resolve } = nodePath();
42
48
  const base = resolve(basePath);
43
49
  const resolved = resolve(base, userPath);
44
50
 
@@ -61,6 +67,7 @@ export const securePath = (
61
67
  * `basePath`.
62
68
  */
63
69
  export const isPathSafe = (basePath: string, userPath: string): boolean => {
70
+ const { resolve } = nodePath();
64
71
  const base = resolve(basePath);
65
72
  const resolved = resolve(base, userPath);
66
73
  return isWithin(base, resolved);
@@ -74,6 +81,7 @@ export const deriveSafePath = (
74
81
  basePath: string,
75
82
  ...segments: string[]
76
83
  ): Result<string, PermissionError> => {
84
+ const { normalize, resolve } = nodePath();
77
85
  const base = resolve(basePath);
78
86
  const joined = resolve(base, ...segments.map((s) => normalize(s)));
79
87