@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
@@ -0,0 +1,510 @@
1
+ import type { SQLQueryBindings } from 'bun:sqlite';
2
+ import { existsSync, statSync } from 'node:fs';
3
+
4
+ import { NotFoundError } from './errors.js';
5
+ import type { Topo } from './topo.js';
6
+ import { resource } from './resource.js';
7
+ import { Result } from './result.js';
8
+ import {
9
+ TOPO_SCHEMA_VERSION,
10
+ ensureTopoSnapshotSchema,
11
+ listTopoSnapshots as listStoredTopoSnapshots,
12
+ pinTopoSnapshot as pinStoredTopoSnapshot,
13
+ unpinTopoSnapshot as unpinStoredTopoSnapshot,
14
+ } from './internal/topo-snapshots.js';
15
+ import type {
16
+ CreateTopoSnapshotInput,
17
+ ListTopoSnapshotsOptions,
18
+ TopoSnapshot,
19
+ } from './internal/topo-snapshots.js';
20
+ import type {
21
+ TopoStoreExportRecord,
22
+ TopoStoreResourceRecord,
23
+ TopoStoreRef,
24
+ TopoStoreTrailDetailRecord,
25
+ TopoStoreTrailRecord,
26
+ } from './internal/topo-store-read.js';
27
+ import {
28
+ getTopoStoreExport,
29
+ getTopoStoreResource,
30
+ getTopoStoreTrail,
31
+ listTopoStoreResources,
32
+ listTopoStoreSnapshots,
33
+ listTopoStoreTrails,
34
+ queryTopoStore,
35
+ readTopoStoreSnapshot,
36
+ } from './internal/topo-store-read.js';
37
+ import { createTopoSnapshot as storeTopoSnapshot } from './internal/topo-store.js';
38
+ import {
39
+ openReadTrailsDb,
40
+ openWriteTrailsDb,
41
+ deriveTrailsDbPath,
42
+ } from './internal/trails-db.js';
43
+ import type { TrailsDbLocationOptions } from './internal/trails-db.js';
44
+
45
+ interface MigratedDbIdentity {
46
+ readonly mtimeMs: number;
47
+ readonly size: number;
48
+ }
49
+
50
+ const migratedTopoDbPaths = new Map<string, MigratedDbIdentity>();
51
+
52
+ /**
53
+ * Test-only instrumentation counters. Incremented by the read-path migration
54
+ * check to let tests assert that a current-schema store does not escalate to
55
+ * a write-mode open, and that cache invalidation re-runs the check when the
56
+ * underlying file is replaced.
57
+ *
58
+ * @internal
59
+ */
60
+ export const __topoStoreMigrationStats = {
61
+ peekCalls: 0,
62
+ writeEscalations: 0,
63
+ };
64
+
65
+ const peekTopoSchemaVersion = (
66
+ options: TrailsDbLocationOptions | undefined
67
+ ): number | undefined => {
68
+ let db: ReturnType<typeof openReadTrailsDb> | undefined;
69
+ try {
70
+ db = openReadTrailsDb(options);
71
+ const row = db
72
+ .query<{ version: number }, [string]>(
73
+ 'SELECT version FROM meta_schema_versions WHERE subsystem = ?'
74
+ )
75
+ .get('topo');
76
+ return row?.version ?? 0;
77
+ } catch {
78
+ // Table missing or unexpected shape: caller will escalate to a write-mode
79
+ // open and run the migration, which rebuilds the schema.
80
+ return undefined;
81
+ } finally {
82
+ db?.close();
83
+ }
84
+ };
85
+
86
+ const statIdentity = (dbPath: string): MigratedDbIdentity | undefined => {
87
+ try {
88
+ const info = statSync(dbPath);
89
+ return { mtimeMs: info.mtimeMs, size: info.size };
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ };
94
+
95
+ const identitiesEqual = (
96
+ a: MigratedDbIdentity,
97
+ b: MigratedDbIdentity
98
+ ): boolean => a.mtimeMs === b.mtimeMs && a.size === b.size;
99
+
100
+ const runTopoMigrationEscalation = (
101
+ options: TrailsDbLocationOptions | undefined,
102
+ dbPath: string,
103
+ fallbackIdentity: MigratedDbIdentity
104
+ ): void => {
105
+ __topoStoreMigrationStats.writeEscalations += 1;
106
+ const db = openWriteTrailsDb(options);
107
+ try {
108
+ ensureTopoSnapshotSchema(db);
109
+ } finally {
110
+ db.close();
111
+ }
112
+ // Re-stat after the migration so the cached identity matches the file we
113
+ // just touched, avoiding a spurious second escalation on the next read.
114
+ const postIdentity = statIdentity(dbPath) ?? fallbackIdentity;
115
+ migratedTopoDbPaths.set(dbPath, postIdentity);
116
+ };
117
+
118
+ const resolveIdentityIfFresh = (
119
+ dbPath: string
120
+ ): MigratedDbIdentity | undefined => {
121
+ if (!existsSync(dbPath)) {
122
+ return undefined;
123
+ }
124
+ const identity = statIdentity(dbPath);
125
+ if (identity === undefined) {
126
+ return undefined;
127
+ }
128
+ const cached = migratedTopoDbPaths.get(dbPath);
129
+ if (cached !== undefined && identitiesEqual(cached, identity)) {
130
+ return undefined;
131
+ }
132
+ return identity;
133
+ };
134
+
135
+ /**
136
+ * Ensure the topo snapshot schema is at the current version before any
137
+ * read-only access. Peeks the version through a read-only handle first and
138
+ * only escalates to a write-mode open + migration when the store is stale.
139
+ *
140
+ * Memoized per resolved DB path keyed on file identity (mtime + size) so a
141
+ * long-running process that deletes and recreates `trails.db` re-runs the
142
+ * migration check against the fresh file.
143
+ *
144
+ * If the DB file does not yet exist, this is a no-op — the downstream read
145
+ * path will surface its own NotFoundError.
146
+ */
147
+ const ensureTopoMigratedIfExists = (
148
+ options?: TrailsDbLocationOptions
149
+ ): void => {
150
+ const dbPath = deriveTrailsDbPath(options);
151
+ const identity = resolveIdentityIfFresh(dbPath);
152
+ if (identity === undefined) {
153
+ return;
154
+ }
155
+
156
+ __topoStoreMigrationStats.peekCalls += 1;
157
+ const version = peekTopoSchemaVersion(options);
158
+ if (version !== undefined && version >= TOPO_SCHEMA_VERSION) {
159
+ // Already current — record identity and skip the write-mode open entirely,
160
+ // preserving the read-only contract for callers whose filesystem mounts
161
+ // `trails.db` read-only.
162
+ migratedTopoDbPaths.set(dbPath, identity);
163
+ return;
164
+ }
165
+
166
+ runTopoMigrationEscalation(options, dbPath, identity);
167
+ };
168
+
169
+ export type {
170
+ TopoStoreExportRecord,
171
+ TopoStoreResourceRecord,
172
+ TopoStoreRef,
173
+ TopoStoreTrailDetailRecord,
174
+ TopoStoreTrailRecord,
175
+ } from './internal/topo-store-read.js';
176
+ export type {
177
+ CreateTopoSnapshotInput,
178
+ ListTopoSnapshotsOptions,
179
+ TopoSnapshot,
180
+ } from './internal/topo-snapshots.js';
181
+
182
+ export interface ReadOnlyTopoStore {
183
+ readonly exports: {
184
+ get(ref?: TopoStoreRef): TopoStoreExportRecord | undefined;
185
+ };
186
+ query<TRow extends Record<string, unknown>>(
187
+ sql: string,
188
+ bindings?: readonly SQLQueryBindings[]
189
+ ): readonly TRow[];
190
+ readonly resources: {
191
+ get(
192
+ id: string,
193
+ options?: { readonly snapshot?: TopoStoreRef }
194
+ ): TopoStoreResourceRecord | undefined;
195
+ list(options?: {
196
+ readonly snapshot?: TopoStoreRef;
197
+ }): readonly TopoStoreResourceRecord[];
198
+ };
199
+ readonly snapshots: {
200
+ get(ref?: TopoStoreRef): TopoSnapshot | undefined;
201
+ latest(): TopoSnapshot | undefined;
202
+ list(options?: ListTopoSnapshotsOptions): readonly TopoSnapshot[];
203
+ };
204
+ readonly trails: {
205
+ get(
206
+ id: string,
207
+ options?: { readonly snapshot?: TopoStoreRef }
208
+ ): TopoStoreTrailDetailRecord | undefined;
209
+ list(options?: {
210
+ readonly intent?: TopoStoreTrailRecord['intent'];
211
+ readonly snapshot?: TopoStoreRef;
212
+ }): readonly TopoStoreTrailRecord[];
213
+ };
214
+ }
215
+
216
+ export interface MockTopoStoreSeed {
217
+ readonly exports?: readonly TopoStoreExportRecord[];
218
+ readonly resources?: readonly TopoStoreResourceRecord[];
219
+ readonly snapshots?: readonly TopoSnapshot[];
220
+ readonly trails?: readonly TopoStoreTrailDetailRecord[];
221
+ }
222
+
223
+ const missingStoreMessage =
224
+ 'No saved topo state found. Populate trails.db first or run a topo-backed surface.';
225
+
226
+ const resolveStoreRootDir = (options?: TrailsDbLocationOptions): string =>
227
+ options?.rootDir ?? process.cwd();
228
+
229
+ const requireReadDb = (
230
+ options?: TrailsDbLocationOptions
231
+ ): ReturnType<typeof openReadTrailsDb> => {
232
+ const dbPath = deriveTrailsDbPath(options);
233
+ if (!existsSync(dbPath)) {
234
+ throw new NotFoundError(missingStoreMessage);
235
+ }
236
+ ensureTopoMigratedIfExists(options);
237
+ return openReadTrailsDb(options);
238
+ };
239
+
240
+ const requireSavedTopoState = (
241
+ db: ReturnType<typeof openReadTrailsDb>
242
+ ): void => {
243
+ if (readTopoStoreSnapshot(db) === undefined) {
244
+ throw new NotFoundError(missingStoreMessage);
245
+ }
246
+ };
247
+
248
+ const withStoredTopoState = <T>(
249
+ options: TrailsDbLocationOptions | undefined,
250
+ run: (db: ReturnType<typeof openReadTrailsDb>) => T
251
+ ): T => {
252
+ const db = requireReadDb(options);
253
+ try {
254
+ requireSavedTopoState(db);
255
+ return run(db);
256
+ } finally {
257
+ db.close();
258
+ }
259
+ };
260
+
261
+ const createSeedResolver = (seed?: MockTopoStoreSeed) => {
262
+ const snapshots = [...(seed?.snapshots ?? [])];
263
+ const trails = [...(seed?.trails ?? [])];
264
+ const resources = [...(seed?.resources ?? [])];
265
+ const exports = [...(seed?.exports ?? [])];
266
+
267
+ const resolveSnapshot = (ref?: TopoStoreRef): TopoSnapshot | undefined => {
268
+ if (ref?.snapshotId !== undefined) {
269
+ return snapshots.find((snapshot) => snapshot.id === ref.snapshotId);
270
+ }
271
+ if (ref?.pin !== undefined) {
272
+ return snapshots.find((snapshot) => snapshot.pinnedAs === ref.pin);
273
+ }
274
+ return snapshots[0];
275
+ };
276
+
277
+ return {
278
+ exports,
279
+ resolveSnapshot,
280
+ resources,
281
+ snapshots,
282
+ trails,
283
+ };
284
+ };
285
+
286
+ export const createMockTopoStore = (
287
+ seed?: MockTopoStoreSeed
288
+ ): ReadOnlyTopoStore => {
289
+ const resolved = createSeedResolver(seed);
290
+
291
+ return {
292
+ exports: {
293
+ get(ref?: TopoStoreRef) {
294
+ const snapshot = resolved.resolveSnapshot(ref);
295
+ return snapshot === undefined
296
+ ? undefined
297
+ : resolved.exports.find((entry) => entry.snapshot.id === snapshot.id);
298
+ },
299
+ },
300
+ query() {
301
+ throw new NotFoundError(
302
+ 'Mock topoStore.query() is unsupported. Seed typed accessors instead.'
303
+ );
304
+ },
305
+ resources: {
306
+ get(id, options) {
307
+ const snapshot = resolved.resolveSnapshot(options?.snapshot);
308
+ return resolved.resources.find(
309
+ (item) =>
310
+ item.id === id &&
311
+ (snapshot === undefined || item.snapshotId === snapshot.id)
312
+ );
313
+ },
314
+ list(options) {
315
+ const snapshot = resolved.resolveSnapshot(options?.snapshot);
316
+ return snapshot === undefined
317
+ ? []
318
+ : resolved.resources.filter(
319
+ (item) => item.snapshotId === snapshot.id
320
+ );
321
+ },
322
+ },
323
+ snapshots: {
324
+ get(ref?: TopoStoreRef) {
325
+ return resolved.resolveSnapshot(ref);
326
+ },
327
+ latest() {
328
+ return resolved.snapshots[0];
329
+ },
330
+ list(options) {
331
+ let snapshots =
332
+ options?.pinned === undefined
333
+ ? resolved.snapshots
334
+ : resolved.snapshots.filter((snapshot) =>
335
+ options.pinned
336
+ ? snapshot.pinnedAs !== undefined
337
+ : snapshot.pinnedAs === undefined
338
+ );
339
+ if (options?.before !== undefined) {
340
+ const target = snapshots.find((s) => s.id === options.before);
341
+ if (target !== undefined) {
342
+ snapshots = snapshots.filter(
343
+ (s) =>
344
+ s.createdAt < target.createdAt ||
345
+ (s.createdAt === target.createdAt && s.id < target.id)
346
+ );
347
+ }
348
+ }
349
+ if (options?.limit !== undefined) {
350
+ snapshots = snapshots.slice(0, options.limit);
351
+ }
352
+ return snapshots;
353
+ },
354
+ },
355
+ trails: {
356
+ get(id, options) {
357
+ const snapshot = resolved.resolveSnapshot(options?.snapshot);
358
+ if (snapshot === undefined) {
359
+ return;
360
+ }
361
+ return resolved.trails.find(
362
+ (trail) => trail.id === id && trail.snapshotId === snapshot.id
363
+ );
364
+ },
365
+ list(options) {
366
+ const snapshot = resolved.resolveSnapshot(options?.snapshot);
367
+ if (snapshot === undefined) {
368
+ return [];
369
+ }
370
+ return resolved.trails.filter(
371
+ (trail) =>
372
+ trail.snapshotId === snapshot.id &&
373
+ (options?.intent === undefined || trail.intent === options.intent)
374
+ );
375
+ },
376
+ },
377
+ };
378
+ };
379
+
380
+ export const createTopoStore = (
381
+ options?: TrailsDbLocationOptions
382
+ ): ReadOnlyTopoStore => ({
383
+ exports: {
384
+ get(ref?: TopoStoreRef) {
385
+ return withStoredTopoState(options, (db) => getTopoStoreExport(db, ref));
386
+ },
387
+ },
388
+ query<TRow extends Record<string, unknown>>(
389
+ sql: string,
390
+ bindings?: readonly SQLQueryBindings[]
391
+ ) {
392
+ return withStoredTopoState(options, (db) =>
393
+ queryTopoStore<TRow>(db, sql, bindings)
394
+ );
395
+ },
396
+ resources: {
397
+ get(id, queryOptions) {
398
+ return withStoredTopoState(options, (db) =>
399
+ getTopoStoreResource(db, id, queryOptions)
400
+ );
401
+ },
402
+ list(queryOptions) {
403
+ return withStoredTopoState(options, (db) =>
404
+ listTopoStoreResources(db, queryOptions)
405
+ );
406
+ },
407
+ },
408
+ snapshots: {
409
+ get(ref?: TopoStoreRef) {
410
+ return withStoredTopoState(options, (db) =>
411
+ readTopoStoreSnapshot(db, ref)
412
+ );
413
+ },
414
+ latest() {
415
+ return withStoredTopoState(options, (db) => readTopoStoreSnapshot(db));
416
+ },
417
+ list(snapshotOptions) {
418
+ return withStoredTopoState(options, (db) =>
419
+ listTopoStoreSnapshots(db, snapshotOptions)
420
+ );
421
+ },
422
+ },
423
+ trails: {
424
+ get(id, queryOptions) {
425
+ return withStoredTopoState(options, (db) =>
426
+ getTopoStoreTrail(db, id, queryOptions)
427
+ );
428
+ },
429
+ list(queryOptions) {
430
+ return withStoredTopoState(options, (db) =>
431
+ listTopoStoreTrails(db, queryOptions)
432
+ );
433
+ },
434
+ },
435
+ });
436
+
437
+ export const createTopoSnapshot = (
438
+ topo: Topo,
439
+ options?: TrailsDbLocationOptions & CreateTopoSnapshotInput
440
+ ): Result<TopoSnapshot, Error> => {
441
+ const db = openWriteTrailsDb(options);
442
+ try {
443
+ return storeTopoSnapshot(db, topo, options);
444
+ } finally {
445
+ db.close();
446
+ }
447
+ };
448
+
449
+ export const listTopoSnapshots = (
450
+ options?: TrailsDbLocationOptions & ListTopoSnapshotsOptions
451
+ ): readonly TopoSnapshot[] => {
452
+ const dbPath = deriveTrailsDbPath(options);
453
+ if (!existsSync(dbPath)) {
454
+ return [];
455
+ }
456
+
457
+ ensureTopoMigratedIfExists(options);
458
+ const db = openReadTrailsDb(options);
459
+ try {
460
+ return listStoredTopoSnapshots(db, options);
461
+ } finally {
462
+ db.close();
463
+ }
464
+ };
465
+
466
+ export const pinTopoSnapshot = (
467
+ id: string,
468
+ name: string,
469
+ options?: TrailsDbLocationOptions
470
+ ): TopoSnapshot | undefined => {
471
+ const dbPath = deriveTrailsDbPath(options);
472
+ if (!existsSync(dbPath)) {
473
+ return undefined;
474
+ }
475
+
476
+ const db = openWriteTrailsDb(options);
477
+ try {
478
+ return pinStoredTopoSnapshot(db, { id, name });
479
+ } finally {
480
+ db.close();
481
+ }
482
+ };
483
+
484
+ export const unpinTopoSnapshot = (
485
+ nameOrId: string,
486
+ options?: TrailsDbLocationOptions
487
+ ): TopoSnapshot | undefined => {
488
+ const dbPath = deriveTrailsDbPath(options);
489
+ if (!existsSync(dbPath)) {
490
+ return undefined;
491
+ }
492
+
493
+ const db = openWriteTrailsDb(options);
494
+ try {
495
+ return unpinStoredTopoSnapshot(db, nameOrId);
496
+ } finally {
497
+ db.close();
498
+ }
499
+ };
500
+
501
+ export const topoStore = resource('topo.store', {
502
+ create: (svc) =>
503
+ Result.ok(
504
+ createTopoStore({
505
+ rootDir: svc.workspaceRoot ?? svc.cwd ?? resolveStoreRootDir(),
506
+ })
507
+ ),
508
+ description: 'Read-only query access to saved topo state in trails.db',
509
+ mock: () => createMockTopoStore(),
510
+ });