@ontrails/core 1.0.0-beta.13 → 1.0.0-beta.15

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 (287) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +17 -0
  3. package/README.md +19 -14
  4. package/dist/branded.d.ts +1 -1
  5. package/dist/branded.d.ts.map +1 -1
  6. package/dist/branded.js +1 -1
  7. package/dist/branded.js.map +1 -1
  8. package/dist/context.d.ts +13 -1
  9. package/dist/context.d.ts.map +1 -1
  10. package/dist/context.js +19 -3
  11. package/dist/context.js.map +1 -1
  12. package/dist/contour.d.ts +81 -0
  13. package/dist/contour.d.ts.map +1 -0
  14. package/dist/contour.js +176 -0
  15. package/dist/contour.js.map +1 -0
  16. package/dist/cross-schema.d.ts +18 -0
  17. package/dist/cross-schema.d.ts.map +1 -0
  18. package/dist/cross-schema.js +29 -0
  19. package/dist/cross-schema.js.map +1 -0
  20. package/dist/derive.d.ts +6 -0
  21. package/dist/derive.d.ts.map +1 -1
  22. package/dist/derive.js +29 -5
  23. package/dist/derive.js.map +1 -1
  24. package/dist/draft.d.ts +28 -0
  25. package/dist/draft.d.ts.map +1 -0
  26. package/dist/draft.js +157 -0
  27. package/dist/draft.js.map +1 -0
  28. package/dist/errors.d.ts +62 -4
  29. package/dist/errors.d.ts.map +1 -1
  30. package/dist/errors.js +44 -1
  31. package/dist/errors.js.map +1 -1
  32. package/dist/execute.d.ts +26 -9
  33. package/dist/execute.d.ts.map +1 -1
  34. package/dist/execute.js +441 -20
  35. package/dist/execute.js.map +1 -1
  36. package/dist/fire.d.ts +43 -0
  37. package/dist/fire.d.ts.map +1 -0
  38. package/dist/fire.js +185 -0
  39. package/dist/fire.js.map +1 -0
  40. package/dist/index.d.ts +33 -17
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +26 -12
  43. package/dist/index.js.map +1 -1
  44. package/dist/internal/cross-batch.d.ts +44 -0
  45. package/dist/internal/cross-batch.d.ts.map +1 -0
  46. package/dist/internal/cross-batch.js +58 -0
  47. package/dist/internal/cross-batch.js.map +1 -0
  48. package/dist/internal/fork-ctx.d.ts +41 -0
  49. package/dist/internal/fork-ctx.d.ts.map +1 -0
  50. package/dist/internal/fork-ctx.js +47 -0
  51. package/dist/internal/fork-ctx.js.map +1 -0
  52. package/dist/internal/signal-ref.d.ts +15 -0
  53. package/dist/internal/signal-ref.d.ts.map +1 -0
  54. package/dist/internal/signal-ref.js +41 -0
  55. package/dist/internal/signal-ref.js.map +1 -0
  56. package/dist/internal/topo-saves.d.ts +53 -0
  57. package/dist/internal/topo-saves.d.ts.map +1 -0
  58. package/dist/internal/topo-saves.js +449 -0
  59. package/dist/internal/topo-saves.js.map +1 -0
  60. package/dist/internal/topo-snapshots.d.ts +57 -0
  61. package/dist/internal/topo-snapshots.d.ts.map +1 -0
  62. package/dist/internal/topo-snapshots.js +372 -0
  63. package/dist/internal/topo-snapshots.js.map +1 -0
  64. package/dist/internal/topo-store-read.d.ts +70 -0
  65. package/dist/internal/topo-store-read.d.ts.map +1 -0
  66. package/dist/internal/topo-store-read.js +221 -0
  67. package/dist/internal/topo-store-read.js.map +1 -0
  68. package/dist/internal/topo-store.d.ts +26 -0
  69. package/dist/internal/topo-store.d.ts.map +1 -0
  70. package/dist/internal/topo-store.js +621 -0
  71. package/dist/internal/topo-store.js.map +1 -0
  72. package/dist/internal/tracing.d.ts +98 -0
  73. package/dist/internal/tracing.d.ts.map +1 -0
  74. package/dist/internal/tracing.js +102 -0
  75. package/dist/internal/tracing.js.map +1 -0
  76. package/dist/internal/trails-db.d.ts +16 -0
  77. package/dist/internal/trails-db.d.ts.map +1 -0
  78. package/dist/internal/trails-db.js +118 -0
  79. package/dist/internal/trails-db.js.map +1 -0
  80. package/dist/internal/zod-wrappers.d.ts +40 -0
  81. package/dist/internal/zod-wrappers.d.ts.map +1 -0
  82. package/dist/internal/zod-wrappers.js +59 -0
  83. package/dist/internal/zod-wrappers.js.map +1 -0
  84. package/dist/layer.d.ts +22 -5
  85. package/dist/layer.d.ts.map +1 -1
  86. package/dist/layer.js +20 -3
  87. package/dist/layer.js.map +1 -1
  88. package/dist/path-security.d.ts +1 -1
  89. package/dist/path-security.d.ts.map +1 -1
  90. package/dist/path-security.js +1 -1
  91. package/dist/path-security.js.map +1 -1
  92. package/dist/resilience.d.ts +1 -1
  93. package/dist/resilience.d.ts.map +1 -1
  94. package/dist/resilience.js +3 -3
  95. package/dist/resilience.js.map +1 -1
  96. package/dist/resource-config.d.ts +22 -0
  97. package/dist/resource-config.d.ts.map +1 -0
  98. package/dist/resource-config.js +210 -0
  99. package/dist/resource-config.js.map +1 -0
  100. package/dist/resource.d.ts +74 -0
  101. package/dist/resource.d.ts.map +1 -0
  102. package/dist/resource.js +61 -0
  103. package/dist/resource.js.map +1 -0
  104. package/dist/run.d.ts +4 -3
  105. package/dist/run.d.ts.map +1 -1
  106. package/dist/run.js +5 -4
  107. package/dist/run.js.map +1 -1
  108. package/dist/serialization.d.ts.map +1 -1
  109. package/dist/serialization.js +2 -1
  110. package/dist/serialization.js.map +1 -1
  111. package/dist/store/accessor-protocol.d.ts +51 -0
  112. package/dist/store/accessor-protocol.d.ts.map +1 -0
  113. package/dist/store/accessor-protocol.js +2 -0
  114. package/dist/store/accessor-protocol.js.map +1 -0
  115. package/dist/store/index.d.ts +2 -0
  116. package/dist/store/index.d.ts.map +1 -0
  117. package/dist/store/index.js +2 -0
  118. package/dist/store/index.js.map +1 -0
  119. package/dist/surface-filter.d.ts +13 -0
  120. package/dist/surface-filter.d.ts.map +1 -0
  121. package/dist/surface-filter.js +76 -0
  122. package/dist/surface-filter.js.map +1 -0
  123. package/dist/topo-store.d.ts +62 -0
  124. package/dist/topo-store.d.ts.map +1 -0
  125. package/dist/topo-store.js +319 -0
  126. package/dist/topo-store.js.map +1 -0
  127. package/dist/topo.d.ts +23 -9
  128. package/dist/topo.d.ts.map +1 -1
  129. package/dist/topo.js +175 -46
  130. package/dist/topo.js.map +1 -1
  131. package/dist/trail.d.ts +86 -19
  132. package/dist/trail.d.ts.map +1 -1
  133. package/dist/trail.js +59 -4
  134. package/dist/trail.js.map +1 -1
  135. package/dist/trails/derive-trail.d.ts +66 -0
  136. package/dist/trails/derive-trail.d.ts.map +1 -0
  137. package/dist/trails/derive-trail.js +378 -0
  138. package/dist/trails/derive-trail.js.map +1 -0
  139. package/dist/trails/index.d.ts +5 -0
  140. package/dist/trails/index.d.ts.map +1 -0
  141. package/dist/trails/index.js +3 -0
  142. package/dist/trails/index.js.map +1 -0
  143. package/dist/trails/ingest.d.ts +24 -0
  144. package/dist/trails/ingest.d.ts.map +1 -0
  145. package/dist/trails/ingest.js +63 -0
  146. package/dist/trails/ingest.js.map +1 -0
  147. package/dist/transport-error-map.d.ts +106 -0
  148. package/dist/transport-error-map.d.ts.map +1 -0
  149. package/dist/transport-error-map.js +24 -0
  150. package/dist/transport-error-map.js.map +1 -0
  151. package/dist/type-checks.test-d.d.ts +71 -0
  152. package/dist/type-checks.test-d.d.ts.map +1 -0
  153. package/dist/type-checks.test-d.js +12 -0
  154. package/dist/type-checks.test-d.js.map +1 -0
  155. package/dist/type-utils.d.ts +11 -2
  156. package/dist/type-utils.d.ts.map +1 -1
  157. package/dist/type-utils.js.map +1 -1
  158. package/dist/types.d.ts +113 -7
  159. package/dist/types.d.ts.map +1 -1
  160. package/dist/types.js.map +1 -1
  161. package/dist/validate-established-topo.d.ts +76 -0
  162. package/dist/validate-established-topo.d.ts.map +1 -0
  163. package/dist/validate-established-topo.js +43 -0
  164. package/dist/validate-established-topo.js.map +1 -0
  165. package/dist/validate-topo.d.ts.map +1 -1
  166. package/dist/validate-topo.js +27 -8
  167. package/dist/validate-topo.js.map +1 -1
  168. package/dist/workspace.d.ts +1 -1
  169. package/dist/workspace.d.ts.map +1 -1
  170. package/dist/workspace.js +1 -1
  171. package/dist/workspace.js.map +1 -1
  172. package/package.json +10 -1
  173. package/src/__tests__/branded.test.ts +5 -5
  174. package/src/__tests__/context.test.ts +3 -3
  175. package/src/__tests__/contour.test.ts +263 -0
  176. package/src/__tests__/derive-trail.test.ts +668 -0
  177. package/src/__tests__/derive.test.ts +58 -1
  178. package/src/__tests__/errors.test.ts +118 -0
  179. package/src/__tests__/execute.test.ts +1179 -141
  180. package/src/__tests__/fire.test.ts +1056 -0
  181. package/src/__tests__/fork-ctx.test.ts +83 -0
  182. package/src/__tests__/ingest.test.ts +191 -0
  183. package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
  184. package/src/__tests__/path-security.test.ts +7 -7
  185. package/src/__tests__/resilience.test.ts +8 -8
  186. package/src/__tests__/resource.test.ts +204 -0
  187. package/src/__tests__/run.test.ts +13 -13
  188. package/src/__tests__/service-config.test.ts +28 -32
  189. package/src/__tests__/surface-filter.test.ts +196 -0
  190. package/src/__tests__/topo-store-read.test.ts +256 -0
  191. package/src/__tests__/topo-store.test.ts +869 -0
  192. package/src/__tests__/topo.test.ts +350 -54
  193. package/src/__tests__/trail.test.ts +189 -24
  194. package/src/__tests__/trails-db.test.ts +198 -0
  195. package/src/__tests__/transport-error-map.test.ts +82 -0
  196. package/src/__tests__/type-utils.test.ts +29 -1
  197. package/src/__tests__/validate-topo.test.ts +305 -16
  198. package/src/__tests__/workspace.test.ts +6 -6
  199. package/src/__tests__/zod-wrappers.test.ts +62 -0
  200. package/src/branded.ts +1 -1
  201. package/src/context.ts +24 -4
  202. package/src/contour.ts +344 -0
  203. package/src/cross-schema.ts +36 -0
  204. package/src/derive.ts +39 -8
  205. package/src/draft.ts +338 -0
  206. package/src/errors.ts +62 -18
  207. package/src/execute.ts +840 -29
  208. package/src/fire.ts +295 -0
  209. package/src/index.ts +142 -26
  210. package/src/internal/cross-batch.ts +77 -0
  211. package/src/internal/fork-ctx.ts +69 -0
  212. package/src/internal/signal-ref.ts +87 -0
  213. package/src/internal/topo-snapshots.ts +517 -0
  214. package/src/internal/topo-store-read.ts +481 -0
  215. package/src/internal/topo-store.ts +1192 -0
  216. package/src/internal/tracing.ts +187 -0
  217. package/src/internal/trails-db.ts +187 -0
  218. package/src/internal/zod-wrappers.ts +78 -0
  219. package/src/layer.ts +50 -0
  220. package/src/path-security.ts +1 -1
  221. package/src/resilience.ts +3 -3
  222. package/src/{provision-config.ts → resource-config.ts} +109 -123
  223. package/src/resource.ts +155 -0
  224. package/src/run.ts +5 -4
  225. package/src/serialization.ts +2 -0
  226. package/src/store/accessor-protocol.ts +56 -0
  227. package/src/store/index.ts +4 -0
  228. package/src/surface-filter.ts +182 -0
  229. package/src/topo-store.ts +510 -0
  230. package/src/topo.ts +328 -57
  231. package/src/trail.ts +199 -31
  232. package/src/trails/derive-trail.ts +842 -0
  233. package/src/trails/index.ts +9 -0
  234. package/src/trails/ingest.ts +146 -0
  235. package/src/transport-error-map.ts +56 -0
  236. package/src/type-checks.test-d.ts +106 -0
  237. package/src/type-utils.ts +17 -2
  238. package/src/types.ts +148 -11
  239. package/src/validate-established-topo.ts +63 -0
  240. package/src/validate-topo.ts +37 -9
  241. package/src/workspace.ts +1 -1
  242. package/tsconfig.tests.json +10 -0
  243. package/tsconfig.tsbuildinfo +1 -1
  244. package/dist/adapters.d.ts +0 -39
  245. package/dist/adapters.d.ts.map +0 -1
  246. package/dist/adapters.js +0 -2
  247. package/dist/adapters.js.map +0 -1
  248. package/dist/dispatch.d.ts +0 -27
  249. package/dist/dispatch.d.ts.map +0 -1
  250. package/dist/dispatch.js +0 -34
  251. package/dist/dispatch.js.map +0 -1
  252. package/dist/event.d.ts +0 -8
  253. package/dist/event.d.ts.map +0 -1
  254. package/dist/event.js +0 -7
  255. package/dist/event.js.map +0 -1
  256. package/dist/gate.d.ts +0 -17
  257. package/dist/gate.d.ts.map +0 -1
  258. package/dist/gate.js +0 -21
  259. package/dist/gate.js.map +0 -1
  260. package/dist/health.d.ts +0 -18
  261. package/dist/health.d.ts.map +0 -1
  262. package/dist/health.js +0 -5
  263. package/dist/health.js.map +0 -1
  264. package/dist/job.d.ts +0 -24
  265. package/dist/job.d.ts.map +0 -1
  266. package/dist/job.js +0 -17
  267. package/dist/job.js.map +0 -1
  268. package/dist/provision-config.d.ts +0 -22
  269. package/dist/provision-config.d.ts.map +0 -1
  270. package/dist/provision-config.js +0 -210
  271. package/dist/provision-config.js.map +0 -1
  272. package/dist/provision.d.ts +0 -71
  273. package/dist/provision.d.ts.map +0 -1
  274. package/dist/provision.js +0 -56
  275. package/dist/provision.js.map +0 -1
  276. package/dist/service-config.d.ts +0 -22
  277. package/dist/service-config.d.ts.map +0 -1
  278. package/dist/service-config.js +0 -210
  279. package/dist/service-config.js.map +0 -1
  280. package/dist/service.d.ts +0 -71
  281. package/dist/service.d.ts.map +0 -1
  282. package/dist/service.js +0 -56
  283. package/dist/service.js.map +0 -1
  284. package/src/__tests__/service.test.ts +0 -197
  285. package/src/event.ts +0 -15
  286. package/src/gate.ts +0 -44
  287. package/src/provision.ts +0 -148
@@ -1,22 +1,22 @@
1
1
  /**
2
- * Provision resolution pipeline.
2
+ * Resource resolution pipeline.
3
3
  *
4
4
  * Extracted from execute.ts to keep both modules under the 400 LOC ceiling.
5
5
  * Handles config validation, singleton caching, concurrent-creation dedup,
6
- * and the full resolve-or-create flow for declared provisions.
6
+ * and the full resolve-or-create flow for declared resources.
7
7
  */
8
8
 
9
9
  import type {
10
- AnyProvision,
11
- ProvisionContext,
12
- ProvisionOverrideMap,
13
- } from './provision.js';
10
+ AnyResource,
11
+ ResourceContext,
12
+ ResourceOverrideMap,
13
+ } from './resource.js';
14
14
  import type { AnyTrail } from './trail.js';
15
15
  import type { TrailContext } from './types.js';
16
16
 
17
17
  import { InternalError, ValidationError } from './errors.js';
18
18
  import { Result } from './result.js';
19
- import { createProvisionLookup } from './provision.js';
19
+ import { createResourceLookup } from './resource.js';
20
20
 
21
21
  type MutableTrailContext = {
22
22
  -readonly [K in keyof TrailContext]: TrailContext[K];
@@ -28,11 +28,11 @@ type ConfigValues = Readonly<Record<string, Record<string, unknown>>>;
28
28
  // Singleton caches
29
29
  // ---------------------------------------------------------------------------
30
30
 
31
- const singletonProvisions = new WeakMap<AnyProvision, Map<string, unknown>>();
31
+ const singletonResources = new WeakMap<AnyResource, Map<string, unknown>>();
32
32
 
33
- /** In-flight provision creation promises, keyed by provision x context. */
33
+ /** In-flight resource creation promises, keyed by resource x context. */
34
34
  const pendingCreations = new WeakMap<
35
- AnyProvision,
35
+ AnyResource,
36
36
  Map<string, Promise<Result<unknown, Error>>>
37
37
  >();
38
38
 
@@ -40,17 +40,17 @@ const pendingCreations = new WeakMap<
40
40
  // Context helpers
41
41
  // ---------------------------------------------------------------------------
42
42
 
43
- const toProvisionContext = (
43
+ const toResourceContext = (
44
44
  ctx: TrailContext,
45
45
  config?: unknown
46
- ): ProvisionContext => ({
46
+ ): ResourceContext => ({
47
47
  config,
48
48
  cwd: ctx.cwd,
49
49
  env: ctx.env,
50
50
  workspaceRoot: ctx.workspaceRoot,
51
51
  });
52
52
 
53
- const toProvisionContextKey = (ctx: ProvisionContext): string =>
53
+ const toResourceContextKey = (ctx: ResourceContext): string =>
54
54
  JSON.stringify({
55
55
  config: ctx.config,
56
56
  cwd: ctx.cwd,
@@ -64,27 +64,27 @@ const toProvisionContextKey = (ctx: ProvisionContext): string =>
64
64
  // Config validation
65
65
  // ---------------------------------------------------------------------------
66
66
 
67
- /** Validate and resolve a provision's config from the provided configValues map. */
68
- const resolveProvisionConfig = (
69
- declaredProvision: AnyProvision,
67
+ /** Validate and resolve a resource's config from the provided configValues map. */
68
+ const resolveResourceConfig = (
69
+ declaredResource: AnyResource,
70
70
  configValues?: ConfigValues
71
71
  ): Result<unknown, Error> => {
72
- if (declaredProvision.config === undefined) {
72
+ if (declaredResource.config === undefined) {
73
73
  return Result.ok();
74
74
  }
75
- const raw = configValues?.[declaredProvision.id];
75
+ const raw = configValues?.[declaredResource.id];
76
76
  if (raw === undefined) {
77
77
  return Result.err(
78
78
  new ValidationError(
79
- `Provision "${declaredProvision.id}" declares a config schema but no config was provided`
79
+ `Resource "${declaredResource.id}" declares a config schema but no config was provided`
80
80
  )
81
81
  );
82
82
  }
83
- const parsed = declaredProvision.config.safeParse(raw);
83
+ const parsed = declaredResource.config.safeParse(raw);
84
84
  if (!parsed.success) {
85
85
  return Result.err(
86
86
  new ValidationError(
87
- `Provision "${declaredProvision.id}" config validation failed: ${parsed.error.message}`
87
+ `Resource "${declaredResource.id}" config validation failed: ${parsed.error.message}`
88
88
  )
89
89
  );
90
90
  }
@@ -95,22 +95,22 @@ const resolveProvisionConfig = (
95
95
  // Override / cache lookups
96
96
  // ---------------------------------------------------------------------------
97
97
 
98
- const hasOwnProvisionOverride = (
99
- overrides: ProvisionOverrideMap | undefined,
98
+ const hasOwnResourceOverride = (
99
+ overrides: ResourceOverrideMap | undefined,
100
100
  id: string
101
- ): overrides is ProvisionOverrideMap =>
101
+ ): overrides is ResourceOverrideMap =>
102
102
  overrides !== undefined && Object.hasOwn(overrides, id);
103
103
 
104
- const getCachedSingletonProvision = (
105
- declaredProvision: AnyProvision,
106
- provisionContext: ProvisionContext
104
+ const getCachedSingletonResource = (
105
+ declaredResource: AnyResource,
106
+ resourceContext: ResourceContext
107
107
  ): { readonly found: boolean; readonly value: unknown } => {
108
- const scopedCache = singletonProvisions.get(declaredProvision);
108
+ const scopedCache = singletonResources.get(declaredResource);
109
109
  if (scopedCache === undefined) {
110
110
  return { found: false, value: undefined };
111
111
  }
112
112
 
113
- const key = toProvisionContextKey(provisionContext);
113
+ const key = toResourceContextKey(resourceContext);
114
114
  if (!scopedCache.has(key)) {
115
115
  return { found: false, value: undefined };
116
116
  }
@@ -121,14 +121,14 @@ const getCachedSingletonProvision = (
121
121
  };
122
122
  };
123
123
 
124
- const getProvidedProvision = (
124
+ const getProvidedResource = (
125
125
  ctx: TrailContext,
126
- overrides: ProvisionOverrideMap | undefined,
127
- declaredProvision: AnyProvision,
128
- provisionContext: ProvisionContext
126
+ overrides: ResourceOverrideMap | undefined,
127
+ declaredResource: AnyResource,
128
+ resourceContext: ResourceContext
129
129
  ): Result<unknown, Error> | undefined => {
130
- const { id } = declaredProvision;
131
- if (hasOwnProvisionOverride(overrides, id)) {
130
+ const { id } = declaredResource;
131
+ if (hasOwnResourceOverride(overrides, id)) {
132
132
  return Result.ok(overrides[id]);
133
133
  }
134
134
 
@@ -136,10 +136,7 @@ const getProvidedProvision = (
136
136
  return Result.ok(ctx.extensions?.[id]);
137
137
  }
138
138
 
139
- const cached = getCachedSingletonProvision(
140
- declaredProvision,
141
- provisionContext
142
- );
139
+ const cached = getCachedSingletonResource(declaredResource, resourceContext);
143
140
  if (cached.found) {
144
141
  return Result.ok(cached.value);
145
142
  }
@@ -149,216 +146,205 @@ const getProvidedProvision = (
149
146
 
150
147
  const getOverrideOrExtension = (
151
148
  ctx: TrailContext,
152
- overrides: ProvisionOverrideMap | undefined,
153
- declaredProvision: AnyProvision
149
+ overrides: ResourceOverrideMap | undefined,
150
+ declaredResource: AnyResource
154
151
  ): Result<unknown, Error> | undefined =>
155
- getProvidedProvision(
156
- ctx,
157
- overrides,
158
- declaredProvision,
159
- toProvisionContext(ctx)
160
- );
152
+ getProvidedResource(ctx, overrides, declaredResource, toResourceContext(ctx));
161
153
 
162
154
  type ConfigAwareResolution =
163
155
  | Result<{ readonly kind: 'provided'; readonly value: unknown }, Error>
164
156
  | Result<
165
- { readonly kind: 'context'; readonly provisionContext: ProvisionContext },
157
+ { readonly kind: 'context'; readonly resourceContext: ResourceContext },
166
158
  Error
167
159
  >;
168
160
 
169
- const resolveConfigAwareProvidedProvision = (
161
+ const resolveConfigAwareProvidedResource = (
170
162
  ctx: TrailContext,
171
- declaredProvision: AnyProvision,
163
+ declaredResource: AnyResource,
172
164
  configValues: ConfigValues | undefined
173
165
  ): ConfigAwareResolution => {
174
- const configResult = resolveProvisionConfig(declaredProvision, configValues);
166
+ const configResult = resolveResourceConfig(declaredResource, configValues);
175
167
  if (configResult.isErr()) {
176
168
  return configResult;
177
169
  }
178
170
 
179
- const provisionContext = toProvisionContext(ctx, configResult.value);
180
- const provided = getProvidedProvision(
171
+ const resourceContext = toResourceContext(ctx, configResult.value);
172
+ const provided = getProvidedResource(
181
173
  ctx,
182
174
  undefined,
183
- declaredProvision,
184
- provisionContext
175
+ declaredResource,
176
+ resourceContext
185
177
  );
186
178
 
187
179
  return provided
188
180
  ? Result.ok({ kind: 'provided', value: provided.unwrap() })
189
- : Result.ok({ kind: 'context', provisionContext });
181
+ : Result.ok({ kind: 'context', resourceContext });
190
182
  };
191
183
 
192
184
  // ---------------------------------------------------------------------------
193
185
  // Instance creation
194
186
  // ---------------------------------------------------------------------------
195
187
 
196
- const toInternalProvisionError = (
197
- id: string,
198
- error: unknown
199
- ): InternalError => {
188
+ const toInternalResourceError = (id: string, error: unknown): InternalError => {
200
189
  const cause = error instanceof Error ? error : undefined;
201
190
  const message = cause?.message ?? String(error);
202
- return new InternalError(`Provision "${id}" failed to resolve: ${message}`, {
191
+ return new InternalError(`Resource "${id}" failed to resolve: ${message}`, {
203
192
  ...(cause ? { cause } : {}),
204
- context: { provisionId: id },
193
+ context: { resourceId: id },
205
194
  });
206
195
  };
207
196
 
208
- const getSingletonProvisionCache = (
209
- declaredProvision: AnyProvision
197
+ const getSingletonResourceCache = (
198
+ declaredResource: AnyResource
210
199
  ): Map<string, unknown> => {
211
- const existing = singletonProvisions.get(declaredProvision);
200
+ const existing = singletonResources.get(declaredResource);
212
201
  if (existing !== undefined) {
213
202
  return existing;
214
203
  }
215
204
 
216
205
  const created = new Map<string, unknown>();
217
- singletonProvisions.set(declaredProvision, created);
206
+ singletonResources.set(declaredResource, created);
218
207
  return created;
219
208
  };
220
209
 
221
- const doCreateProvisionInstance = async (
222
- declaredProvision: AnyProvision,
223
- provisionContext: ProvisionContext
210
+ const doCreateResourceInstance = async (
211
+ declaredResource: AnyResource,
212
+ resourceContext: ResourceContext
224
213
  ): Promise<Result<unknown, Error>> => {
225
214
  try {
226
- const created = await declaredProvision.create(provisionContext);
215
+ const created = await declaredResource.create(resourceContext);
227
216
  if (created.isErr()) {
228
217
  return Result.err(created.error);
229
218
  }
230
219
 
231
220
  const instance = created.unwrap();
232
- getSingletonProvisionCache(declaredProvision).set(
233
- toProvisionContextKey(provisionContext),
221
+ getSingletonResourceCache(declaredResource).set(
222
+ toResourceContextKey(resourceContext),
234
223
  instance
235
224
  );
236
225
  return Result.ok(instance);
237
226
  } catch (error: unknown) {
238
- return Result.err(toInternalProvisionError(declaredProvision.id, error));
227
+ return Result.err(toInternalResourceError(declaredResource.id, error));
239
228
  }
240
229
  };
241
230
 
242
231
  const trackPendingCreation = (
243
- declaredProvision: AnyProvision,
232
+ declaredResource: AnyResource,
244
233
  key: string,
245
234
  promise: Promise<Result<unknown, Error>>
246
235
  ): void => {
247
- const pending = pendingCreations.get(declaredProvision);
236
+ const pending = pendingCreations.get(declaredResource);
248
237
  if (pending) {
249
238
  pending.set(key, promise);
250
239
  } else {
251
- pendingCreations.set(declaredProvision, new Map([[key, promise]]));
240
+ pendingCreations.set(declaredResource, new Map([[key, promise]]));
252
241
  }
253
242
  };
254
243
 
255
244
  /**
256
- * Deduplicates concurrent creation of the same provision singleton.
257
- * If a creation is already in flight for this provision x context key,
245
+ * Deduplicates concurrent creation of the same resource singleton.
246
+ * If a creation is already in flight for this resource x context key,
258
247
  * returns the existing promise instead of spawning a second factory call.
259
248
  */
260
- const createProvisionInstance = async (
261
- declaredProvision: AnyProvision,
262
- provisionContext: ProvisionContext
249
+ const createResourceInstance = async (
250
+ declaredResource: AnyResource,
251
+ resourceContext: ResourceContext
263
252
  ): Promise<Result<unknown, Error>> => {
264
- const key = toProvisionContextKey(provisionContext);
265
- const inflight = pendingCreations.get(declaredProvision)?.get(key);
253
+ const key = toResourceContextKey(resourceContext);
254
+ const inflight = pendingCreations.get(declaredResource)?.get(key);
266
255
  if (inflight) {
267
256
  return inflight;
268
257
  }
269
258
 
270
- const promise = doCreateProvisionInstance(
271
- declaredProvision,
272
- provisionContext
273
- );
274
- trackPendingCreation(declaredProvision, key, promise);
259
+ const promise = doCreateResourceInstance(declaredResource, resourceContext);
260
+ trackPendingCreation(declaredResource, key, promise);
275
261
 
276
262
  try {
277
263
  return await promise;
278
264
  } finally {
279
- pendingCreations.get(declaredProvision)?.delete(key);
265
+ pendingCreations.get(declaredResource)?.delete(key);
280
266
  }
281
267
  };
282
268
 
283
- /** Validate config and resolve a single declared provision. */
284
- const resolveDeclaredProvision = async (
285
- declaredProvision: AnyProvision,
269
+ /** Validate config and resolve a single declared resource. */
270
+ const resolveDeclaredResource = async (
271
+ declaredResource: AnyResource,
286
272
  ctx: TrailContext,
287
- overrides: ProvisionOverrideMap | undefined,
273
+ overrides: ResourceOverrideMap | undefined,
288
274
  configValues: ConfigValues | undefined
289
275
  ): Promise<Result<unknown, Error>> => {
290
276
  // Check overrides/extensions first — skip config validation entirely when
291
- // a provision instance is already provided.
277
+ // a resource instance is already provided.
292
278
  const overrideOrExtension = getOverrideOrExtension(
293
279
  ctx,
294
280
  overrides,
295
- declaredProvision
281
+ declaredResource
296
282
  );
297
283
  if (overrideOrExtension !== undefined) {
298
284
  return overrideOrExtension;
299
285
  }
300
286
 
301
287
  // Resolve config before consulting the singleton cache so config-aware
302
- // provisions use the same canonical context for cache reads and writes.
303
- const configAwareProvision = resolveConfigAwareProvidedProvision(
288
+ // resources use the same canonical context for cache reads and writes.
289
+ const configAwareResource = resolveConfigAwareProvidedResource(
304
290
  ctx,
305
- declaredProvision,
291
+ declaredResource,
306
292
  configValues
307
293
  );
308
- if (configAwareProvision.isErr()) {
309
- return configAwareProvision;
294
+ if (configAwareResource.isErr()) {
295
+ return configAwareResource;
310
296
  }
311
297
 
312
298
  // No provided instance — create via factory.
313
- const resolved = configAwareProvision.unwrap();
299
+ const resolved = configAwareResource.unwrap();
314
300
  if (resolved.kind === 'provided') {
315
301
  return Result.ok(resolved.value);
316
302
  }
317
303
 
318
- return await createProvisionInstance(
319
- declaredProvision,
320
- resolved.provisionContext
304
+ return await createResourceInstance(
305
+ declaredResource,
306
+ resolved.resourceContext
321
307
  );
322
308
  };
323
309
 
324
310
  // ---------------------------------------------------------------------------
325
- // Full trail provision resolution
311
+ // Full trail resource resolution
326
312
  // ---------------------------------------------------------------------------
327
313
 
328
- const withResolvedProvisions = (
314
+ const withResolvedResources = (
329
315
  ctx: TrailContext,
330
- resolvedProvisions: Record<string, unknown>
316
+ resolvedResources: Record<string, unknown>
331
317
  ): TrailContext => {
332
- const extensions = { ...ctx.extensions, ...resolvedProvisions };
318
+ const extensions = { ...ctx.extensions, ...resolvedResources };
333
319
  const resolvedCtx = { ...ctx, extensions } as MutableTrailContext;
334
- const lookup = createProvisionLookup(() => resolvedCtx);
335
- resolvedCtx.provision = lookup;
320
+ const lookup = createResourceLookup(() => resolvedCtx);
321
+ resolvedCtx.resource = lookup;
336
322
  return resolvedCtx;
337
323
  };
338
324
 
339
325
  /**
340
- * Resolve all declared provisions for a trail.
326
+ * Resolve all declared resources for a trail.
341
327
  *
342
- * Validates per-provision config, checks overrides and caches, and creates
343
- * new instances as needed. Returns an enriched context with all provision
328
+ * Validates per-resource config, checks overrides and caches, and creates
329
+ * new instances as needed. Returns an enriched context with all resource
344
330
  * instances injected into extensions.
345
331
  */
346
- export const resolveProvisions = async (
332
+ export const createResources = async (
347
333
  trail: AnyTrail,
348
334
  ctx: TrailContext,
349
- overrides?: ProvisionOverrideMap,
335
+ overrides?: ResourceOverrideMap,
350
336
  configValues?: ConfigValues
351
337
  ): Promise<Result<TrailContext, Error>> => {
352
- const { provisions } = trail;
353
- if (provisions.length === 0) {
338
+ const { resources } = trail;
339
+ if (resources.length === 0) {
354
340
  return Result.ok(ctx);
355
341
  }
356
342
 
357
- const resolvedProvisions: Record<string, unknown> = {};
343
+ const resolvedResources: Record<string, unknown> = {};
358
344
 
359
- for (const declaredProvision of provisions) {
360
- const resolved = await resolveDeclaredProvision(
361
- declaredProvision,
345
+ for (const declaredResource of resources) {
346
+ const resolved = await resolveDeclaredResource(
347
+ declaredResource,
362
348
  ctx,
363
349
  overrides,
364
350
  configValues
@@ -366,8 +352,8 @@ export const resolveProvisions = async (
366
352
  if (resolved.isErr()) {
367
353
  return resolved;
368
354
  }
369
- resolvedProvisions[declaredProvision.id] = resolved.unwrap();
355
+ resolvedResources[declaredResource.id] = resolved.unwrap();
370
356
  }
371
357
 
372
- return Result.ok(withResolvedProvisions(ctx, resolvedProvisions));
358
+ return Result.ok(withResolvedResources(ctx, resolvedResources));
373
359
  };
@@ -0,0 +1,155 @@
1
+ import { InternalError } from './errors.js';
2
+ import type { Result } from './result.js';
3
+ import type { AnySignal } from './signal.js';
4
+ import type { ResourceLookup, TrailContext } from './types.js';
5
+ import type { z } from 'zod';
6
+
7
+ /**
8
+ * Stable process-scoped fields available when constructing a resource.
9
+ *
10
+ * Resources are app-level singletons, so they intentionally do not receive
11
+ * the full per-request TrailContext. When a resource declares a `config` schema,
12
+ * the validated config is passed as `svc.config`.
13
+ */
14
+ export type ResourceContext<C = unknown> = Pick<
15
+ TrailContext,
16
+ 'cwd' | 'env' | 'workspaceRoot'
17
+ > & {
18
+ readonly config: C;
19
+ };
20
+
21
+ /**
22
+ * Everything needed to describe a resource before a factory is introduced.
23
+ *
24
+ * When `config` is a Zod schema, the `create` callback receives
25
+ * `ResourceContext<C>` with the validated config value.
26
+ */
27
+ export interface ResourceSpec<T, C = unknown> {
28
+ /** Create the resource instance from stable process-scoped context. */
29
+ readonly create: (
30
+ svc: ResourceContext<C>
31
+ ) => Result<T, Error> | Promise<Result<T, Error>>;
32
+ /** Config schema — when present, config is validated and passed to `create`. */
33
+ readonly config?: z.ZodType<C> | undefined;
34
+ /** Optional cleanup performed when the hosting trailhead shuts down. */
35
+ readonly dispose?: ((resource: T) => void | Promise<void>) | undefined;
36
+ /** Optional operational readiness probe for introspection tooling. */
37
+ readonly health?:
38
+ | ((
39
+ resource: T
40
+ ) => Result<unknown, Error> | Promise<Result<unknown, Error>>)
41
+ | undefined;
42
+ /** Optional test factory used by higher-level helpers. */
43
+ readonly mock?: (() => T | Promise<T>) | undefined;
44
+ /** Human-readable description. */
45
+ readonly description?: string | undefined;
46
+ /** Arbitrary meta for tooling and filtering. */
47
+ readonly meta?: Readonly<Record<string, unknown>> | undefined;
48
+ /** Signals projected or owned by this resource. */
49
+ readonly signals?: readonly AnySignal[] | undefined;
50
+ }
51
+
52
+ /** A typed resource definition. */
53
+ export interface Resource<T> extends ResourceSpec<T> {
54
+ readonly kind: 'resource';
55
+ readonly id: string;
56
+ /** Read the resolved resource instance from a trail context. */
57
+ from(ctx: TrailContext): T;
58
+ }
59
+
60
+ /**
61
+ * Existential type for heterogeneous resource collections.
62
+ *
63
+ * `Resource<T>` includes function parameters in `dispose`/`health`, so
64
+ * `unknown` is too narrow for mixed resource arrays. `any` is the correct
65
+ * existential here.
66
+ */
67
+ // oxlint-disable-next-line no-explicit-any -- existential type for heterogeneous resource collections
68
+ export type AnyResource = Resource<any>;
69
+
70
+ /** Explicit runtime overrides keyed by resource ID. */
71
+ export type ResourceOverrideMap = Readonly<Record<string, unknown>>;
72
+
73
+ const getResourceId = <T>(
74
+ resourceOrId: string | Pick<Resource<T>, 'id'>
75
+ ): string =>
76
+ typeof resourceOrId === 'string' ? resourceOrId : resourceOrId.id;
77
+
78
+ const getResourceInstance = <T>(
79
+ ctx: Pick<TrailContext, 'extensions'>,
80
+ resourceOrId: string | Pick<Resource<T>, 'id'>
81
+ ): T => {
82
+ const id = getResourceId(resourceOrId);
83
+ return ctx.extensions?.[id] as T;
84
+ };
85
+
86
+ const hasResourceInstance = (
87
+ ctx: Pick<TrailContext, 'extensions'>,
88
+ resourceOrId: string | Pick<AnyResource, 'id'>
89
+ ): boolean => Object.hasOwn(ctx.extensions ?? {}, getResourceId(resourceOrId));
90
+
91
+ /** Create a `ctx.resource(...)` accessor bound to a concrete context snapshot. */
92
+ export const createResourceLookup = (
93
+ getContext: () => Pick<TrailContext, 'extensions'>
94
+ ): ResourceLookup =>
95
+ ((resourceOrId: string | Pick<AnyResource, 'id'>) => {
96
+ const id = getResourceId(resourceOrId);
97
+ const ctx = getContext();
98
+ if (!hasResourceInstance(ctx, id)) {
99
+ throw new InternalError(`Resource "${id}" not provisioned in context`);
100
+ }
101
+ return getResourceInstance(ctx, id);
102
+ }) as ResourceLookup;
103
+
104
+ /**
105
+ * Create a typed resource definition.
106
+ *
107
+ * The resource object is inert until a later execution branch resolves concrete
108
+ * instances into TrailContext extensions.
109
+ */
110
+ export const resource = <T>(id: string, spec: ResourceSpec<T>): Resource<T> =>
111
+ (() => {
112
+ if (id.includes(':')) {
113
+ throw new InternalError(
114
+ `Resource "${id}" is invalid because resource ids may not contain ":"`
115
+ );
116
+ }
117
+
118
+ return Object.freeze({
119
+ ...spec,
120
+ from(ctx: TrailContext): T {
121
+ const lookup = ctx.resource ?? createResourceLookup(() => ctx);
122
+ return lookup(this);
123
+ },
124
+ id,
125
+ kind: 'resource' as const,
126
+ });
127
+ })();
128
+
129
+ /** Narrow unknown values to resource definitions during topo discovery. */
130
+ export const isResource = (value: unknown): value is AnyResource => {
131
+ if (typeof value !== 'object' || value === null) {
132
+ return false;
133
+ }
134
+ const v = value as { kind?: unknown; id?: unknown };
135
+ return v.kind === 'resource' && typeof v.id === 'string';
136
+ };
137
+
138
+ /**
139
+ * Return the first duplicate resource ID in a collection, if any.
140
+ *
141
+ * This supports later topo registration without each caller duplicating the
142
+ * same scan logic.
143
+ */
144
+ export const findDuplicateResourceId = (
145
+ resources: readonly Pick<AnyResource, 'id'>[]
146
+ ): string | undefined => {
147
+ const seen = new Set<string>();
148
+ for (const candidate of resources) {
149
+ if (seen.has(candidate.id)) {
150
+ return candidate.id;
151
+ }
152
+ seen.add(candidate.id);
153
+ }
154
+ return undefined;
155
+ };
package/src/run.ts CHANGED
@@ -26,9 +26,10 @@ export type RunOptions = ExecuteTrailOptions;
26
26
  * Execute a trail by ID from a topo without mounting a trailhead.
27
27
  *
28
28
  * Resolves the trail from the topo, then runs it through the standard
29
- * `executeTrail` pipeline. Returns `Result.err(NotFoundError)` if the
30
- * trail ID is not registered. Never throws — unexpected exceptions are
31
- * returned as `Result.err(InternalError)`.
29
+ * `executeTrail` pipeline with `topo` threaded so `ctx.fire()` is bound
30
+ * to the producer's context inside `executeTrail`. Returns
31
+ * `Result.err(NotFoundError)` if the trail ID is not registered. Never
32
+ * throws — unexpected exceptions are returned as `Result.err(InternalError)`.
32
33
  *
33
34
  * @example
34
35
  * ```typescript
@@ -50,5 +51,5 @@ export const run = (
50
51
  )
51
52
  );
52
53
  }
53
- return executeTrail(trail, input, options);
54
+ return executeTrail(trail, input, { ...options, topo });
54
55
  };
@@ -18,6 +18,7 @@ import {
18
18
  RateLimitError,
19
19
  NetworkError,
20
20
  InternalError,
21
+ DerivationError,
21
22
  AuthError,
22
23
  CancelledError,
23
24
  isTrailsError,
@@ -100,6 +101,7 @@ const errorConstructorsByName: Record<string, ErrorFactory> = {
100
101
  AuthError: (msg, opts) => new AuthError(msg, opts),
101
102
  CancelledError: (msg, opts) => new CancelledError(msg, opts),
102
103
  ConflictError: (msg, opts) => new ConflictError(msg, opts),
104
+ DerivationError: (msg, opts) => new DerivationError(msg, opts),
103
105
  InternalError: (msg, opts) => new InternalError(msg, opts),
104
106
  NetworkError: (msg, opts) => new NetworkError(msg, opts),
105
107
  NotFoundError: (msg, opts) => new NotFoundError(msg, opts),