@ontrails/core 1.0.0-beta.4 → 1.0.0-beta.41

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.
Files changed (247) hide show
  1. package/CHANGELOG.md +412 -12
  2. package/README.md +112 -28
  3. package/package.json +11 -1
  4. package/src/activation-provenance.ts +116 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +227 -0
  7. package/src/activation-source.ts +93 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/compose-batch.ts +69 -0
  11. package/src/compose-schema.ts +36 -0
  12. package/src/context.ts +59 -11
  13. package/src/derive.ts +271 -19
  14. package/src/detours.ts +8 -0
  15. package/src/diagnostics.ts +21 -0
  16. package/src/draft.ts +350 -0
  17. package/src/entity.ts +346 -0
  18. package/src/error-projection.ts +51 -0
  19. package/src/errors.ts +335 -48
  20. package/src/execute.ts +1577 -0
  21. package/src/fire.ts +1172 -0
  22. package/src/glob.ts +81 -0
  23. package/src/index.ts +568 -32
  24. package/src/internal/fork-ctx.ts +69 -0
  25. package/src/layer-projection.ts +193 -0
  26. package/src/layer.ts +43 -6
  27. package/src/observe.ts +361 -0
  28. package/src/path-scope.ts +66 -0
  29. package/src/path-security.ts +11 -3
  30. package/src/permits.ts +12 -0
  31. package/src/queue.ts +163 -0
  32. package/src/redaction/patterns.ts +6 -3
  33. package/src/resilience.ts +11 -11
  34. package/src/resource-config.ts +804 -0
  35. package/src/resource.ts +194 -0
  36. package/src/result.ts +18 -0
  37. package/src/run.ts +76 -0
  38. package/src/runtime-builtins.ts +69 -0
  39. package/src/schedule-runtime.ts +689 -0
  40. package/src/schedule.ts +326 -0
  41. package/src/serialization.ts +92 -55
  42. package/src/sha256.ts +136 -0
  43. package/src/signal-diagnostics.ts +633 -0
  44. package/src/signal-ref.ts +111 -0
  45. package/src/signal.ts +104 -0
  46. package/src/store/accessor-protocol.ts +56 -0
  47. package/src/store/index.ts +4 -0
  48. package/src/structured-examples.ts +248 -0
  49. package/src/surface-derivation.ts +91 -0
  50. package/src/surface-filter.ts +101 -0
  51. package/src/surface-overlay.ts +694 -0
  52. package/src/surface-versioning.ts +42 -0
  53. package/src/topo.ts +762 -38
  54. package/src/tracing.ts +346 -0
  55. package/src/trail-id-glob.ts +15 -0
  56. package/src/trail.ts +1244 -34
  57. package/src/trails/derive-trail.ts +835 -0
  58. package/src/trails/index.ts +9 -0
  59. package/src/trails/ingest.ts +152 -0
  60. package/src/trails-db.ts +212 -0
  61. package/src/transport-error-map.ts +174 -0
  62. package/src/type-utils.ts +87 -0
  63. package/src/types.ts +254 -12
  64. package/src/validate-established-topo.ts +73 -0
  65. package/src/validate-topo.ts +569 -55
  66. package/src/validation.ts +152 -4
  67. package/src/version-marker.ts +716 -0
  68. package/src/version-resolution.ts +308 -0
  69. package/src/version-runtime.ts +120 -0
  70. package/src/webhook.ts +461 -0
  71. package/src/workspace.ts +183 -16
  72. package/src/zod-wrappers.ts +72 -0
  73. package/.turbo/turbo-build.log +0 -1
  74. package/.turbo/turbo-lint.log +0 -3
  75. package/.turbo/turbo-typecheck.log +0 -1
  76. package/dist/adapters.d.ts +0 -39
  77. package/dist/adapters.d.ts.map +0 -1
  78. package/dist/adapters.js +0 -2
  79. package/dist/adapters.js.map +0 -1
  80. package/dist/blob-ref.d.ts +0 -20
  81. package/dist/blob-ref.d.ts.map +0 -1
  82. package/dist/blob-ref.js +0 -22
  83. package/dist/blob-ref.js.map +0 -1
  84. package/dist/branded.d.ts +0 -36
  85. package/dist/branded.d.ts.map +0 -1
  86. package/dist/branded.js +0 -89
  87. package/dist/branded.js.map +0 -1
  88. package/dist/collections.d.ts +0 -31
  89. package/dist/collections.d.ts.map +0 -1
  90. package/dist/collections.js +0 -60
  91. package/dist/collections.js.map +0 -1
  92. package/dist/context.d.ts +0 -10
  93. package/dist/context.d.ts.map +0 -1
  94. package/dist/context.js +0 -15
  95. package/dist/context.js.map +0 -1
  96. package/dist/derive.d.ts +0 -33
  97. package/dist/derive.d.ts.map +0 -1
  98. package/dist/derive.js +0 -125
  99. package/dist/derive.js.map +0 -1
  100. package/dist/errors.d.ts +0 -83
  101. package/dist/errors.d.ts.map +0 -1
  102. package/dist/errors.js +0 -142
  103. package/dist/errors.js.map +0 -1
  104. package/dist/event.d.ts +0 -45
  105. package/dist/event.d.ts.map +0 -1
  106. package/dist/event.js +0 -17
  107. package/dist/event.js.map +0 -1
  108. package/dist/fetch.d.ts +0 -15
  109. package/dist/fetch.d.ts.map +0 -1
  110. package/dist/fetch.js +0 -102
  111. package/dist/fetch.js.map +0 -1
  112. package/dist/guards.d.ts +0 -17
  113. package/dist/guards.d.ts.map +0 -1
  114. package/dist/guards.js +0 -25
  115. package/dist/guards.js.map +0 -1
  116. package/dist/health.d.ts +0 -18
  117. package/dist/health.d.ts.map +0 -1
  118. package/dist/health.js +0 -5
  119. package/dist/health.js.map +0 -1
  120. package/dist/index.d.ts +0 -34
  121. package/dist/index.d.ts.map +0 -1
  122. package/dist/index.js +0 -38
  123. package/dist/index.js.map +0 -1
  124. package/dist/job.d.ts +0 -24
  125. package/dist/job.d.ts.map +0 -1
  126. package/dist/job.js +0 -17
  127. package/dist/job.js.map +0 -1
  128. package/dist/layer.d.ts +0 -17
  129. package/dist/layer.d.ts.map +0 -1
  130. package/dist/layer.js +0 -21
  131. package/dist/layer.js.map +0 -1
  132. package/dist/path-security.d.ts +0 -28
  133. package/dist/path-security.d.ts.map +0 -1
  134. package/dist/path-security.js +0 -63
  135. package/dist/path-security.js.map +0 -1
  136. package/dist/patterns/bulk.d.ts +0 -15
  137. package/dist/patterns/bulk.d.ts.map +0 -1
  138. package/dist/patterns/bulk.js +0 -14
  139. package/dist/patterns/bulk.js.map +0 -1
  140. package/dist/patterns/change.d.ts +0 -10
  141. package/dist/patterns/change.d.ts.map +0 -1
  142. package/dist/patterns/change.js +0 -10
  143. package/dist/patterns/change.js.map +0 -1
  144. package/dist/patterns/date-range.d.ts +0 -10
  145. package/dist/patterns/date-range.d.ts.map +0 -1
  146. package/dist/patterns/date-range.js +0 -10
  147. package/dist/patterns/date-range.js.map +0 -1
  148. package/dist/patterns/index.d.ts +0 -9
  149. package/dist/patterns/index.d.ts.map +0 -1
  150. package/dist/patterns/index.js +0 -9
  151. package/dist/patterns/index.js.map +0 -1
  152. package/dist/patterns/pagination.d.ts +0 -18
  153. package/dist/patterns/pagination.d.ts.map +0 -1
  154. package/dist/patterns/pagination.js +0 -18
  155. package/dist/patterns/pagination.js.map +0 -1
  156. package/dist/patterns/progress.d.ts +0 -11
  157. package/dist/patterns/progress.d.ts.map +0 -1
  158. package/dist/patterns/progress.js +0 -11
  159. package/dist/patterns/progress.js.map +0 -1
  160. package/dist/patterns/sorting.d.ts +0 -13
  161. package/dist/patterns/sorting.d.ts.map +0 -1
  162. package/dist/patterns/sorting.js +0 -10
  163. package/dist/patterns/sorting.js.map +0 -1
  164. package/dist/patterns/status.d.ts +0 -15
  165. package/dist/patterns/status.d.ts.map +0 -1
  166. package/dist/patterns/status.js +0 -9
  167. package/dist/patterns/status.js.map +0 -1
  168. package/dist/patterns/timestamps.d.ts +0 -10
  169. package/dist/patterns/timestamps.d.ts.map +0 -1
  170. package/dist/patterns/timestamps.js +0 -10
  171. package/dist/patterns/timestamps.js.map +0 -1
  172. package/dist/redaction/index.d.ts +0 -4
  173. package/dist/redaction/index.d.ts.map +0 -1
  174. package/dist/redaction/index.js +0 -3
  175. package/dist/redaction/index.js.map +0 -1
  176. package/dist/redaction/patterns.d.ts +0 -9
  177. package/dist/redaction/patterns.d.ts.map +0 -1
  178. package/dist/redaction/patterns.js +0 -39
  179. package/dist/redaction/patterns.js.map +0 -1
  180. package/dist/redaction/redactor.d.ts +0 -27
  181. package/dist/redaction/redactor.d.ts.map +0 -1
  182. package/dist/redaction/redactor.js +0 -89
  183. package/dist/redaction/redactor.js.map +0 -1
  184. package/dist/resilience.d.ts +0 -34
  185. package/dist/resilience.d.ts.map +0 -1
  186. package/dist/resilience.js +0 -164
  187. package/dist/resilience.js.map +0 -1
  188. package/dist/result.d.ts +0 -57
  189. package/dist/result.d.ts.map +0 -1
  190. package/dist/result.js +0 -156
  191. package/dist/result.js.map +0 -1
  192. package/dist/serialization.d.ts +0 -27
  193. package/dist/serialization.d.ts.map +0 -1
  194. package/dist/serialization.js +0 -153
  195. package/dist/serialization.js.map +0 -1
  196. package/dist/topo.d.ts +0 -16
  197. package/dist/topo.d.ts.map +0 -1
  198. package/dist/topo.js +0 -66
  199. package/dist/topo.js.map +0 -1
  200. package/dist/trail.d.ts +0 -89
  201. package/dist/trail.d.ts.map +0 -1
  202. package/dist/trail.js +0 -18
  203. package/dist/trail.js.map +0 -1
  204. package/dist/types.d.ts +0 -46
  205. package/dist/types.d.ts.map +0 -1
  206. package/dist/types.js +0 -2
  207. package/dist/types.js.map +0 -1
  208. package/dist/validate-topo.d.ts +0 -24
  209. package/dist/validate-topo.d.ts.map +0 -1
  210. package/dist/validate-topo.js +0 -158
  211. package/dist/validate-topo.js.map +0 -1
  212. package/dist/validation.d.ts +0 -27
  213. package/dist/validation.d.ts.map +0 -1
  214. package/dist/validation.js +0 -134
  215. package/dist/validation.js.map +0 -1
  216. package/dist/workspace.d.ts +0 -25
  217. package/dist/workspace.d.ts.map +0 -1
  218. package/dist/workspace.js +0 -57
  219. package/dist/workspace.js.map +0 -1
  220. package/src/__tests__/blob-ref.test.ts +0 -103
  221. package/src/__tests__/branded.test.ts +0 -148
  222. package/src/__tests__/collections.test.ts +0 -126
  223. package/src/__tests__/context.test.ts +0 -66
  224. package/src/__tests__/derive.test.ts +0 -203
  225. package/src/__tests__/errors.test.ts +0 -309
  226. package/src/__tests__/event.test.ts +0 -82
  227. package/src/__tests__/fetch.test.ts +0 -217
  228. package/src/__tests__/guards.test.ts +0 -102
  229. package/src/__tests__/job.test.ts +0 -98
  230. package/src/__tests__/layer.test.ts +0 -212
  231. package/src/__tests__/path-security.test.ts +0 -114
  232. package/src/__tests__/patterns.test.ts +0 -273
  233. package/src/__tests__/redaction.test.ts +0 -244
  234. package/src/__tests__/resilience.test.ts +0 -246
  235. package/src/__tests__/result.test.ts +0 -155
  236. package/src/__tests__/serialization.test.ts +0 -401
  237. package/src/__tests__/topo.test.ts +0 -181
  238. package/src/__tests__/trail.test.ts +0 -217
  239. package/src/__tests__/validate-topo.test.ts +0 -278
  240. package/src/__tests__/validation.test.ts +0 -283
  241. package/src/__tests__/workspace.test.ts +0 -183
  242. package/src/adapters.ts +0 -68
  243. package/src/event.ts +0 -77
  244. package/src/health.ts +0 -23
  245. package/src/job.ts +0 -20
  246. package/tsconfig.json +0 -9
  247. package/tsconfig.tsbuildinfo +0 -1
package/src/derive.ts CHANGED
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * Schema-driven field derivation for @ontrails/core
3
3
  *
4
- * Introspects Zod v4 schemas to produce a surface-agnostic Field[] descriptor
5
- * that UI layers (CLI prompts, web forms, etc.) can consume.
4
+ * Introspects Zod v4 schemas to produce a runtime-agnostic Field[] descriptor
5
+ * that UI consumers (CLI prompts, web forms, etc.) can consume.
6
6
  */
7
7
 
8
8
  import type { z } from 'zod';
9
9
 
10
+ import { ValidationError } from './errors.js';
11
+
10
12
  // ---------------------------------------------------------------------------
11
13
  // Public types
12
14
  // ---------------------------------------------------------------------------
13
15
 
14
- /** A surface-agnostic field descriptor derived from a Zod schema. */
16
+ /** A runtime-agnostic field descriptor derived from a Zod schema. */
15
17
  export interface Field {
16
18
  readonly name: string;
17
19
  readonly type:
@@ -48,6 +50,57 @@ export interface FieldOverride {
48
50
  | undefined;
49
51
  }
50
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
+
51
104
  // ---------------------------------------------------------------------------
52
105
  // Zod v4 internals accessor
53
106
  // ---------------------------------------------------------------------------
@@ -92,7 +145,7 @@ const propagateDescription = (
92
145
  }
93
146
  };
94
147
 
95
- /** 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. */
96
149
  const unwrapStep = (
97
150
  current: ZodInternals,
98
151
  state: {
@@ -102,10 +155,16 @@ const unwrapStep = (
102
155
  }
103
156
  ): ZodInternals | null => {
104
157
  const defType = current._zod.def['type'] as string;
105
- if (defType !== 'optional' && defType !== 'default') {
158
+ if (
159
+ defType !== 'optional' &&
160
+ defType !== 'default' &&
161
+ defType !== 'readonly'
162
+ ) {
106
163
  return null;
107
164
  }
108
- state.required = false;
165
+ if (defType !== 'readonly') {
166
+ state.required = false;
167
+ }
109
168
  if (defType === 'default') {
110
169
  state.defaultValue = current._zod.def['defaultValue'];
111
170
  }
@@ -140,14 +199,21 @@ interface DerivedFieldType {
140
199
  type: Field['type'];
141
200
  }
142
201
 
143
- const fieldTypeByDef: Record<string, (s: ZodInternals) => DerivedFieldType> = {
202
+ const fieldTypeByDef: Record<
203
+ string,
204
+ (s: ZodInternals) => DerivedFieldType | null
205
+ > = {
144
206
  array: (s) => {
145
207
  const element = s._zod.def['element'] as unknown as ZodInternals;
146
- const elementType = element._zod.def['type'] as string;
208
+ const { inner } = unwrap(element);
209
+ const elementType = inner._zod.def['type'] as string;
147
210
  if (elementType === 'enum') {
148
- const entries = element._zod.def['entries'] as Record<string, string>;
211
+ const entries = inner._zod.def['entries'] as Record<string, string>;
149
212
  return { options: Object.values(entries), type: 'multiselect' };
150
213
  }
214
+ if (elementType !== 'number' && elementType !== 'string') {
215
+ return null;
216
+ }
151
217
  return {
152
218
  options: undefined,
153
219
  type: elementType === 'number' ? 'number[]' : 'string[]',
@@ -163,10 +229,10 @@ const fieldTypeByDef: Record<string, (s: ZodInternals) => DerivedFieldType> = {
163
229
  };
164
230
 
165
231
  /** Derive field type and raw options from the unwrapped Zod def. */
166
- const deriveFieldType = (s: ZodInternals): DerivedFieldType => {
232
+ const deriveFieldType = (s: ZodInternals): DerivedFieldType | null => {
167
233
  const defType = s._zod.def['type'] as string;
168
234
  const derive = fieldTypeByDef[defType];
169
- return derive ? derive(s) : { options: undefined, type: 'string' };
235
+ return derive ? derive(s) : null;
170
236
  };
171
237
 
172
238
  /** Build options array, merging with overrides when present. */
@@ -189,29 +255,213 @@ const buildOptions = (
189
255
  });
190
256
  };
191
257
 
192
- // ---------------------------------------------------------------------------
193
- // Public API
194
- // ---------------------------------------------------------------------------
258
+ /**
259
+ * Derive the canonical ordered CLI path from a trail ID.
260
+ *
261
+ * @throws {ValidationError} if the trail ID contains empty segments (e.g. consecutive dots).
262
+ */
263
+ export const deriveCliPath = (trailId: string): string[] => {
264
+ const segments = trailId.split('.');
265
+ const emptyIndex = segments.findIndex((s) => s.length === 0);
266
+ if (emptyIndex !== -1) {
267
+ throw new ValidationError(
268
+ `Trail ID "${trailId}" contains an empty segment at position ${emptyIndex}`
269
+ );
270
+ }
271
+ return segments;
272
+ };
273
+
274
+ const hasWhitespace = (value: string): boolean => /\s/.test(value);
275
+
276
+ const validateCliSegment = (segment: string, context: string): string => {
277
+ const normalized = segment.trim();
278
+ if (normalized.length === 0) {
279
+ throw new ValidationError(`${context} cannot contain empty segments`);
280
+ }
281
+ if (hasWhitespace(normalized)) {
282
+ throw new ValidationError(
283
+ `${context} segment "${segment}" cannot contain whitespace`
284
+ );
285
+ }
286
+ return normalized;
287
+ };
288
+
289
+ const splitCliPathString = (value: string, context: string): string[] => {
290
+ const segments = value
291
+ .trim()
292
+ .split(/\s+/)
293
+ .filter((segment) => segment.length > 0);
294
+ if (segments.length === 0) {
295
+ throw new ValidationError(`${context} cannot be empty`);
296
+ }
297
+ return segments.map((segment) => validateCliSegment(segment, context));
298
+ };
299
+
300
+ /** Normalize an authored CLI command path. */
301
+ export const normalizeCliCommandPath = (
302
+ value: CliCommandPathInput,
303
+ context = 'CLI command path'
304
+ ): readonly string[] =>
305
+ typeof value === 'string'
306
+ ? splitCliPathString(value, context)
307
+ : value.map((segment) => validateCliSegment(segment, context));
308
+
309
+ const isTrailCliProjection = (
310
+ value: CliCommandPathInput | TrailCliProjection
311
+ ): value is TrailCliProjection =>
312
+ typeof value !== 'string' &&
313
+ !Array.isArray(value) &&
314
+ value !== null &&
315
+ typeof value === 'object';
316
+
317
+ const trailCliProjectionFor = (
318
+ trail: TrailCliProjectionInput
319
+ ): TrailCliProjection | undefined => {
320
+ if (trail.cli === undefined) {
321
+ return undefined;
322
+ }
323
+ return isTrailCliProjection(trail.cli) ? trail.cli : { path: trail.cli };
324
+ };
325
+
326
+ const deriveCanonicalCliRoute = (
327
+ trail: TrailCliProjectionInput
328
+ ): CliCommandRoute => {
329
+ const projection = trailCliProjectionFor(trail);
330
+ const path =
331
+ projection?.path === undefined
332
+ ? deriveCliPath(trail.id)
333
+ : normalizeCliCommandPath(
334
+ projection.path,
335
+ `CLI command path for trail "${trail.id}"`
336
+ );
337
+ return {
338
+ kind: 'canonical',
339
+ path,
340
+ source: projection?.path === undefined ? 'derived' : 'trail',
341
+ target: trail.id,
342
+ };
343
+ };
344
+
345
+ const normalizeCliAlias = ({
346
+ alias,
347
+ canonicalPath,
348
+ source,
349
+ target,
350
+ }: {
351
+ readonly alias: CliCommandAliasInput;
352
+ readonly canonicalPath: readonly string[];
353
+ readonly source: Extract<CliCommandRouteSource, 'surface' | 'trail'>;
354
+ readonly target: string;
355
+ }): CliCommandRoute => {
356
+ const context = `CLI command alias for trail "${target}"`;
357
+ if (typeof alias === 'string') {
358
+ const segment = alias.trim();
359
+ if (segment.length === 0) {
360
+ throw new ValidationError(`${context} cannot be empty`);
361
+ }
362
+ if (hasWhitespace(segment)) {
363
+ throw new ValidationError(
364
+ `${context} must be a single command segment; use a string array for absolute paths`
365
+ );
366
+ }
367
+ return {
368
+ kind: 'alias',
369
+ path: [
370
+ ...canonicalPath.slice(0, -1),
371
+ validateCliSegment(segment, context),
372
+ ],
373
+ source,
374
+ target,
375
+ };
376
+ }
377
+ return {
378
+ kind: 'alias',
379
+ path: normalizeCliCommandPath(alias, context),
380
+ source,
381
+ target,
382
+ };
383
+ };
195
384
 
196
385
  /**
197
- * Derive a surface-agnostic Field[] from a Zod object schema.
386
+ * Convert app name + trail ID to an MCP-safe tool name.
198
387
  *
199
- * Uses Zod v4's `_zod.def` for introspection. Returns fields sorted by name.
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
+ * ```
200
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
+
406
+ /** Derive resolved CLI command routes for one trail. */
407
+ export const deriveTrailCliCommandProjection = (
408
+ trail: TrailCliProjectionInput,
409
+ options?: DeriveTrailCliCommandProjectionOptions
410
+ ): TrailCliCommandProjection => {
411
+ const canonical = deriveCanonicalCliRoute(trail);
412
+ const projection = trailCliProjectionFor(trail);
413
+ const trailAliases =
414
+ projection?.aliases?.map((alias) =>
415
+ normalizeCliAlias({
416
+ alias,
417
+ canonicalPath: canonical.path,
418
+ source: 'trail',
419
+ target: trail.id,
420
+ })
421
+ ) ?? [];
422
+ const surfaceAliases =
423
+ options?.aliases?.map((alias) =>
424
+ normalizeCliAlias({
425
+ alias,
426
+ canonicalPath: canonical.path,
427
+ source: options.aliasSource ?? 'surface',
428
+ target: trail.id,
429
+ })
430
+ ) ?? [];
431
+
432
+ return {
433
+ path: canonical.path,
434
+ routes: [canonical, ...trailAliases, ...surfaceAliases],
435
+ };
436
+ };
437
+
438
+ // ---------------------------------------------------------------------------
439
+ // Public API
440
+ // ---------------------------------------------------------------------------
441
+
201
442
  /** Derive a single field from a shape entry. */
202
443
  const deriveField = (
203
444
  key: string,
204
445
  value: ZodInternals,
205
446
  overrides?: Record<string, FieldOverride>
206
- ): Field => {
447
+ ): Field | null => {
207
448
  const { inner, required, defaultValue, description } = unwrap(value);
208
- const { type, options: rawOptions } = deriveFieldType(inner);
449
+ const derived = deriveFieldType(inner);
450
+ if (!derived) {
451
+ return null;
452
+ }
453
+ const { type, options: rawOptions } = derived;
209
454
  const override = overrides?.[key];
210
455
  const label = override?.label ?? description ?? humanize(key);
211
456
  const options = buildOptions(rawOptions, override?.options);
212
457
  return { default: defaultValue, label, name: key, options, required, type };
213
458
  };
214
459
 
460
+ /**
461
+ * Derive a runtime-agnostic Field[] from a Zod object schema.
462
+ *
463
+ * Uses Zod v4's `_zod.def` for introspection. Returns fields sorted by name.
464
+ */
215
465
  export const deriveFields = (
216
466
  schema: z.ZodType,
217
467
  overrides?: Record<string, FieldOverride>
@@ -229,5 +479,7 @@ export const deriveFields = (
229
479
  const fields = Object.entries(shape).map(([key, value]) =>
230
480
  deriveField(key, value, overrides)
231
481
  );
232
- return fields.toSorted((a, b) => a.name.localeCompare(b.name));
482
+ return fields
483
+ .filter((field): field is Field => field !== null)
484
+ .toSorted((a, b) => a.name.localeCompare(b.name));
233
485
  };
package/src/detours.ts ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Hard upper bound for detour recovery attempts.
3
+ *
4
+ * Execution and derived surface/topo projections both clamp declared detour
5
+ * attempts to this value so runtime behavior and inspectable contracts stay in
6
+ * lockstep.
7
+ */
8
+ export const DETOUR_MAX_ATTEMPTS_CAP = 5;
@@ -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
+ }