@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/topo.ts CHANGED
@@ -2,46 +2,87 @@
2
2
  * Application entry point — scans module exports to build a topology graph.
3
3
  */
4
4
 
5
+ import type { AnyContour } from './contour.js';
5
6
  import { ValidationError } from './errors.js';
6
- import type { AnySignal } from './event.js';
7
- import type { AnyProvision } from './provision.js';
8
- import { isProvision } from './provision.js';
7
+ import type { ActivationEntry } from './activation-source.js';
8
+ import {
9
+ getLateBoundSignalRef,
10
+ parseLateBoundSignalMarker,
11
+ } from './signal-ref.js';
12
+ import type { Layer } from './layer.js';
13
+ import {
14
+ hasObserveCapabilities,
15
+ isLogger,
16
+ isLogSink,
17
+ isObserveConfig,
18
+ isObserveInput,
19
+ isTraceSink,
20
+ normalizeObserve,
21
+ } from './observe.js';
22
+ import type { ObserveConfig, TopoOptions } from './observe.js';
23
+ import type { AnySignal } from './signal.js';
24
+ import type { AnyResource } from './resource.js';
25
+ import { isResource } from './resource.js';
9
26
  import type { AnyTrail } from './trail.js';
10
27
 
11
28
  // ---------------------------------------------------------------------------
12
29
  // Public types
13
30
  // ---------------------------------------------------------------------------
14
31
 
32
+ export interface TopoIdentity {
33
+ readonly name: string;
34
+ readonly version?: string;
35
+ readonly description?: string;
36
+ }
37
+
15
38
  export interface Topo {
16
39
  readonly name: string;
40
+ readonly version?: string;
41
+ readonly description?: string;
42
+ readonly contours: ReadonlyMap<string, AnyContour>;
17
43
  readonly trails: ReadonlyMap<string, AnyTrail>;
18
44
  readonly signals: ReadonlyMap<string, AnySignal>;
19
- readonly provisions: ReadonlyMap<string, AnyProvision>;
45
+ readonly resources: ReadonlyMap<string, AnyResource>;
46
+ readonly observe?: ObserveConfig | undefined;
47
+ /**
48
+ * Typed layers attached at topo scope (always present, default `[]`).
49
+ *
50
+ * The CLI/MCP/HTTP surfaces forward these into `executeTrail` as
51
+ * `topoLayers`, where they are composed outermost in the layer chain.
52
+ * The final composition order is `topo → surface → trail → blaze`
53
+ * (outermost-first).
54
+ */
55
+ readonly layers: readonly Layer[];
20
56
  readonly count: number;
21
- readonly provisionCount: number;
57
+ readonly contourCount: number;
58
+ readonly resourceCount: number;
59
+ getContour(name: string): AnyContour | undefined;
22
60
  get(id: string): AnyTrail | undefined;
23
- getProvision(id: string): AnyProvision | undefined;
61
+ getResource(id: string): AnyResource | undefined;
62
+ hasContour(name: string): boolean;
24
63
  has(id: string): boolean;
25
- hasProvision(id: string): boolean;
64
+ hasResource(id: string): boolean;
65
+ contourIds(): string[];
26
66
  ids(): string[];
27
- provisionIds(): string[];
67
+ resourceIds(): string[];
68
+ listContours(): AnyContour[];
28
69
  list(): AnyTrail[];
29
70
  listSignals(): AnySignal[];
30
- listProvisions(): AnyProvision[];
71
+ listResources(): AnyResource[];
31
72
  }
32
73
 
33
74
  // ---------------------------------------------------------------------------
34
75
  // Kind discriminant check
35
76
  // ---------------------------------------------------------------------------
36
77
 
37
- type Registrable = AnyTrail | AnySignal | AnyProvision;
78
+ type Registrable = AnyContour | AnyTrail | AnySignal | AnyResource;
38
79
 
39
80
  const isRegistrable = (value: unknown): value is Registrable => {
40
81
  if (typeof value !== 'object' || value === null) {
41
82
  return false;
42
83
  }
43
84
  const { kind } = value as Record<string, unknown>;
44
- return kind === 'trail' || kind === 'signal';
85
+ return kind === 'contour' || kind === 'trail' || kind === 'signal';
45
86
  };
46
87
 
47
88
  // ---------------------------------------------------------------------------
@@ -49,23 +90,37 @@ const isRegistrable = (value: unknown): value is Registrable => {
49
90
  // ---------------------------------------------------------------------------
50
91
 
51
92
  const createTopo = (
52
- name: string,
93
+ identity: TopoIdentity,
94
+ contours: ReadonlyMap<string, AnyContour>,
53
95
  trails: ReadonlyMap<string, AnyTrail>,
54
96
  signals: ReadonlyMap<string, AnySignal>,
55
- provisions: ReadonlyMap<string, AnyProvision>
97
+ resources: ReadonlyMap<string, AnyResource>,
98
+ observe: ObserveConfig | undefined,
99
+ layers: readonly Layer[]
56
100
  ): Topo => ({
101
+ contourCount: contours.size,
102
+ contourIds(): string[] {
103
+ return [...contours.keys()];
104
+ },
105
+ contours,
57
106
  count: trails.size,
58
107
  get(id: string): AnyTrail | undefined {
59
108
  return trails.get(id);
60
109
  },
61
- getProvision(id: string): AnyProvision | undefined {
62
- return provisions.get(id);
110
+ getContour(contourName: string): AnyContour | undefined {
111
+ return contours.get(contourName);
112
+ },
113
+ getResource(id: string): AnyResource | undefined {
114
+ return resources.get(id);
63
115
  },
64
116
  has(id: string): boolean {
65
117
  return trails.has(id);
66
118
  },
67
- hasProvision(id: string): boolean {
68
- return provisions.has(id);
119
+ hasContour(contourName: string): boolean {
120
+ return contours.has(contourName);
121
+ },
122
+ hasResource(id: string): boolean {
123
+ return resources.has(id);
69
124
  },
70
125
  ids(): string[] {
71
126
  return [...trails.keys()];
@@ -74,21 +129,29 @@ const createTopo = (
74
129
  list(): AnyTrail[] {
75
130
  return [...trails.values()];
76
131
  },
77
- listProvisions(): AnyProvision[] {
78
- return [...provisions.values()];
132
+ listContours(): AnyContour[] {
133
+ return [...contours.values()];
134
+ },
135
+ listResources(): AnyResource[] {
136
+ return [...resources.values()];
79
137
  },
80
138
 
81
139
  listSignals(): AnySignal[] {
82
140
  return [...signals.values()];
83
141
  },
84
142
 
85
- name,
86
- provisionCount: provisions.size,
87
-
88
- provisionIds(): string[] {
89
- return [...provisions.keys()];
143
+ name: identity.name,
144
+ ...(identity.version !== undefined && { version: identity.version }),
145
+ ...(identity.description !== undefined && {
146
+ description: identity.description,
147
+ }),
148
+ ...(observe !== undefined && { observe }),
149
+ layers,
150
+ resourceCount: resources.size,
151
+ resourceIds(): string[] {
152
+ return [...resources.keys()];
90
153
  },
91
- provisions,
154
+ resources,
92
155
  signals,
93
156
  trails,
94
157
  });
@@ -97,35 +160,279 @@ const createTopo = (
97
160
  // topo()
98
161
  // ---------------------------------------------------------------------------
99
162
 
163
+ const registerUnique = <T>(
164
+ collection: Map<string, T>,
165
+ id: string,
166
+ value: T,
167
+ duplicateMessage: string
168
+ ): void => {
169
+ const existing = collection.get(id);
170
+ if (existing === value) {
171
+ return;
172
+ }
173
+ if (existing !== undefined) {
174
+ throw new ValidationError(duplicateMessage);
175
+ }
176
+ collection.set(id, value);
177
+ };
178
+
179
+ const registerContour = (
180
+ contour: AnyContour,
181
+ contours: Map<string, AnyContour>
182
+ ): void => {
183
+ registerUnique(
184
+ contours,
185
+ contour.name,
186
+ contour,
187
+ `Duplicate contour name: "${contour.name}"`
188
+ );
189
+ };
190
+
191
+ const registerResourceValue = (
192
+ resource: AnyResource,
193
+ resources: Map<string, AnyResource>
194
+ ): void => {
195
+ registerUnique(
196
+ resources,
197
+ resource.id,
198
+ resource,
199
+ `Duplicate resource ID: "${resource.id}"`
200
+ );
201
+ };
202
+
203
+ const registerSignal = (
204
+ signal: AnySignal,
205
+ signals: Map<string, AnySignal>
206
+ ): void => {
207
+ registerUnique(
208
+ signals,
209
+ signal.id,
210
+ signal,
211
+ `Duplicate signal ID: "${signal.id}"`
212
+ );
213
+ };
214
+
215
+ const registerResourceSignals = (
216
+ resource: AnyResource,
217
+ signals: Map<string, AnySignal>
218
+ ): void => {
219
+ for (const derived of resource.signals ?? []) {
220
+ registerSignal(derived, signals);
221
+ }
222
+ };
223
+
224
+ const registerTrail = (
225
+ trail: AnyTrail,
226
+ trails: Map<string, AnyTrail>
227
+ ): void => {
228
+ registerUnique(trails, trail.id, trail, `Duplicate trail ID: "${trail.id}"`);
229
+ };
230
+
231
+ const registerLateBoundSignalId = (
232
+ byToken: Map<string, Set<string>>,
233
+ signal: AnySignal
234
+ ): void => {
235
+ const ref = getLateBoundSignalRef(signal);
236
+ if (!ref) {
237
+ return;
238
+ }
239
+
240
+ const ids = byToken.get(ref.token) ?? new Set<string>();
241
+ ids.add(signal.id);
242
+ byToken.set(ref.token, ids);
243
+ };
244
+
245
+ const collectLateBoundSignalIdsByToken = (
246
+ resources: ReadonlyMap<string, AnyResource>
247
+ ): ReadonlyMap<string, readonly string[]> => {
248
+ const byToken = new Map<string, Set<string>>();
249
+
250
+ for (const resource of resources.values()) {
251
+ for (const signal of resource.signals ?? []) {
252
+ registerLateBoundSignalId(byToken, signal);
253
+ }
254
+ }
255
+
256
+ return new Map(
257
+ [...byToken.entries()].map(([token, ids]) => [token, [...ids]])
258
+ );
259
+ };
260
+
261
+ const resolveLateBoundSignalId = (
262
+ trailId: string,
263
+ signalId: string,
264
+ lateBoundSignalIdsByToken: ReadonlyMap<string, readonly string[]>
265
+ ): string => {
266
+ const marker = parseLateBoundSignalMarker(signalId);
267
+ if (!marker) {
268
+ return signalId;
269
+ }
270
+
271
+ const matches = lateBoundSignalIdsByToken.get(marker.token) ?? [];
272
+ if (matches.length === 1) {
273
+ return matches[0] ?? signalId;
274
+ }
275
+
276
+ if (matches.length === 0) {
277
+ // Intentional throw: split-topo composition (where a trail and the
278
+ // store that backs its signals live in different topos) is not yet
279
+ // supported. Failing loudly here surfaces the case at assembly time
280
+ // instead of silently producing a trail with an unresolved store
281
+ // reference that would misbehave at runtime.
282
+ throw new ValidationError(
283
+ `Trail "${trailId}" references store-derived signal "${marker.displayId}", but no resource bound in this topo exposes it. ` +
284
+ 'This usually means the store that backs this signal is not bound in this topo. ' +
285
+ `Bind the store via resource() in the same topo() call as "${trailId}", or compose this topo with the topo that binds the store. ` +
286
+ 'Splitting a trail and its backing store across independent topos is not yet supported.'
287
+ );
288
+ }
289
+
290
+ throw new ValidationError(
291
+ `Trail "${trailId}" references late-bound signal "${marker.displayId}" but it resolves to multiple bound resource signals: ${matches.join(', ')}. Use canonical scoped ids when the same store definition is bound more than once.`
292
+ );
293
+ };
294
+
295
+ const resolveTrailSignalIds = (
296
+ trailId: string,
297
+ signalIds: readonly string[],
298
+ lateBoundSignalIdsByToken: ReadonlyMap<string, readonly string[]>
299
+ ): { changed: boolean; ids: readonly string[] } => {
300
+ let changed = false;
301
+ const ids = Object.freeze(
302
+ signalIds.map((signalId) => {
303
+ const resolved = resolveLateBoundSignalId(
304
+ trailId,
305
+ signalId,
306
+ lateBoundSignalIdsByToken
307
+ );
308
+ changed ||= resolved !== signalId;
309
+ return resolved;
310
+ })
311
+ );
312
+
313
+ return { changed, ids };
314
+ };
315
+
316
+ const resolveTrailActivationSources = (
317
+ trailId: string,
318
+ activations: readonly ActivationEntry[],
319
+ lateBoundSignalIdsByToken: ReadonlyMap<string, readonly string[]>
320
+ ): { changed: boolean; activations: readonly ActivationEntry[] } => {
321
+ let changed = false;
322
+ const resolved = Object.freeze(
323
+ activations.map((entry) => {
324
+ if (entry.source.kind !== 'signal') {
325
+ return entry;
326
+ }
327
+
328
+ const resolvedId = resolveLateBoundSignalId(
329
+ trailId,
330
+ entry.source.id,
331
+ lateBoundSignalIdsByToken
332
+ );
333
+ if (resolvedId === entry.source.id) {
334
+ return entry;
335
+ }
336
+
337
+ changed = true;
338
+ return Object.freeze({
339
+ ...entry,
340
+ source: Object.freeze({ ...entry.source, id: resolvedId }),
341
+ });
342
+ })
343
+ );
344
+
345
+ return { activations: resolved, changed };
346
+ };
347
+
348
+ const finalizeTrailSignals = (
349
+ trails: ReadonlyMap<string, AnyTrail>,
350
+ resources: ReadonlyMap<string, AnyResource>
351
+ ): Map<string, AnyTrail> => {
352
+ const lateBoundSignalIdsByToken = collectLateBoundSignalIdsByToken(resources);
353
+ const finalized = new Map<string, AnyTrail>();
354
+
355
+ for (const trail of trails.values()) {
356
+ const resolvedFires = resolveTrailSignalIds(
357
+ trail.id,
358
+ trail.fires ?? [],
359
+ lateBoundSignalIdsByToken
360
+ );
361
+ const resolvedOn = resolveTrailSignalIds(
362
+ trail.id,
363
+ trail.on ?? [],
364
+ lateBoundSignalIdsByToken
365
+ );
366
+ const resolvedActivationSources = resolveTrailActivationSources(
367
+ trail.id,
368
+ trail.activationSources ?? [],
369
+ lateBoundSignalIdsByToken
370
+ );
371
+
372
+ if (
373
+ !resolvedFires.changed &&
374
+ !resolvedOn.changed &&
375
+ !resolvedActivationSources.changed
376
+ ) {
377
+ finalized.set(trail.id, trail);
378
+ continue;
379
+ }
380
+
381
+ finalized.set(
382
+ trail.id,
383
+ Object.freeze({
384
+ ...trail,
385
+ activationSources: resolvedActivationSources.activations,
386
+ fires: resolvedFires.ids,
387
+ on: resolvedOn.ids,
388
+ })
389
+ );
390
+ }
391
+
392
+ return finalized;
393
+ };
394
+
100
395
  /** Register a single registrable value into the appropriate map. */
101
396
  const register = (
102
397
  value: Registrable,
398
+ contours: Map<string, AnyContour>,
103
399
  trails: Map<string, AnyTrail>,
104
400
  signals: Map<string, AnySignal>,
105
- provisions: Map<string, AnyProvision>
401
+ resources: Map<string, AnyResource>
106
402
  ): void => {
107
- const { id } = value as { id: string };
108
- const registrars: Record<string, () => void> = {
109
- provision: () => {
110
- if (provisions.has(id)) {
111
- throw new ValidationError(`Duplicate provision ID: "${id}"`);
112
- }
113
- provisions.set(id, value as AnyProvision);
114
- },
115
- signal: () => {
116
- if (signals.has(id)) {
117
- throw new ValidationError(`Duplicate signal ID: "${id}"`);
118
- }
119
- signals.set(id, value as AnySignal);
120
- },
121
- trail: () => {
122
- if (trails.has(id)) {
123
- throw new ValidationError(`Duplicate trail ID: "${id}"`);
124
- }
125
- trails.set(id, value as AnyTrail);
126
- },
127
- };
128
- registrars[value.kind]?.();
403
+ switch (value.kind) {
404
+ case 'contour': {
405
+ registerContour(value as AnyContour, contours);
406
+ break;
407
+ }
408
+ case 'resource': {
409
+ registerResourceValue(value as AnyResource, resources);
410
+ break;
411
+ }
412
+ case 'signal': {
413
+ registerSignal(value as AnySignal, signals);
414
+ break;
415
+ }
416
+ case 'trail': {
417
+ registerTrail(value as AnyTrail, trails);
418
+ break;
419
+ }
420
+ default: {
421
+ throw new ValidationError('Unsupported registrable value in topo()');
422
+ }
423
+ }
424
+ };
425
+
426
+ const registerTrailContours = (
427
+ trail: AnyTrail,
428
+ contours: Map<string, AnyContour>,
429
+ trails: Map<string, AnyTrail>,
430
+ signals: Map<string, AnySignal>,
431
+ resources: Map<string, AnyResource>
432
+ ): void => {
433
+ for (const contour of trail.contours ?? []) {
434
+ register(contour, contours, trails, signals, resources);
435
+ }
129
436
  };
130
437
 
131
438
  const markUniqueObject = (
@@ -144,41 +451,385 @@ const markUniqueObject = (
144
451
 
145
452
  const registerModuleValue = (
146
453
  value: unknown,
454
+ contours: Map<string, AnyContour>,
147
455
  trails: Map<string, AnyTrail>,
148
456
  signals: Map<string, AnySignal>,
149
- provisions: Map<string, AnyProvision>
457
+ resources: Map<string, AnyResource>
150
458
  ): void => {
151
- if (isProvision(value) || isRegistrable(value)) {
152
- register(value, trails, signals, provisions);
459
+ if (isResource(value) || isRegistrable(value)) {
460
+ register(value, contours, trails, signals, resources);
461
+ }
462
+
463
+ if (isResource(value)) {
464
+ registerResourceSignals(value, signals);
465
+ }
466
+
467
+ if (
468
+ typeof value === 'object' &&
469
+ value !== null &&
470
+ (value as { kind?: unknown }).kind === 'trail'
471
+ ) {
472
+ registerTrailContours(
473
+ value as AnyTrail,
474
+ contours,
475
+ trails,
476
+ signals,
477
+ resources
478
+ );
153
479
  }
154
480
  };
155
481
 
156
482
  const registerModuleValues = (
157
483
  mod: Record<string, unknown>,
484
+ contours: Map<string, AnyContour>,
158
485
  trails: Map<string, AnyTrail>,
159
486
  signals: Map<string, AnySignal>,
160
- provisions: Map<string, AnyProvision>
487
+ resources: Map<string, AnyResource>
161
488
  ): void => {
162
489
  const seenValues = new WeakSet<object>();
163
490
  for (const value of Object.values(mod)) {
164
491
  if (!markUniqueObject(value, seenValues)) {
165
492
  continue;
166
493
  }
167
- registerModuleValue(value, trails, signals, provisions);
494
+ registerModuleValue(value, contours, trails, signals, resources);
495
+ }
496
+ };
497
+
498
+ const TOPO_OPTION_KEYS = ['layers', 'observe'] as const;
499
+ const TOPO_OPTION_KEY_SET: ReadonlySet<string> = new Set(TOPO_OPTION_KEYS);
500
+
501
+ /**
502
+ * Brand symbol applied by `topo.options()`. The presence of this symbol
503
+ * marks an object as an explicit `TopoOptions` payload, which is the
504
+ * unambiguous way to disambiguate a trailing options object from a
505
+ * trailing module export. Use `topo.options()` whenever a module might
506
+ * legitimately export only fields whose names collide with topo options
507
+ * (for example a module whose sole export is `observe`).
508
+ */
509
+ const TOPO_OPTIONS_BRAND: unique symbol = Symbol('trails.topo.options');
510
+
511
+ const hasOptionsBrand = (value: object): boolean =>
512
+ (value as { [TOPO_OPTIONS_BRAND]?: true })[TOPO_OPTIONS_BRAND] === true;
513
+
514
+ const looksLikeTopoOptionsShape = (value: object): boolean => {
515
+ const keys = Object.keys(value);
516
+ if (keys.length === 0) {
517
+ return false;
168
518
  }
519
+ return keys.every((key) => TOPO_OPTION_KEY_SET.has(key));
169
520
  };
170
521
 
171
- export const topo = (
172
- name: string,
173
- ...modules: Record<string, unknown>[]
522
+ const detectUnknownOptionKeys = (value: object): readonly string[] => {
523
+ const unknown: string[] = [];
524
+ for (const key of Object.keys(value)) {
525
+ if (!TOPO_OPTION_KEY_SET.has(key)) {
526
+ unknown.push(key);
527
+ }
528
+ }
529
+ return unknown;
530
+ };
531
+
532
+ const hasRegistrableKind = (value: unknown): boolean => {
533
+ if (typeof value !== 'object' || value === null) {
534
+ return false;
535
+ }
536
+ const { kind } = value as { kind?: unknown };
537
+ return (
538
+ kind === 'contour' ||
539
+ kind === 'trail' ||
540
+ kind === 'signal' ||
541
+ kind === 'resource'
542
+ );
543
+ };
544
+
545
+ /**
546
+ * When a branded `topo.options()` payload carries a bare `LogSink` in the
547
+ * `observe` slot, rewrite it to the explicit `{ log: sink }` form. The brand
548
+ * already signals "this is options, not a module"; once that is settled, a
549
+ * bare `LogSink` unambiguously names a log target and should not be rejected
550
+ * as ambiguous downstream.
551
+ *
552
+ * Bare `TraceSink` values (no `name` field) are left untouched — they already
553
+ * round-trip through `normalizeObserve` via the `isTraceSink` fallthrough.
554
+ * Already-disambiguated shapes (`{ log }`, `{ trace }`, `Logger`,
555
+ * `ObserveCapable`, etc.) are also left untouched.
556
+ */
557
+ const disambiguateBrandedObserve = (options: TopoOptions): TopoOptions => {
558
+ const { observe } = options;
559
+ if (
560
+ observe === undefined ||
561
+ isLogger(observe) ||
562
+ isObserveConfig(observe) ||
563
+ hasObserveCapabilities(observe)
564
+ ) {
565
+ return options;
566
+ }
567
+ if (isLogSink(observe)) {
568
+ return { ...options, observe: { log: observe } };
569
+ }
570
+ return options;
571
+ };
572
+
573
+ /**
574
+ * Decide whether the trailing argument should be treated as a
575
+ * `TopoOptions` payload, a module export, or rejected as ambiguous.
576
+ *
577
+ * Resolution rules (in order):
578
+ * 1. Branded via `topo.options()` → always options. Unknown option
579
+ * keys throw, and downstream `normalizeObserve` rejects malformed
580
+ * values. A bare `LogSink` (`{ name, write }`) in the `observe`
581
+ * slot is auto-routed to `{ log: sink }` so the brand is the
582
+ * complete escape hatch the docs promise.
583
+ * 2. The shape does not look like `TopoOptions` (mixed keys or no
584
+ * keys) → module.
585
+ * 3. The trailing arg is a registrable module export
586
+ * (`kind: 'trail' | 'contour' | …`) under a known option key →
587
+ * module. Preserves the "module exporting a single trail named
588
+ * `observe`" case that the warden and existing apps rely on.
589
+ * 4. The `observe` value is a bare `LogSink` or `TraceSink` (a sink
590
+ * shape that could equally plausibly be a module export named
591
+ * `observe`) → throw, since the call is genuinely ambiguous.
592
+ * `topo.options()` exists to disambiguate.
593
+ * 5. The `observe` value is otherwise a recognizable `ObserveInput`
594
+ * (`Logger`, `ObserveConfig`, or `ObserveCapable`) → options.
595
+ * Those shapes carry enough structure that they cannot be
596
+ * mistaken for a generic module export.
597
+ * 6. Otherwise (non-sink helper object, function, primitive, etc.)
598
+ * → module. The non-registrable export is silently ignored, the
599
+ * same as any other unrecognized value in a module record.
600
+ */
601
+ const classifyTrailingArgument = (
602
+ value: unknown
603
+ ):
604
+ | { readonly kind: 'options'; readonly options: TopoOptions }
605
+ | { readonly kind: 'module' }
606
+ | { readonly kind: 'invalid'; readonly message: string } => {
607
+ if (typeof value !== 'object' || value === null) {
608
+ return { kind: 'module' };
609
+ }
610
+
611
+ if (hasOptionsBrand(value)) {
612
+ const unknown = detectUnknownOptionKeys(value);
613
+ if (unknown.length > 0) {
614
+ return {
615
+ kind: 'invalid',
616
+ message: `topo.options() received unknown option keys: ${unknown
617
+ .map((key) => `"${key}"`)
618
+ .join(', ')}. Expected one of: ${TOPO_OPTION_KEYS.map(
619
+ (key) => `"${key}"`
620
+ ).join(', ')}.`,
621
+ };
622
+ }
623
+ // Branding via `topo.options()` is the documented escape hatch for
624
+ // disambiguating bare-sink shorthand. Route a bare `LogSink` into the
625
+ // explicit `{ log: sink }` slot before handing off to `normalizeObserve`,
626
+ // which would otherwise reject it as ambiguous (a LogSink shape matches
627
+ // both `isLogSink` and `isTraceSink`). A bare TraceSink (no `name`) does
628
+ // not need rewriting because `normalizeObserve` already routes it via
629
+ // the `isTraceSink` fallthrough.
630
+ return {
631
+ kind: 'options',
632
+ options: disambiguateBrandedObserve(value as TopoOptions),
633
+ };
634
+ }
635
+
636
+ if (!looksLikeTopoOptionsShape(value)) {
637
+ return { kind: 'module' };
638
+ }
639
+
640
+ // The shape matches `TopoOptions`. Decide whether the values are
641
+ // valid options, a registrable module export, or a non-registrable
642
+ // helper that should be treated as a (silently ignored) module
643
+ // export.
644
+ const observeValue = (value as TopoOptions).observe;
645
+ const layersValue = (value as TopoOptions).layers;
646
+
647
+ // A module export named `layers` would be a non-registrable array, but
648
+ // an explicit `topo.options({ layers: [...] })` is the documented escape
649
+ // hatch for that disambiguation. Without the brand, accept the trailing
650
+ // argument as options when `layers` is an array and `observe` is either
651
+ // unset or already an unambiguous option shape — this mirrors how
652
+ // `observe`-only options are accepted today.
653
+ if (Array.isArray(layersValue) && observeValue === undefined) {
654
+ return { kind: 'options', options: value as TopoOptions };
655
+ }
656
+
657
+ if (hasRegistrableKind(observeValue)) {
658
+ return { kind: 'module' };
659
+ }
660
+ // Unambiguous option shapes: `Logger`, `ObserveConfig`, and
661
+ // `ObserveCapable` carry enough structure that they cannot be
662
+ // confused with a generic module export. Check these first so the
663
+ // sink-ambiguity guard below does not accidentally reject an
664
+ // `ObserveCapable` whose underlying shape happens to also satisfy
665
+ // `isLogSink` / `isTraceSink`.
666
+ if (
667
+ isLogger(observeValue) ||
668
+ isObserveConfig(observeValue) ||
669
+ hasObserveCapabilities(observeValue)
670
+ ) {
671
+ return { kind: 'options', options: value as TopoOptions };
672
+ }
673
+ // Bare sink shapes (`{ write }` / `{ name, write }`) are genuinely
674
+ // ambiguous — they may equally plausibly be a module export named
675
+ // `observe`. Refuse to guess; require `topo.options()` to make the
676
+ // intent explicit.
677
+ if (isLogSink(observeValue) || isTraceSink(observeValue)) {
678
+ return {
679
+ kind: 'invalid',
680
+ message:
681
+ 'topo() received a trailing argument shaped like `{ observe: sink }` that is ambiguous: ' +
682
+ 'the value matches both a TopoOptions sink and a non-registrable module export. ' +
683
+ 'Wrap the options with `topo.options({ observe: sink })` to disambiguate.',
684
+ };
685
+ }
686
+ if (isObserveInput(observeValue)) {
687
+ // Catch-all for any future `ObserveInput` variant added to the
688
+ // type. Today this branch is unreachable given the guards above.
689
+ return { kind: 'options', options: value as TopoOptions };
690
+ }
691
+ // Non-registrable, non-sink helper. Treat as a module export; the
692
+ // unrecognized value is silently ignored during registration, matching
693
+ // the behavior of any other non-registrable export.
694
+ return { kind: 'module' };
695
+ };
696
+
697
+ const splitTopoArguments = (
698
+ modulesOrOptions: readonly (Record<string, unknown> | TopoOptions)[]
699
+ ): {
700
+ readonly modules: readonly Record<string, unknown>[];
701
+ readonly options: TopoOptions | undefined;
702
+ } => {
703
+ // A branded `topo.options(...)` payload is an explicit user signal and must
704
+ // appear last. If it shows up in a non-trailing position the caller almost
705
+ // certainly intended it as options but lost the configuration silently.
706
+ // Reject it so the misconfiguration is visible at construction.
707
+ for (let i = 0; i < modulesOrOptions.length - 1; i += 1) {
708
+ const arg = modulesOrOptions[i];
709
+ if (typeof arg === 'object' && arg !== null && hasOptionsBrand(arg)) {
710
+ throw new ValidationError(
711
+ `topo.options(...) must be the final argument to topo(); received at position ${i + 2} of ${modulesOrOptions.length + 1}.`
712
+ );
713
+ }
714
+ }
715
+
716
+ const last = modulesOrOptions.at(-1);
717
+ const classification = classifyTrailingArgument(last);
718
+
719
+ if (classification.kind === 'invalid') {
720
+ throw new ValidationError(classification.message);
721
+ }
722
+ if (classification.kind === 'options') {
723
+ return {
724
+ modules: modulesOrOptions.slice(0, -1) as Record<string, unknown>[],
725
+ options: classification.options,
726
+ };
727
+ }
728
+ return {
729
+ modules: modulesOrOptions as readonly Record<string, unknown>[],
730
+ options: undefined,
731
+ };
732
+ };
733
+
734
+ /**
735
+ * Brand a plain `TopoOptions` payload so `topo()` treats the trailing
736
+ * argument as options unambiguously, regardless of which keys it
737
+ * contains. Useful when a module export shape would otherwise collide
738
+ * with the inline shorthand (e.g. a module exporting only `observe`).
739
+ *
740
+ * @example
741
+ * ```ts
742
+ * topo('app', userTrails, topo.options({ observe: traceSink }));
743
+ * ```
744
+ */
745
+ const describeNonPlainObject = (value: unknown): string => {
746
+ if (value === null) {
747
+ return 'null';
748
+ }
749
+ if (Array.isArray(value)) {
750
+ return 'array';
751
+ }
752
+ return typeof value;
753
+ };
754
+
755
+ const brandTopoOptions = (options: TopoOptions): TopoOptions => {
756
+ // Reject non-plain-object inputs up front. `{ ...options }` happily
757
+ // accepts `null`, `undefined`, primitives, and arrays, silently
758
+ // producing an empty branded payload that callers would then assume
759
+ // carried real options. Throwing here mirrors the strict handling
760
+ // applied to other malformed options elsewhere in the classifier.
761
+ if (
762
+ typeof options !== 'object' ||
763
+ options === null ||
764
+ Array.isArray(options)
765
+ ) {
766
+ throw new ValidationError(
767
+ `topo.options() expects a plain options object; received ${describeNonPlainObject(
768
+ options
769
+ )}`
770
+ );
771
+ }
772
+ // Return a fresh object rather than mutating the caller's payload.
773
+ // Mutating in place breaks frozen / non-extensible inputs (for example
774
+ // `topo.options(Object.freeze({ observe: sink }))`), which would throw
775
+ // a `TypeError` from `Object.defineProperty` even though the value is
776
+ // a valid `TopoOptions` shape.
777
+ const branded = { ...options };
778
+ Object.defineProperty(branded, TOPO_OPTIONS_BRAND, {
779
+ configurable: false,
780
+ enumerable: false,
781
+ value: true,
782
+ writable: false,
783
+ });
784
+ return branded;
785
+ };
786
+
787
+ interface TopoFn {
788
+ (
789
+ nameOrIdentity: string | TopoIdentity,
790
+ ...modulesOrOptions: (Record<string, unknown> | TopoOptions)[]
791
+ ): Topo;
792
+ /**
793
+ * Brand a plain `TopoOptions` payload so `topo()` treats the trailing
794
+ * argument as options unambiguously, regardless of key shape. Use this
795
+ * when a module export shape might otherwise collide with the inline
796
+ * options shorthand.
797
+ */
798
+ readonly options: (options: TopoOptions) => TopoOptions;
799
+ }
800
+
801
+ const topoImpl = (
802
+ nameOrIdentity: string | TopoIdentity,
803
+ ...modulesOrOptions: (Record<string, unknown> | TopoOptions)[]
174
804
  ): Topo => {
805
+ const identity: TopoIdentity =
806
+ typeof nameOrIdentity === 'string'
807
+ ? { name: nameOrIdentity }
808
+ : nameOrIdentity;
809
+ const { modules, options } = splitTopoArguments(modulesOrOptions);
810
+ const observe = normalizeObserve(options?.observe);
811
+ const layers = Object.freeze([...(options?.layers ?? [])]);
812
+
813
+ const contours = new Map<string, AnyContour>();
175
814
  const trails = new Map<string, AnyTrail>();
176
815
  const signals = new Map<string, AnySignal>();
177
- const provisions = new Map<string, AnyProvision>();
816
+ const resources = new Map<string, AnyResource>();
178
817
 
179
818
  for (const mod of modules) {
180
- registerModuleValues(mod, trails, signals, provisions);
819
+ registerModuleValues(mod, contours, trails, signals, resources);
181
820
  }
182
821
 
183
- return createTopo(name, trails, signals, provisions);
822
+ return createTopo(
823
+ identity,
824
+ contours,
825
+ finalizeTrailSignals(trails, resources),
826
+ signals,
827
+ resources,
828
+ observe,
829
+ layers
830
+ );
184
831
  };
832
+
833
+ export const topo: TopoFn = Object.assign(topoImpl, {
834
+ options: brandTopoOptions,
835
+ });