@ontrails/core 1.0.0-beta.14 → 1.0.0-beta.16

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 (314) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/README.md +73 -24
  3. package/package.json +11 -4
  4. package/src/activation-provenance.ts +111 -0
  5. package/src/activation-source-compatibility.ts +430 -0
  6. package/src/activation-source-projection.ts +221 -0
  7. package/src/activation-source.ts +91 -0
  8. package/src/blob-ref.ts +51 -0
  9. package/src/branded.ts +1 -1
  10. package/src/context.ts +28 -4
  11. package/src/contour.ts +344 -0
  12. package/src/cross-batch.ts +69 -0
  13. package/src/cross-schema.ts +36 -0
  14. package/src/detours.ts +8 -0
  15. package/src/draft.ts +60 -52
  16. package/src/error-projection.ts +51 -0
  17. package/src/errors.ts +245 -48
  18. package/src/execute.ts +1250 -40
  19. package/src/fire.ts +1169 -0
  20. package/src/index.ts +383 -49
  21. package/src/internal/fork-ctx.ts +69 -0
  22. package/src/layer-projection.ts +192 -0
  23. package/src/layer.ts +81 -0
  24. package/src/observe.ts +361 -0
  25. package/src/path-security.ts +1 -1
  26. package/src/permits.ts +12 -0
  27. package/src/redaction/patterns.ts +6 -3
  28. package/src/resilience.ts +11 -11
  29. package/src/resource-config.ts +792 -0
  30. package/src/resource.ts +175 -0
  31. package/src/result.ts +18 -0
  32. package/src/run.ts +7 -6
  33. package/src/schedule-runtime.ts +689 -0
  34. package/src/schedule.ts +324 -0
  35. package/src/serialization.ts +90 -55
  36. package/src/signal-diagnostics.ts +633 -0
  37. package/src/signal-ref.ts +87 -0
  38. package/src/signal.ts +37 -0
  39. package/src/store/accessor-protocol.ts +56 -0
  40. package/src/store/index.ts +4 -0
  41. package/src/structured-examples.ts +245 -0
  42. package/src/surface-derivation.ts +91 -0
  43. package/src/surface-filter.ts +176 -0
  44. package/src/topo.ts +710 -59
  45. package/src/tracing.ts +345 -0
  46. package/src/trail.ts +347 -33
  47. package/src/trails/derive-trail.ts +842 -0
  48. package/src/trails/index.ts +9 -0
  49. package/src/trails/ingest.ts +138 -0
  50. package/src/{internal/trails-db.ts → trails-db.ts} +50 -31
  51. package/src/transport-error-map.ts +174 -0
  52. package/src/type-utils.ts +17 -2
  53. package/src/types.ts +208 -17
  54. package/src/validate-established-topo.ts +8 -2
  55. package/src/validate-topo.ts +244 -13
  56. package/src/validation.ts +58 -1
  57. package/src/webhook.ts +276 -0
  58. package/src/workspace.ts +1 -1
  59. package/src/zod-wrappers.ts +72 -0
  60. package/.turbo/turbo-build.log +0 -1
  61. package/.turbo/turbo-lint.log +0 -3
  62. package/.turbo/turbo-typecheck.log +0 -1
  63. package/dist/adapters.d.ts +0 -39
  64. package/dist/adapters.d.ts.map +0 -1
  65. package/dist/adapters.js +0 -2
  66. package/dist/adapters.js.map +0 -1
  67. package/dist/blob-ref.d.ts +0 -20
  68. package/dist/blob-ref.d.ts.map +0 -1
  69. package/dist/blob-ref.js +0 -22
  70. package/dist/blob-ref.js.map +0 -1
  71. package/dist/branded.d.ts +0 -36
  72. package/dist/branded.d.ts.map +0 -1
  73. package/dist/branded.js +0 -89
  74. package/dist/branded.js.map +0 -1
  75. package/dist/collections.d.ts +0 -31
  76. package/dist/collections.d.ts.map +0 -1
  77. package/dist/collections.js +0 -60
  78. package/dist/collections.js.map +0 -1
  79. package/dist/context.d.ts +0 -10
  80. package/dist/context.d.ts.map +0 -1
  81. package/dist/context.js +0 -21
  82. package/dist/context.js.map +0 -1
  83. package/dist/derive.d.ts +0 -44
  84. package/dist/derive.d.ts.map +0 -1
  85. package/dist/derive.js +0 -149
  86. package/dist/derive.js.map +0 -1
  87. package/dist/dispatch.d.ts +0 -27
  88. package/dist/dispatch.d.ts.map +0 -1
  89. package/dist/dispatch.js +0 -34
  90. package/dist/dispatch.js.map +0 -1
  91. package/dist/draft.d.ts +0 -28
  92. package/dist/draft.d.ts.map +0 -1
  93. package/dist/draft.js +0 -156
  94. package/dist/draft.js.map +0 -1
  95. package/dist/errors.d.ts +0 -83
  96. package/dist/errors.d.ts.map +0 -1
  97. package/dist/errors.js +0 -142
  98. package/dist/errors.js.map +0 -1
  99. package/dist/event.d.ts +0 -8
  100. package/dist/event.d.ts.map +0 -1
  101. package/dist/event.js +0 -7
  102. package/dist/event.js.map +0 -1
  103. package/dist/execute.d.ts +0 -35
  104. package/dist/execute.d.ts.map +0 -1
  105. package/dist/execute.js +0 -90
  106. package/dist/execute.js.map +0 -1
  107. package/dist/fetch.d.ts +0 -15
  108. package/dist/fetch.d.ts.map +0 -1
  109. package/dist/fetch.js +0 -102
  110. package/dist/fetch.js.map +0 -1
  111. package/dist/gate.d.ts +0 -17
  112. package/dist/gate.d.ts.map +0 -1
  113. package/dist/gate.js +0 -21
  114. package/dist/gate.js.map +0 -1
  115. package/dist/guards.d.ts +0 -17
  116. package/dist/guards.d.ts.map +0 -1
  117. package/dist/guards.js +0 -25
  118. package/dist/guards.js.map +0 -1
  119. package/dist/health.d.ts +0 -18
  120. package/dist/health.d.ts.map +0 -1
  121. package/dist/health.js +0 -5
  122. package/dist/health.js.map +0 -1
  123. package/dist/index.d.ts +0 -48
  124. package/dist/index.d.ts.map +0 -1
  125. package/dist/index.js +0 -51
  126. package/dist/index.js.map +0 -1
  127. package/dist/internal/topo-saves.d.ts +0 -47
  128. package/dist/internal/topo-saves.d.ts.map +0 -1
  129. package/dist/internal/topo-saves.js +0 -310
  130. package/dist/internal/topo-saves.js.map +0 -1
  131. package/dist/internal/topo-store-read.d.ts +0 -67
  132. package/dist/internal/topo-store-read.d.ts.map +0 -1
  133. package/dist/internal/topo-store-read.js +0 -222
  134. package/dist/internal/topo-store-read.js.map +0 -1
  135. package/dist/internal/topo-store.d.ts +0 -12
  136. package/dist/internal/topo-store.d.ts.map +0 -1
  137. package/dist/internal/topo-store.js +0 -571
  138. package/dist/internal/topo-store.js.map +0 -1
  139. package/dist/internal/trails-db.d.ts +0 -16
  140. package/dist/internal/trails-db.d.ts.map +0 -1
  141. package/dist/internal/trails-db.js +0 -118
  142. package/dist/internal/trails-db.js.map +0 -1
  143. package/dist/job.d.ts +0 -24
  144. package/dist/job.d.ts.map +0 -1
  145. package/dist/job.js +0 -17
  146. package/dist/job.js.map +0 -1
  147. package/dist/layer.d.ts +0 -6
  148. package/dist/layer.d.ts.map +0 -1
  149. package/dist/layer.js +0 -4
  150. package/dist/layer.js.map +0 -1
  151. package/dist/path-security.d.ts +0 -28
  152. package/dist/path-security.d.ts.map +0 -1
  153. package/dist/path-security.js +0 -63
  154. package/dist/path-security.js.map +0 -1
  155. package/dist/patterns/bulk.d.ts +0 -15
  156. package/dist/patterns/bulk.d.ts.map +0 -1
  157. package/dist/patterns/bulk.js +0 -14
  158. package/dist/patterns/bulk.js.map +0 -1
  159. package/dist/patterns/change.d.ts +0 -10
  160. package/dist/patterns/change.d.ts.map +0 -1
  161. package/dist/patterns/change.js +0 -10
  162. package/dist/patterns/change.js.map +0 -1
  163. package/dist/patterns/date-range.d.ts +0 -10
  164. package/dist/patterns/date-range.d.ts.map +0 -1
  165. package/dist/patterns/date-range.js +0 -10
  166. package/dist/patterns/date-range.js.map +0 -1
  167. package/dist/patterns/index.d.ts +0 -9
  168. package/dist/patterns/index.d.ts.map +0 -1
  169. package/dist/patterns/index.js +0 -9
  170. package/dist/patterns/index.js.map +0 -1
  171. package/dist/patterns/pagination.d.ts +0 -18
  172. package/dist/patterns/pagination.d.ts.map +0 -1
  173. package/dist/patterns/pagination.js +0 -18
  174. package/dist/patterns/pagination.js.map +0 -1
  175. package/dist/patterns/progress.d.ts +0 -11
  176. package/dist/patterns/progress.d.ts.map +0 -1
  177. package/dist/patterns/progress.js +0 -11
  178. package/dist/patterns/progress.js.map +0 -1
  179. package/dist/patterns/sorting.d.ts +0 -13
  180. package/dist/patterns/sorting.d.ts.map +0 -1
  181. package/dist/patterns/sorting.js +0 -10
  182. package/dist/patterns/sorting.js.map +0 -1
  183. package/dist/patterns/status.d.ts +0 -15
  184. package/dist/patterns/status.d.ts.map +0 -1
  185. package/dist/patterns/status.js +0 -9
  186. package/dist/patterns/status.js.map +0 -1
  187. package/dist/patterns/timestamps.d.ts +0 -10
  188. package/dist/patterns/timestamps.d.ts.map +0 -1
  189. package/dist/patterns/timestamps.js +0 -10
  190. package/dist/patterns/timestamps.js.map +0 -1
  191. package/dist/provision-config.d.ts +0 -22
  192. package/dist/provision-config.d.ts.map +0 -1
  193. package/dist/provision-config.js +0 -210
  194. package/dist/provision-config.js.map +0 -1
  195. package/dist/provision.d.ts +0 -71
  196. package/dist/provision.d.ts.map +0 -1
  197. package/dist/provision.js +0 -56
  198. package/dist/provision.js.map +0 -1
  199. package/dist/redaction/index.d.ts +0 -4
  200. package/dist/redaction/index.d.ts.map +0 -1
  201. package/dist/redaction/index.js +0 -3
  202. package/dist/redaction/index.js.map +0 -1
  203. package/dist/redaction/patterns.d.ts +0 -9
  204. package/dist/redaction/patterns.d.ts.map +0 -1
  205. package/dist/redaction/patterns.js +0 -39
  206. package/dist/redaction/patterns.js.map +0 -1
  207. package/dist/redaction/redactor.d.ts +0 -27
  208. package/dist/redaction/redactor.d.ts.map +0 -1
  209. package/dist/redaction/redactor.js +0 -89
  210. package/dist/redaction/redactor.js.map +0 -1
  211. package/dist/resilience.d.ts +0 -34
  212. package/dist/resilience.d.ts.map +0 -1
  213. package/dist/resilience.js +0 -164
  214. package/dist/resilience.js.map +0 -1
  215. package/dist/result.d.ts +0 -57
  216. package/dist/result.d.ts.map +0 -1
  217. package/dist/result.js +0 -156
  218. package/dist/result.js.map +0 -1
  219. package/dist/run.d.ts +0 -27
  220. package/dist/run.d.ts.map +0 -1
  221. package/dist/run.js +0 -34
  222. package/dist/run.js.map +0 -1
  223. package/dist/serialization.d.ts +0 -27
  224. package/dist/serialization.d.ts.map +0 -1
  225. package/dist/serialization.js +0 -153
  226. package/dist/serialization.js.map +0 -1
  227. package/dist/service-config.d.ts +0 -22
  228. package/dist/service-config.d.ts.map +0 -1
  229. package/dist/service-config.js +0 -210
  230. package/dist/service-config.js.map +0 -1
  231. package/dist/service.d.ts +0 -71
  232. package/dist/service.d.ts.map +0 -1
  233. package/dist/service.js +0 -56
  234. package/dist/service.js.map +0 -1
  235. package/dist/signal.d.ts +0 -33
  236. package/dist/signal.d.ts.map +0 -1
  237. package/dist/signal.js +0 -17
  238. package/dist/signal.js.map +0 -1
  239. package/dist/topo-store.d.ts +0 -48
  240. package/dist/topo-store.d.ts.map +0 -1
  241. package/dist/topo-store.js +0 -175
  242. package/dist/topo-store.js.map +0 -1
  243. package/dist/topo.d.ts +0 -25
  244. package/dist/topo.d.ts.map +0 -1
  245. package/dist/topo.js +0 -112
  246. package/dist/topo.js.map +0 -1
  247. package/dist/trail.d.ts +0 -96
  248. package/dist/trail.d.ts.map +0 -1
  249. package/dist/trail.js +0 -20
  250. package/dist/trail.js.map +0 -1
  251. package/dist/type-utils.d.ts +0 -24
  252. package/dist/type-utils.d.ts.map +0 -1
  253. package/dist/type-utils.js +0 -12
  254. package/dist/type-utils.js.map +0 -1
  255. package/dist/types.d.ts +0 -71
  256. package/dist/types.d.ts.map +0 -1
  257. package/dist/types.js +0 -3
  258. package/dist/types.js.map +0 -1
  259. package/dist/validate-established-topo.d.ts +0 -76
  260. package/dist/validate-established-topo.d.ts.map +0 -1
  261. package/dist/validate-established-topo.js +0 -43
  262. package/dist/validate-established-topo.js.map +0 -1
  263. package/dist/validate-topo.d.ts +0 -24
  264. package/dist/validate-topo.d.ts.map +0 -1
  265. package/dist/validate-topo.js +0 -176
  266. package/dist/validate-topo.js.map +0 -1
  267. package/dist/validation.d.ts +0 -27
  268. package/dist/validation.d.ts.map +0 -1
  269. package/dist/validation.js +0 -165
  270. package/dist/validation.js.map +0 -1
  271. package/dist/workspace.d.ts +0 -25
  272. package/dist/workspace.d.ts.map +0 -1
  273. package/dist/workspace.js +0 -57
  274. package/dist/workspace.js.map +0 -1
  275. package/src/__tests__/blob-ref.test.ts +0 -103
  276. package/src/__tests__/branded.test.ts +0 -148
  277. package/src/__tests__/collections.test.ts +0 -126
  278. package/src/__tests__/context.test.ts +0 -77
  279. package/src/__tests__/derive.test.ts +0 -260
  280. package/src/__tests__/errors.test.ts +0 -309
  281. package/src/__tests__/execute.test.ts +0 -525
  282. package/src/__tests__/fetch.test.ts +0 -217
  283. package/src/__tests__/gate.test.ts +0 -125
  284. package/src/__tests__/guards.test.ts +0 -102
  285. package/src/__tests__/path-security.test.ts +0 -114
  286. package/src/__tests__/patterns.test.ts +0 -273
  287. package/src/__tests__/redaction.test.ts +0 -244
  288. package/src/__tests__/resilience.test.ts +0 -246
  289. package/src/__tests__/result.test.ts +0 -155
  290. package/src/__tests__/run.test.ts +0 -181
  291. package/src/__tests__/serialization.test.ts +0 -401
  292. package/src/__tests__/service-config.test.ts +0 -228
  293. package/src/__tests__/service.test.ts +0 -197
  294. package/src/__tests__/signal.test.ts +0 -82
  295. package/src/__tests__/topo-store-read.test.ts +0 -251
  296. package/src/__tests__/topo-store.test.ts +0 -469
  297. package/src/__tests__/topo.test.ts +0 -277
  298. package/src/__tests__/trail-permit.test.ts +0 -60
  299. package/src/__tests__/trail.test.ts +0 -261
  300. package/src/__tests__/trails-db.test.ts +0 -191
  301. package/src/__tests__/type-utils.test.ts +0 -90
  302. package/src/__tests__/validate-topo.test.ts +0 -489
  303. package/src/__tests__/validation.test.ts +0 -336
  304. package/src/__tests__/workspace.test.ts +0 -183
  305. package/src/event.ts +0 -15
  306. package/src/gate.ts +0 -44
  307. package/src/internal/topo-saves.ts +0 -429
  308. package/src/internal/topo-store-read.ts +0 -473
  309. package/src/internal/topo-store.ts +0 -1087
  310. package/src/provision-config.ts +0 -373
  311. package/src/provision.ts +0 -148
  312. package/src/topo-store.ts +0 -301
  313. package/tsconfig.json +0 -9
  314. package/tsconfig.tsbuildinfo +0 -1
package/src/signal.ts CHANGED
@@ -4,6 +4,29 @@
4
4
 
5
5
  import type { z } from 'zod';
6
6
 
7
+ const formatExampleIssues = (issues: readonly z.core.$ZodIssue[]): string =>
8
+ issues
9
+ .map((issue) => {
10
+ const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
11
+ return `${path}: ${issue.message}`;
12
+ })
13
+ .join('; ');
14
+
15
+ const assertSignalExamples = <T>(
16
+ id: string,
17
+ payload: z.ZodType<T>,
18
+ examples: readonly T[]
19
+ ): void => {
20
+ for (const [index, example] of examples.entries()) {
21
+ const parsed = payload.safeParse(example);
22
+ if (!parsed.success) {
23
+ throw new TypeError(
24
+ `signal("${id}") example ${index} is invalid: ${formatExampleIssues(parsed.error.issues)}`
25
+ );
26
+ }
27
+ }
28
+ };
29
+
7
30
  // ---------------------------------------------------------------------------
8
31
  // Spec (input to the factory)
9
32
  // ---------------------------------------------------------------------------
@@ -11,6 +34,8 @@ import type { z } from 'zod';
11
34
  export interface SignalSpec<T> {
12
35
  readonly payload: z.ZodType<T>;
13
36
  readonly description?: string | undefined;
37
+ /** Example payloads validated against the signal payload schema. */
38
+ readonly examples?: readonly T[] | undefined;
14
39
  readonly meta?: Readonly<Record<string, unknown>> | undefined;
15
40
  /** Trail IDs that produce this signal (e.g. the trails it originates from). */
16
41
  readonly from?: readonly string[] | undefined;
@@ -25,6 +50,8 @@ export interface Signal<T> {
25
50
  readonly kind: 'signal';
26
51
  readonly payload: z.ZodType<T>;
27
52
  readonly description?: string | undefined;
53
+ /** Example payloads validated against the signal payload schema. */
54
+ readonly examples?: readonly T[] | undefined;
28
55
  readonly meta?: Readonly<Record<string, unknown>> | undefined;
29
56
  /** Trail IDs that produce this signal (e.g. the trails it originates from). */
30
57
  readonly from?: readonly string[] | undefined;
@@ -51,8 +78,18 @@ export function signal<T>(
51
78
  const resolvedId = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
52
79
  // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
53
80
  const resolvedSpec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
81
+ if (resolvedSpec.examples !== undefined) {
82
+ assertSignalExamples(
83
+ resolvedId,
84
+ resolvedSpec.payload,
85
+ resolvedSpec.examples
86
+ );
87
+ }
54
88
  return Object.freeze({
55
89
  description: resolvedSpec.description,
90
+ examples: resolvedSpec.examples
91
+ ? Object.freeze([...resolvedSpec.examples])
92
+ : undefined,
56
93
  from: resolvedSpec.from ? Object.freeze([...resolvedSpec.from]) : undefined,
57
94
  id: resolvedId,
58
95
  kind: 'signal' as const,
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Structural accessor protocol used by `deriveTrail()` to synthesize default
3
+ * blazes for standard CRUD operations without depending on `@ontrails/store`.
4
+ *
5
+ * @remarks
6
+ * This type is intentionally minimal and structural. `@ontrails/store`'s
7
+ * `StoreAccessor` / `StoreTableAccessor` interfaces satisfy this protocol
8
+ * through a compile-time extends check (see `packages/store/src/types.ts`).
9
+ * Keeping the protocol in core avoids a core → store dependency while still
10
+ * allowing the derivation helper to call accessors by convention.
11
+ *
12
+ * Required methods (`get`, `list`, `upsert`, `remove`) match the
13
+ * backend-agnostic write contract every bound store must expose. Optional
14
+ * methods (`insert`, `update`) are declared by tabular adapters that
15
+ * distinguish create-only and patch-only operations from the generalized
16
+ * `upsert` contract.
17
+ */
18
+ export interface StoreAccessorProtocol<
19
+ TInput,
20
+ TEntity,
21
+ TId,
22
+ TFilters = unknown,
23
+ > {
24
+ /** Retrieve a single entity by identity. Returns `null` when not found. */
25
+ get(id: TId): Promise<TEntity | null>;
26
+ /** List entities, optionally filtered. Returns `[]` when no rows match. */
27
+ list(filters?: TFilters): Promise<readonly TEntity[]>;
28
+ /** Create-or-replace one entity using the backend-agnostic contract. */
29
+ upsert(input: TInput): Promise<TEntity>;
30
+ /**
31
+ * Remove an entity by identity. Returns `{ deleted: true }` when the row
32
+ * was found and removed, `{ deleted: false }` when no matching row
33
+ * existed (not an error).
34
+ */
35
+ remove(id: TId): Promise<{ readonly deleted: boolean }>;
36
+ /**
37
+ * Optional insert — available on tabular adapters that distinguish
38
+ * create from update. When absent, synthesized blazes fall back to
39
+ * `upsert`.
40
+ */
41
+ insert?(input: TInput): Promise<TEntity>;
42
+ /**
43
+ * Optional patch-by-identity — available on tabular adapters. Returns
44
+ * `null` when no row with the given identity exists. When absent,
45
+ * synthesized update blazes fall back to `get` + merge + `upsert`.
46
+ */
47
+ update?(id: TId, patch: Partial<TInput>): Promise<TEntity | null>;
48
+ }
49
+
50
+ /**
51
+ * Record shape returned by a resource's `from(ctx)` call, keyed by accessor
52
+ * name. Used by `deriveTrail()` to resolve an accessor by contour name.
53
+ */
54
+ export type StoreAccessorRecord = Readonly<
55
+ Record<string, StoreAccessorProtocol<unknown, unknown, unknown, unknown>>
56
+ >;
@@ -0,0 +1,4 @@
1
+ export type {
2
+ StoreAccessorProtocol,
3
+ StoreAccessorRecord,
4
+ } from './accessor-protocol.js';
@@ -0,0 +1,245 @@
1
+ import type { TrailExample, TrailExampleSignalAssertion } from './trail.js';
2
+
3
+ export interface StructuredTrailExampleProvenance {
4
+ readonly source: 'trail.examples';
5
+ }
6
+
7
+ export interface StructuredSignalExampleProvenance {
8
+ readonly source: 'signal.examples';
9
+ }
10
+
11
+ export type StructuredTrailExampleKind = 'success' | 'error';
12
+
13
+ export interface StructuredTrailExample {
14
+ readonly description?: string | undefined;
15
+ readonly error?: string | undefined;
16
+ readonly expected?: unknown | undefined;
17
+ readonly expectedMatch?: unknown | undefined;
18
+ readonly input: unknown;
19
+ readonly kind: StructuredTrailExampleKind;
20
+ readonly name: string;
21
+ readonly provenance: StructuredTrailExampleProvenance;
22
+ readonly signals?:
23
+ | readonly StructuredTrailExampleSignalAssertion[]
24
+ | undefined;
25
+ }
26
+
27
+ export interface StructuredTrailExampleSignalAssertion {
28
+ readonly payload?: unknown | undefined;
29
+ readonly payloadMatch?: unknown | undefined;
30
+ readonly signalId: string;
31
+ readonly times?: number | undefined;
32
+ }
33
+
34
+ export interface StructuredSignalExample {
35
+ readonly kind: 'payload';
36
+ readonly payload: unknown;
37
+ readonly provenance: StructuredSignalExampleProvenance;
38
+ }
39
+
40
+ // `Date`, `RegExp`, `Map`, and `Set` are objects (`typeof === 'object'`),
41
+ // so they would pass the structural walk and reach `JSON.stringify`, which
42
+ // silently coerces them: a `Date` becomes its ISO string, a `RegExp` and
43
+ // any `Map`/`Set` become `{}`. Either way the projected shape diverges
44
+ // from the example author's declared input. Treat them as non-serializable
45
+ // leaves so the example is dropped rather than misrepresented to MCP
46
+ // clients.
47
+ const isNonJsonLeaf = (value: unknown): boolean => {
48
+ const kind = typeof value;
49
+ if (kind === 'function' || kind === 'symbol' || kind === 'bigint') {
50
+ return true;
51
+ }
52
+ return (
53
+ value instanceof Date ||
54
+ value instanceof RegExp ||
55
+ value instanceof Map ||
56
+ value instanceof Set
57
+ );
58
+ };
59
+
60
+ // `JSON.stringify` silently drops function / symbol property values rather than
61
+ // throwing, so we walk the value first and reject any example whose graph
62
+ // contains a non-serializable leaf. Without this, an MCP client consuming
63
+ // `ontrails/examples` would receive structurally incorrect inputs.
64
+ const containsNonSerializableLeaf = (
65
+ value: unknown,
66
+ seen: WeakSet<object>
67
+ ): boolean => {
68
+ if (isNonJsonLeaf(value)) {
69
+ return true;
70
+ }
71
+ if (value === null || typeof value !== 'object') {
72
+ return false;
73
+ }
74
+ const obj = value as object;
75
+ if (seen.has(obj)) {
76
+ return false;
77
+ }
78
+ seen.add(obj);
79
+ if (Array.isArray(value)) {
80
+ return value.some((entry) => containsNonSerializableLeaf(entry, seen));
81
+ }
82
+ return Object.values(obj).some((entry) =>
83
+ containsNonSerializableLeaf(entry, seen)
84
+ );
85
+ };
86
+
87
+ const toJsonSerializable = (value: unknown): unknown | undefined => {
88
+ if (containsNonSerializableLeaf(value, new WeakSet<object>())) {
89
+ return undefined;
90
+ }
91
+ try {
92
+ const encoded = JSON.stringify(value);
93
+ return encoded === undefined ? undefined : JSON.parse(encoded);
94
+ } catch {
95
+ return undefined;
96
+ }
97
+ };
98
+
99
+ const signalIdFromAssertion = (
100
+ assertion: TrailExampleSignalAssertion
101
+ ): string | undefined => {
102
+ if (typeof assertion.signal === 'string') {
103
+ return assertion.signal;
104
+ }
105
+ return typeof assertion.signal.id === 'string'
106
+ ? assertion.signal.id
107
+ : undefined;
108
+ };
109
+
110
+ const projectSignalAssertion = (
111
+ assertion: TrailExampleSignalAssertion
112
+ ): StructuredTrailExampleSignalAssertion | undefined => {
113
+ const signalId = signalIdFromAssertion(assertion);
114
+ if (signalId === undefined) {
115
+ return undefined;
116
+ }
117
+
118
+ const projected: Record<string, unknown> = { signalId };
119
+ if (assertion.payload !== undefined) {
120
+ const payload = toJsonSerializable(assertion.payload);
121
+ if (payload === undefined) {
122
+ return undefined;
123
+ }
124
+ projected['payload'] = payload;
125
+ }
126
+ if (assertion.payloadMatch !== undefined) {
127
+ const payloadMatch = toJsonSerializable(assertion.payloadMatch);
128
+ if (payloadMatch === undefined) {
129
+ return undefined;
130
+ }
131
+ projected['payloadMatch'] = payloadMatch;
132
+ }
133
+ if (assertion.times !== undefined) {
134
+ projected['times'] = assertion.times;
135
+ }
136
+ return Object.freeze(
137
+ projected
138
+ ) as unknown as StructuredTrailExampleSignalAssertion;
139
+ };
140
+
141
+ const projectSignalAssertions = (
142
+ assertions: readonly TrailExampleSignalAssertion[] | undefined
143
+ ): readonly StructuredTrailExampleSignalAssertion[] | undefined => {
144
+ if (assertions === undefined) {
145
+ return undefined;
146
+ }
147
+ const projected = assertions.map(projectSignalAssertion);
148
+ if (projected.some((assertion) => assertion === undefined)) {
149
+ return undefined;
150
+ }
151
+ return Object.freeze(projected as StructuredTrailExampleSignalAssertion[]);
152
+ };
153
+
154
+ const projectExample = (
155
+ example: TrailExample<unknown, unknown>
156
+ ): StructuredTrailExample | undefined => {
157
+ const input = toJsonSerializable(example.input);
158
+ if (input === undefined) {
159
+ return undefined;
160
+ }
161
+
162
+ const projected: Record<string, unknown> = {
163
+ input,
164
+ kind: example.error === undefined ? 'success' : 'error',
165
+ name: example.name,
166
+ provenance: { source: 'trail.examples' },
167
+ };
168
+
169
+ if (example.description !== undefined) {
170
+ projected['description'] = example.description;
171
+ }
172
+ if (example.expected !== undefined) {
173
+ const expected = toJsonSerializable(example.expected);
174
+ if (expected === undefined) {
175
+ return undefined;
176
+ }
177
+ projected['expected'] = expected;
178
+ }
179
+ if (example.expectedMatch !== undefined) {
180
+ const expectedMatch = toJsonSerializable(example.expectedMatch);
181
+ if (expectedMatch === undefined) {
182
+ return undefined;
183
+ }
184
+ projected['expectedMatch'] = expectedMatch;
185
+ }
186
+ if (example.error !== undefined) {
187
+ projected['error'] = example.error;
188
+ }
189
+ if (example.signals !== undefined) {
190
+ const signals = projectSignalAssertions(example.signals);
191
+ if (signals === undefined) {
192
+ return undefined;
193
+ }
194
+ projected['signals'] = signals;
195
+ }
196
+
197
+ return Object.freeze(projected) as unknown as StructuredTrailExample;
198
+ };
199
+
200
+ const projectSignalExample = (
201
+ payload: unknown
202
+ ): StructuredSignalExample | undefined => {
203
+ const serializablePayload = toJsonSerializable(payload);
204
+ if (serializablePayload === undefined) {
205
+ return undefined;
206
+ }
207
+
208
+ return Object.freeze({
209
+ kind: 'payload',
210
+ payload: serializablePayload,
211
+ provenance: { source: 'signal.examples' },
212
+ } satisfies StructuredSignalExample);
213
+ };
214
+
215
+ export const deriveStructuredTrailExamples = (
216
+ examples: readonly TrailExample<unknown, unknown>[] | undefined
217
+ ): readonly StructuredTrailExample[] | undefined => {
218
+ if (examples === undefined || examples.length === 0) {
219
+ return undefined;
220
+ }
221
+
222
+ const projected = examples
223
+ .map(projectExample)
224
+ .filter(
225
+ (example): example is StructuredTrailExample => example !== undefined
226
+ );
227
+
228
+ return projected.length > 0 ? Object.freeze(projected) : undefined;
229
+ };
230
+
231
+ export const deriveStructuredSignalExamples = (
232
+ examples: readonly unknown[] | undefined
233
+ ): readonly StructuredSignalExample[] | undefined => {
234
+ if (examples === undefined || examples.length === 0) {
235
+ return undefined;
236
+ }
237
+
238
+ const projected = examples
239
+ .map(projectSignalExample)
240
+ .filter(
241
+ (example): example is StructuredSignalExample => example !== undefined
242
+ );
243
+
244
+ return projected.length > 0 ? Object.freeze(projected) : undefined;
245
+ };
@@ -0,0 +1,91 @@
1
+ import { Result } from './result.js';
2
+ import type { Layer } from './layer.js';
3
+ import type { Intent } from './trail.js';
4
+ import type { Topo } from './topo.js';
5
+ import type { SurfaceName } from './transport-error-map.js';
6
+ import type { TrailContextInit } from './types.js';
7
+ import { SURFACE_KEY, SURFACE_LAYER_NAMES_KEY } from './types.js';
8
+ import { validateEstablishedTopo } from './validate-established-topo.js';
9
+
10
+ export type SurfaceConfigValues = Readonly<
11
+ Record<string, Record<string, unknown>>
12
+ >;
13
+
14
+ export interface SurfaceSelectionOptions {
15
+ /** Glob patterns that remove matching trail IDs. */
16
+ readonly exclude?: readonly string[] | undefined;
17
+ /** Glob patterns that keep only matching trail IDs when provided. */
18
+ readonly include?: readonly string[] | undefined;
19
+ /** Allowed intents for exposed surfaces. Empty arrays act as no filter. */
20
+ readonly intent?: readonly Intent[] | undefined;
21
+ }
22
+
23
+ export interface SurfaceValidationOptions {
24
+ /** Set to `false` to skip established-topo validation during projection. */
25
+ readonly validate?: boolean | undefined;
26
+ }
27
+
28
+ export interface BaseSurfaceOptions
29
+ extends SurfaceSelectionOptions, SurfaceValidationOptions {
30
+ /** Config values for resources that declare a `config` schema, keyed by resource ID. */
31
+ readonly configValues?: SurfaceConfigValues | undefined;
32
+ }
33
+
34
+ export const shouldValidateSurfaceTopo = (
35
+ options?: SurfaceValidationOptions
36
+ ): boolean => options?.validate !== false;
37
+
38
+ export const validateSurfaceTopo = (
39
+ graph: Topo,
40
+ options?: SurfaceValidationOptions
41
+ ): Result<void, Error> => {
42
+ if (!shouldValidateSurfaceTopo(options)) {
43
+ return Result.ok();
44
+ }
45
+
46
+ const validated = validateEstablishedTopo(graph);
47
+ return validated.isErr() ? Result.err(validated.error) : Result.ok();
48
+ };
49
+
50
+ export type SurfaceMarkedContext = Partial<TrailContextInit> & {
51
+ readonly extensions: Readonly<Record<string, unknown>>;
52
+ };
53
+
54
+ export const withSurfaceMarker = (
55
+ surface: SurfaceName,
56
+ ctx: Partial<TrailContextInit> = {}
57
+ ): SurfaceMarkedContext => ({
58
+ ...ctx,
59
+ extensions: {
60
+ ...ctx.extensions,
61
+ [SURFACE_KEY]: surface,
62
+ },
63
+ });
64
+
65
+ const readSurfaceLayerNameRecord = (
66
+ value: unknown
67
+ ): Record<string, readonly string[]> =>
68
+ value !== null && typeof value === 'object' && !Array.isArray(value)
69
+ ? (value as Record<string, readonly string[]>)
70
+ : {};
71
+
72
+ export const withSurfaceLayerNames = (
73
+ surface: SurfaceName,
74
+ layers: readonly Layer[],
75
+ ctx: Partial<TrailContextInit> = {}
76
+ ): SurfaceMarkedContext => {
77
+ const existing = readSurfaceLayerNameRecord(
78
+ ctx.extensions?.[SURFACE_LAYER_NAMES_KEY]
79
+ );
80
+ return {
81
+ ...ctx,
82
+ extensions: {
83
+ ...ctx.extensions,
84
+ [SURFACE_KEY]: surface,
85
+ [SURFACE_LAYER_NAMES_KEY]: {
86
+ ...existing,
87
+ [surface]: layers.map((layer) => layer.name),
88
+ },
89
+ },
90
+ };
91
+ };
@@ -0,0 +1,176 @@
1
+ import type { Intent, Trail } from './trail.js';
2
+ import type { SurfaceSelectionOptions } from './surface-derivation.js';
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Public types
6
+ // ---------------------------------------------------------------------------
7
+
8
+ export type SurfaceFilterOptions = SurfaceSelectionOptions;
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Glob matching
12
+ // ---------------------------------------------------------------------------
13
+
14
+ const trailIdSegments = (trailId: string): readonly string[] =>
15
+ trailId.split('.');
16
+
17
+ const patternSegments = (pattern: string): readonly string[] =>
18
+ pattern.split('.');
19
+
20
+ type SegmentMatcher = (
21
+ trail: readonly string[],
22
+ pattern: readonly string[],
23
+ trailIndex: number,
24
+ patternIndex: number
25
+ ) => boolean;
26
+
27
+ const matchesDoubleStar = (
28
+ trail: readonly string[],
29
+ pattern: readonly string[],
30
+ trailIndex: number,
31
+ patternIndex: number,
32
+ matchSegments: SegmentMatcher
33
+ ): boolean =>
34
+ patternIndex === pattern.length - 1 ||
35
+ Array.from(
36
+ { length: trail.length - trailIndex + 1 },
37
+ (_, offset) => trailIndex + offset
38
+ ).some((index) => matchSegments(trail, pattern, index, patternIndex + 1));
39
+
40
+ const matchesSingleSegment = (
41
+ trail: readonly string[],
42
+ pattern: readonly string[],
43
+ trailIndex: number,
44
+ patternIndex: number,
45
+ current: string,
46
+ matchSegments: SegmentMatcher
47
+ ): boolean =>
48
+ trailIndex < trail.length &&
49
+ (current === '*' || current === trail[trailIndex]) &&
50
+ matchSegments(trail, pattern, trailIndex + 1, patternIndex + 1);
51
+
52
+ const matchesFrom = function matchesFrom(
53
+ trail: readonly string[],
54
+ pattern: readonly string[],
55
+ trailIndex: number,
56
+ patternIndex: number
57
+ ): boolean {
58
+ if (patternIndex >= pattern.length) {
59
+ return trailIndex >= trail.length;
60
+ }
61
+
62
+ const current = pattern[patternIndex];
63
+ if (current === undefined) {
64
+ return false;
65
+ }
66
+
67
+ return current === '**'
68
+ ? matchesDoubleStar(trail, pattern, trailIndex, patternIndex, matchesFrom)
69
+ : matchesSingleSegment(
70
+ trail,
71
+ pattern,
72
+ trailIndex,
73
+ patternIndex,
74
+ current,
75
+ matchesFrom
76
+ );
77
+ };
78
+
79
+ export const matchesTrailPattern = (
80
+ trailId: string,
81
+ pattern: string
82
+ ): boolean => {
83
+ if (pattern === trailId) {
84
+ return true;
85
+ }
86
+ return matchesFrom(trailIdSegments(trailId), patternSegments(pattern), 0, 0);
87
+ };
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Surface filtering
91
+ // ---------------------------------------------------------------------------
92
+
93
+ const matchesAnyPattern = (
94
+ trailId: string,
95
+ patterns: readonly string[] | undefined
96
+ ): boolean =>
97
+ patterns !== undefined &&
98
+ patterns.some((pattern) => matchesTrailPattern(trailId, pattern));
99
+
100
+ const isExplicitInternalInclude = (
101
+ trailId: string,
102
+ include: readonly string[] | undefined
103
+ ): boolean => include !== undefined && include.includes(trailId);
104
+
105
+ /**
106
+ * Resolve the effective visibility for a trail.
107
+ *
108
+ * Returns `'internal'` when either the explicit `visibility` field is
109
+ * `'internal'` or the legacy `meta.internal === true` convention is set.
110
+ * Honoring the legacy flag keeps trails authored before the visibility
111
+ * field was introduced (e.g. `meta: { internal: true }`) off surfaces.
112
+ *
113
+ * The runtime always fills in `visibility: 'public'` when the spec did not
114
+ * declare it, so we cannot distinguish explicit `'public'` from the default.
115
+ * This means a trail that sets both `meta.internal = true` and
116
+ * `visibility: 'public'` will still be treated as internal — that
117
+ * combination has never been a documented override and, if the author
118
+ * really means "public", they should remove the legacy flag.
119
+ */
120
+ const effectiveVisibility = (
121
+ trail: Trail<unknown, unknown, unknown>
122
+ ): 'public' | 'internal' => {
123
+ if (trail.visibility === 'internal') {
124
+ return 'internal';
125
+ }
126
+ return trail.meta?.['internal'] === true ? 'internal' : 'public';
127
+ };
128
+
129
+ const isVisibleToSurfaces = (
130
+ trail: Trail<unknown, unknown, unknown>,
131
+ include: readonly string[] | undefined
132
+ ): boolean =>
133
+ effectiveVisibility(trail) !== 'internal' ||
134
+ isExplicitInternalInclude(trail.id, include);
135
+
136
+ const passesIncludeFilter = (
137
+ trailId: string,
138
+ include: readonly string[] | undefined
139
+ ): boolean =>
140
+ include === undefined ||
141
+ include.length === 0 ||
142
+ matchesAnyPattern(trailId, include);
143
+
144
+ const passesIntentFilter = (
145
+ trail: Trail<unknown, unknown, unknown>,
146
+ intent: readonly Intent[] | undefined
147
+ ): boolean =>
148
+ intent === undefined || intent.length === 0 || intent.includes(trail.intent);
149
+
150
+ export const shouldIncludeTrailForSurface = (
151
+ trail: Trail<unknown, unknown, unknown>,
152
+ options: SurfaceFilterOptions = {}
153
+ ): boolean => {
154
+ if (trail.activationSources.length > 0) {
155
+ return false;
156
+ }
157
+
158
+ const { exclude, include, intent } = options;
159
+ if (!isVisibleToSurfaces(trail, include)) {
160
+ return false;
161
+ }
162
+
163
+ if (matchesAnyPattern(trail.id, exclude)) {
164
+ return false;
165
+ }
166
+
167
+ return (
168
+ passesIncludeFilter(trail.id, include) && passesIntentFilter(trail, intent)
169
+ );
170
+ };
171
+
172
+ export const filterSurfaceTrails = (
173
+ trails: readonly Trail<unknown, unknown, unknown>[],
174
+ options: SurfaceFilterOptions = {}
175
+ ): Trail<unknown, unknown, unknown>[] =>
176
+ trails.filter((trail) => shouldIncludeTrailForSurface(trail, options));