@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,87 @@
1
+ import type { AnySignal, Signal } from '../signal.js';
2
+
3
+ export interface LateBoundSignalRef {
4
+ readonly kind: 'store-derived';
5
+ readonly token: string;
6
+ }
7
+
8
+ export interface LateBoundSignalMarker {
9
+ readonly displayId: string;
10
+ readonly token: string;
11
+ }
12
+
13
+ const LATE_BOUND_SIGNAL_REF = Symbol('trails.late-bound-signal-ref');
14
+ const LATE_BOUND_SIGNAL_MARKER_PREFIX = '@@trails:late-bound-signal-ref:';
15
+
16
+ const defineLateBoundSignalRef = <T extends object>(
17
+ value: T,
18
+ ref: LateBoundSignalRef
19
+ ): T => {
20
+ Object.defineProperty(value, LATE_BOUND_SIGNAL_REF, {
21
+ configurable: false,
22
+ enumerable: false,
23
+ value: ref,
24
+ writable: false,
25
+ });
26
+ return value;
27
+ };
28
+
29
+ export const getLateBoundSignalRef = (
30
+ signal: Pick<AnySignal, 'id'> | undefined
31
+ ): LateBoundSignalRef | undefined =>
32
+ signal === undefined
33
+ ? undefined
34
+ : ((signal as Record<PropertyKey, unknown>)[LATE_BOUND_SIGNAL_REF] as
35
+ | LateBoundSignalRef
36
+ | undefined);
37
+
38
+ export const attachLateBoundSignalRef = <T>(
39
+ signal: Signal<T>,
40
+ ref: LateBoundSignalRef
41
+ ): Signal<T> =>
42
+ Object.freeze(
43
+ defineLateBoundSignalRef(
44
+ {
45
+ ...signal,
46
+ },
47
+ ref
48
+ )
49
+ ) as Signal<T>;
50
+
51
+ export const cloneSignalWithId = <T>(
52
+ signal: Signal<T>,
53
+ id: string
54
+ ): Signal<T> => {
55
+ const clone = {
56
+ ...signal,
57
+ id,
58
+ };
59
+ const ref = getLateBoundSignalRef(signal);
60
+ return Object.freeze(
61
+ ref ? defineLateBoundSignalRef(clone, ref) : clone
62
+ ) as Signal<T>;
63
+ };
64
+
65
+ export const createLateBoundSignalMarker = (
66
+ ref: LateBoundSignalRef,
67
+ displayId: string
68
+ ): string => `${LATE_BOUND_SIGNAL_MARKER_PREFIX}${ref.token}:${displayId}`;
69
+
70
+ export const parseLateBoundSignalMarker = (
71
+ value: string
72
+ ): LateBoundSignalMarker | null => {
73
+ if (!value.startsWith(LATE_BOUND_SIGNAL_MARKER_PREFIX)) {
74
+ return null;
75
+ }
76
+
77
+ const remainder = value.slice(LATE_BOUND_SIGNAL_MARKER_PREFIX.length);
78
+ const separator = remainder.indexOf(':');
79
+ if (separator <= 0 || separator === remainder.length - 1) {
80
+ return null;
81
+ }
82
+
83
+ return {
84
+ displayId: remainder.slice(separator + 1),
85
+ token: remainder.slice(0, separator),
86
+ };
87
+ };
@@ -0,0 +1,517 @@
1
+ import type { Database, SQLQueryBindings } from 'bun:sqlite';
2
+
3
+ import { ensureSubsystemSchema } from './trails-db.js';
4
+
5
+ const TOPO_SUBSYSTEM = 'topo';
6
+ const TOPO_TABLE_STATEMENTS = [
7
+ `CREATE TABLE IF NOT EXISTS topo_snapshots (
8
+ id TEXT PRIMARY KEY,
9
+ git_sha TEXT,
10
+ git_dirty INTEGER NOT NULL DEFAULT 0,
11
+ trail_count INTEGER NOT NULL DEFAULT 0,
12
+ signal_count INTEGER NOT NULL DEFAULT 0,
13
+ resource_count INTEGER NOT NULL DEFAULT 0,
14
+ pinned_as TEXT,
15
+ created_at TEXT NOT NULL
16
+ )`,
17
+ `CREATE TABLE IF NOT EXISTS topo_trails (
18
+ id TEXT NOT NULL,
19
+ intent TEXT,
20
+ idempotent INTEGER NOT NULL DEFAULT 0,
21
+ has_output INTEGER NOT NULL DEFAULT 0,
22
+ has_examples INTEGER NOT NULL DEFAULT 0,
23
+ example_count INTEGER NOT NULL DEFAULT 0,
24
+ description TEXT,
25
+ pattern TEXT,
26
+ meta TEXT,
27
+ snapshot_id TEXT NOT NULL,
28
+ PRIMARY KEY (id, snapshot_id),
29
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
30
+ )`,
31
+ `CREATE TABLE IF NOT EXISTS topo_crossings (
32
+ source_id TEXT NOT NULL,
33
+ target_id TEXT NOT NULL,
34
+ snapshot_id TEXT NOT NULL,
35
+ PRIMARY KEY (source_id, target_id, snapshot_id),
36
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
37
+ )`,
38
+ `CREATE TABLE IF NOT EXISTS topo_trail_resources (
39
+ trail_id TEXT NOT NULL,
40
+ resource_id TEXT NOT NULL,
41
+ snapshot_id TEXT NOT NULL,
42
+ PRIMARY KEY (trail_id, resource_id, snapshot_id),
43
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
44
+ )`,
45
+ `CREATE TABLE IF NOT EXISTS topo_resources (
46
+ id TEXT NOT NULL,
47
+ has_mock INTEGER NOT NULL DEFAULT 0,
48
+ has_health INTEGER NOT NULL DEFAULT 0,
49
+ snapshot_id TEXT NOT NULL,
50
+ PRIMARY KEY (id, snapshot_id),
51
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
52
+ )`,
53
+ `CREATE TABLE IF NOT EXISTS topo_signals (
54
+ id TEXT NOT NULL,
55
+ description TEXT,
56
+ snapshot_id TEXT NOT NULL,
57
+ PRIMARY KEY (id, snapshot_id),
58
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
59
+ )`,
60
+ `CREATE TABLE IF NOT EXISTS topo_trail_signals (
61
+ trail_id TEXT NOT NULL,
62
+ signal_id TEXT NOT NULL,
63
+ snapshot_id TEXT NOT NULL,
64
+ PRIMARY KEY (trail_id, signal_id, snapshot_id),
65
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
66
+ )`,
67
+ `CREATE TABLE IF NOT EXISTS topo_surfaces (
68
+ trail_id TEXT NOT NULL,
69
+ surface TEXT NOT NULL,
70
+ derived_name TEXT NOT NULL,
71
+ method TEXT,
72
+ snapshot_id TEXT NOT NULL,
73
+ PRIMARY KEY (trail_id, surface, snapshot_id),
74
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
75
+ )`,
76
+ `CREATE TABLE IF NOT EXISTS topo_examples (
77
+ id TEXT PRIMARY KEY,
78
+ trail_id TEXT NOT NULL,
79
+ ordinal INTEGER NOT NULL,
80
+ name TEXT NOT NULL,
81
+ description TEXT,
82
+ input TEXT NOT NULL,
83
+ expected TEXT,
84
+ error TEXT,
85
+ snapshot_id TEXT NOT NULL,
86
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
87
+ )`,
88
+ `CREATE TABLE IF NOT EXISTS topo_schemas (
89
+ owner_id TEXT NOT NULL,
90
+ owner_kind TEXT NOT NULL,
91
+ schema_kind TEXT NOT NULL,
92
+ zod_hash TEXT NOT NULL,
93
+ json_schema TEXT NOT NULL,
94
+ snapshot_id TEXT NOT NULL,
95
+ PRIMARY KEY (owner_id, owner_kind, schema_kind, snapshot_id),
96
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
97
+ )`,
98
+ `CREATE TABLE IF NOT EXISTS topo_exports (
99
+ snapshot_id TEXT PRIMARY KEY,
100
+ surface_map TEXT NOT NULL,
101
+ surface_hash TEXT NOT NULL,
102
+ serialized_lock TEXT NOT NULL,
103
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
104
+ )`,
105
+ `CREATE TABLE IF NOT EXISTS topo_trail_fires (
106
+ trail_id TEXT NOT NULL,
107
+ signal_id TEXT NOT NULL,
108
+ snapshot_id TEXT NOT NULL,
109
+ PRIMARY KEY (trail_id, signal_id, snapshot_id),
110
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
111
+ )`,
112
+ `CREATE TABLE IF NOT EXISTS topo_trail_on (
113
+ trail_id TEXT NOT NULL,
114
+ signal_id TEXT NOT NULL,
115
+ snapshot_id TEXT NOT NULL,
116
+ PRIMARY KEY (trail_id, signal_id, snapshot_id),
117
+ FOREIGN KEY (snapshot_id) REFERENCES topo_snapshots(id) ON DELETE CASCADE
118
+ )`,
119
+ ] as const;
120
+ const TOPO_INDEX_STATEMENTS = [
121
+ 'CREATE INDEX IF NOT EXISTS idx_topo_snapshots_created_at ON topo_snapshots(created_at DESC)',
122
+ `CREATE UNIQUE INDEX IF NOT EXISTS idx_topo_snapshots_pinned_as
123
+ ON topo_snapshots(pinned_as) WHERE pinned_as IS NOT NULL`,
124
+ 'CREATE INDEX IF NOT EXISTS idx_topo_trails_snapshot_id ON topo_trails(snapshot_id)',
125
+ 'CREATE INDEX IF NOT EXISTS idx_topo_crossings_snapshot_id ON topo_crossings(snapshot_id)',
126
+ 'CREATE INDEX IF NOT EXISTS idx_topo_trail_resources_snapshot_id ON topo_trail_resources(snapshot_id)',
127
+ 'CREATE INDEX IF NOT EXISTS idx_topo_resources_snapshot_id ON topo_resources(snapshot_id)',
128
+ 'CREATE INDEX IF NOT EXISTS idx_topo_signals_snapshot_id ON topo_signals(snapshot_id)',
129
+ 'CREATE INDEX IF NOT EXISTS idx_topo_trail_signals_snapshot_id ON topo_trail_signals(snapshot_id)',
130
+ 'CREATE INDEX IF NOT EXISTS idx_topo_surfaces_snapshot_id ON topo_surfaces(snapshot_id)',
131
+ 'CREATE UNIQUE INDEX IF NOT EXISTS idx_topo_examples_snapshot_trail_ordinal ON topo_examples(snapshot_id, trail_id, ordinal)',
132
+ 'CREATE INDEX IF NOT EXISTS idx_topo_schemas_snapshot_id ON topo_schemas(snapshot_id)',
133
+ `CREATE INDEX IF NOT EXISTS idx_topo_schemas_lookup
134
+ ON topo_schemas(owner_id, owner_kind, schema_kind, zod_hash)`,
135
+ 'CREATE INDEX IF NOT EXISTS idx_topo_trail_fires_snapshot_id ON topo_trail_fires(snapshot_id)',
136
+ 'CREATE INDEX IF NOT EXISTS idx_topo_trail_on_snapshot_id ON topo_trail_on(snapshot_id)',
137
+ ] as const;
138
+ interface TopoSnapshotRow {
139
+ readonly created_at: string;
140
+ readonly git_dirty: number;
141
+ readonly git_sha: string | null;
142
+ readonly id: string;
143
+ readonly pinned_as: string | null;
144
+ readonly resource_count: number;
145
+ readonly signal_count: number;
146
+ readonly trail_count: number;
147
+ }
148
+
149
+ export interface TopoSnapshot {
150
+ readonly createdAt: string;
151
+ readonly gitDirty: boolean;
152
+ readonly gitSha?: string;
153
+ readonly id: string;
154
+ readonly pinnedAs?: string;
155
+ readonly resourceCount: number;
156
+ readonly signalCount: number;
157
+ readonly trailCount: number;
158
+ }
159
+
160
+ export interface CreateTopoSnapshotInput {
161
+ readonly createdAt?: string;
162
+ readonly gitDirty?: boolean;
163
+ readonly gitSha?: string;
164
+ readonly id?: string;
165
+ readonly resourceCount?: number;
166
+ readonly signalCount?: number;
167
+ readonly trailCount?: number;
168
+ }
169
+
170
+ export interface ListTopoSnapshotsOptions {
171
+ readonly before?: string;
172
+ readonly limit?: number;
173
+ readonly pinned?: boolean;
174
+ }
175
+
176
+ const rowToSnapshot = (row: TopoSnapshotRow): TopoSnapshot => ({
177
+ createdAt: row.created_at,
178
+ gitDirty: row.git_dirty === 1,
179
+ id: row.id,
180
+ resourceCount: row.resource_count,
181
+ signalCount: row.signal_count,
182
+ trailCount: row.trail_count,
183
+ ...(row.git_sha === null ? {} : { gitSha: row.git_sha }),
184
+ ...(row.pinned_as === null ? {} : { pinnedAs: row.pinned_as }),
185
+ });
186
+
187
+ const tableExists = (db: Database, tableName: string): boolean => {
188
+ const row = db
189
+ .query<{ name: string }, [string]>(
190
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?"
191
+ )
192
+ .get(tableName);
193
+ return row?.name === tableName;
194
+ };
195
+
196
+ const runStatements = (db: Database, statements: readonly string[]): void => {
197
+ for (const statement of statements) {
198
+ db.run(statement);
199
+ }
200
+ };
201
+
202
+ const createAllTopoTables = (db: Database): void => {
203
+ runStatements(db, TOPO_TABLE_STATEMENTS);
204
+ runStatements(db, TOPO_INDEX_STATEMENTS);
205
+ };
206
+
207
+ /**
208
+ * Current topo subsystem schema version.
209
+ *
210
+ * Version 8 adds `pattern TEXT` column to `topo_trails`.
211
+ *
212
+ * Version 7 defined the snapshot-first topo tables (`topo_snapshots`,
213
+ * `topo_surfaces`, and `snapshot_id` foreign keys) as the only supported
214
+ * schema. Older pre-release tables are ignored in place; we create the current
215
+ * tables and advance the subsystem version without translating or deleting
216
+ * legacy rows.
217
+ */
218
+ export const TOPO_SCHEMA_VERSION = 8;
219
+
220
+ export const ensureTopoSnapshotSchema = (db: Database): void => {
221
+ ensureSubsystemSchema(db, {
222
+ migrate: (currentVersion) => {
223
+ createAllTopoTables(db);
224
+ if (currentVersion === 7) {
225
+ db.run('ALTER TABLE topo_trails ADD COLUMN pattern TEXT');
226
+ }
227
+ },
228
+ subsystem: TOPO_SUBSYSTEM,
229
+ version: TOPO_SCHEMA_VERSION,
230
+ });
231
+ };
232
+
233
+ export const insertTopoSnapshotRecord = (
234
+ db: Database,
235
+ input?: CreateTopoSnapshotInput
236
+ ): TopoSnapshot => {
237
+ const record: TopoSnapshot = {
238
+ createdAt: input?.createdAt ?? new Date().toISOString(),
239
+ gitDirty: input?.gitDirty ?? false,
240
+ id: input?.id ?? Bun.randomUUIDv7(),
241
+ resourceCount: input?.resourceCount ?? 0,
242
+ signalCount: input?.signalCount ?? 0,
243
+ trailCount: input?.trailCount ?? 0,
244
+ ...(input?.gitSha === undefined ? {} : { gitSha: input.gitSha }),
245
+ };
246
+
247
+ db.run(
248
+ `INSERT INTO topo_snapshots (
249
+ id, git_sha, git_dirty, trail_count, signal_count, resource_count, pinned_as, created_at
250
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
251
+ [
252
+ record.id,
253
+ record.gitSha ?? null,
254
+ record.gitDirty ? 1 : 0,
255
+ record.trailCount,
256
+ record.signalCount,
257
+ record.resourceCount,
258
+ null,
259
+ record.createdAt,
260
+ ]
261
+ );
262
+
263
+ return record;
264
+ };
265
+
266
+ const normalizeLimit = (limit?: number): number | undefined => {
267
+ if (limit === undefined) {
268
+ return undefined;
269
+ }
270
+ return Math.max(0, Math.trunc(limit));
271
+ };
272
+
273
+ export const readTopoSnapshot = (
274
+ db: Database,
275
+ id: string
276
+ ): TopoSnapshot | undefined => {
277
+ if (!tableExists(db, 'topo_snapshots')) {
278
+ return undefined;
279
+ }
280
+ const row = db
281
+ .query<TopoSnapshotRow, [string]>(
282
+ `SELECT id, git_sha, git_dirty, trail_count, signal_count, resource_count, pinned_as, created_at
283
+ FROM topo_snapshots
284
+ WHERE id = ?`
285
+ )
286
+ .get(id);
287
+ return row === null || row === undefined ? undefined : rowToSnapshot(row);
288
+ };
289
+
290
+ export const readPinnedTopoSnapshot = (
291
+ db: Database,
292
+ name: string
293
+ ): TopoSnapshot | undefined => {
294
+ if (!tableExists(db, 'topo_snapshots')) {
295
+ return undefined;
296
+ }
297
+ const row = db
298
+ .query<TopoSnapshotRow, [string]>(
299
+ `SELECT id, git_sha, git_dirty, trail_count, signal_count, resource_count, pinned_as, created_at
300
+ FROM topo_snapshots
301
+ WHERE pinned_as = ?
302
+ LIMIT 1`
303
+ )
304
+ .get(name);
305
+ return row === null || row === undefined ? undefined : rowToSnapshot(row);
306
+ };
307
+
308
+ const applyBeforeSnapshotClause = (
309
+ db: Database,
310
+ beforeId: string | undefined,
311
+ bindings: SQLQueryBindings[],
312
+ conditions: string[]
313
+ ): boolean => {
314
+ if (beforeId === undefined) {
315
+ return true;
316
+ }
317
+
318
+ const before = readTopoSnapshot(db, beforeId);
319
+ if (before === undefined) {
320
+ return false;
321
+ }
322
+
323
+ conditions.push('(created_at < ? OR (created_at = ? AND id < ?))');
324
+ bindings.push(before.createdAt, before.createdAt, before.id);
325
+ return true;
326
+ };
327
+
328
+ const applyPinnedSnapshotClause = (
329
+ pinned: boolean | undefined,
330
+ conditions: string[]
331
+ ): void => {
332
+ if (pinned === true) {
333
+ conditions.push('pinned_as IS NOT NULL');
334
+ return;
335
+ }
336
+
337
+ if (pinned === false) {
338
+ conditions.push('pinned_as IS NULL');
339
+ }
340
+ };
341
+
342
+ const buildSnapshotWhereClause = (conditions: readonly string[]): string =>
343
+ conditions.length === 0 ? '' : ` WHERE ${conditions.join(' AND ')}`;
344
+
345
+ const buildSnapshotLimitClause = (
346
+ limit: number | undefined,
347
+ bindings: SQLQueryBindings[]
348
+ ): string => {
349
+ if (limit === undefined) {
350
+ return '';
351
+ }
352
+
353
+ bindings.push(limit);
354
+ return ' LIMIT ?';
355
+ };
356
+
357
+ const listSnapshotRows = (
358
+ db: Database,
359
+ options?: ListTopoSnapshotsOptions
360
+ ): readonly TopoSnapshotRow[] => {
361
+ if (!tableExists(db, 'topo_snapshots')) {
362
+ return [];
363
+ }
364
+
365
+ const bindings: SQLQueryBindings[] = [];
366
+ const conditions: string[] = [];
367
+ if (!applyBeforeSnapshotClause(db, options?.before, bindings, conditions)) {
368
+ return [];
369
+ }
370
+
371
+ applyPinnedSnapshotClause(options?.pinned, conditions);
372
+ const whereClause = buildSnapshotWhereClause(conditions);
373
+ const limitClause = buildSnapshotLimitClause(
374
+ normalizeLimit(options?.limit),
375
+ bindings
376
+ );
377
+
378
+ return db
379
+ .query<TopoSnapshotRow, SQLQueryBindings[]>(
380
+ `SELECT id, git_sha, git_dirty, trail_count, signal_count, resource_count, pinned_as, created_at
381
+ FROM topo_snapshots${whereClause}
382
+ ORDER BY created_at DESC, id DESC${limitClause}`
383
+ )
384
+ .all(...bindings);
385
+ };
386
+
387
+ export const listTopoSnapshots = (
388
+ db: Database,
389
+ options?: ListTopoSnapshotsOptions
390
+ ): readonly TopoSnapshot[] => listSnapshotRows(db, options).map(rowToSnapshot);
391
+
392
+ const countSnapshots = (db: Database, whereClause?: string): number => {
393
+ const query =
394
+ whereClause === undefined
395
+ ? 'SELECT COUNT(*) as count FROM topo_snapshots'
396
+ : `SELECT COUNT(*) as count FROM topo_snapshots WHERE ${whereClause}`;
397
+ const row = db.query<{ count: number }, []>(query).get();
398
+ return row?.count ?? 0;
399
+ };
400
+
401
+ export const countTopoSnapshots = (db: Database): number => {
402
+ if (!tableExists(db, 'topo_snapshots')) {
403
+ return 0;
404
+ }
405
+ return countSnapshots(db);
406
+ };
407
+
408
+ export const countPinnedSnapshots = (db: Database): number => {
409
+ if (!tableExists(db, 'topo_snapshots')) {
410
+ return 0;
411
+ }
412
+ return countSnapshots(db, 'pinned_as IS NOT NULL');
413
+ };
414
+
415
+ export const countPrunableSnapshots = (
416
+ db: Database,
417
+ options: { readonly keep: number }
418
+ ): number => {
419
+ if (!tableExists(db, 'topo_snapshots')) {
420
+ return 0;
421
+ }
422
+ const row = db
423
+ .query<{ count: number }, [number]>(
424
+ `SELECT COUNT(*) as count
425
+ FROM (
426
+ SELECT id
427
+ FROM topo_snapshots
428
+ WHERE pinned_as IS NULL
429
+ ORDER BY created_at DESC, id DESC
430
+ LIMIT -1 OFFSET ?
431
+ )`
432
+ )
433
+ .get(options.keep);
434
+ return row?.count ?? 0;
435
+ };
436
+
437
+ export const createTopoSnapshot = (
438
+ db: Database,
439
+ input?: CreateTopoSnapshotInput
440
+ ): TopoSnapshot => {
441
+ ensureTopoSnapshotSchema(db);
442
+ return insertTopoSnapshotRecord(db, input);
443
+ };
444
+
445
+ export const pinTopoSnapshot = (
446
+ db: Database,
447
+ input: { readonly id: string; readonly name: string }
448
+ ): TopoSnapshot | undefined => {
449
+ ensureTopoSnapshotSchema(db);
450
+
451
+ return db.transaction(() => {
452
+ const snapshot = readTopoSnapshot(db, input.id);
453
+ if (snapshot === undefined) {
454
+ return;
455
+ }
456
+
457
+ db.run('UPDATE topo_snapshots SET pinned_as = NULL WHERE pinned_as = ?', [
458
+ input.name,
459
+ ]);
460
+ db.run('UPDATE topo_snapshots SET pinned_as = ? WHERE id = ?', [
461
+ input.name,
462
+ input.id,
463
+ ]);
464
+
465
+ return readTopoSnapshot(db, input.id);
466
+ })();
467
+ };
468
+
469
+ export const unpinTopoSnapshot = (
470
+ db: Database,
471
+ nameOrId: string
472
+ ): TopoSnapshot | undefined => {
473
+ ensureTopoSnapshotSchema(db);
474
+
475
+ return db.transaction(() => {
476
+ const snapshot =
477
+ readPinnedTopoSnapshot(db, nameOrId) ?? readTopoSnapshot(db, nameOrId);
478
+ if (snapshot?.pinnedAs === undefined) {
479
+ return;
480
+ }
481
+
482
+ db.run('UPDATE topo_snapshots SET pinned_as = NULL WHERE id = ?', [
483
+ snapshot.id,
484
+ ]);
485
+
486
+ return readTopoSnapshot(db, snapshot.id);
487
+ })();
488
+ };
489
+
490
+ export const pruneUnpinnedSnapshots = (
491
+ db: Database,
492
+ options: { readonly keep: number }
493
+ ): number => {
494
+ if (!tableExists(db, 'topo_snapshots')) {
495
+ return 0;
496
+ }
497
+ if (countPrunableSnapshots(db, options) === 0) {
498
+ return 0;
499
+ }
500
+
501
+ db.run(
502
+ `DELETE FROM topo_snapshots
503
+ WHERE id IN (
504
+ SELECT id
505
+ FROM topo_snapshots
506
+ WHERE pinned_as IS NULL
507
+ ORDER BY created_at DESC, id DESC
508
+ LIMIT -1 OFFSET ?
509
+ )`,
510
+ [options.keep]
511
+ );
512
+
513
+ return (
514
+ db.query<{ changes: number }, []>('SELECT changes() as changes').get()
515
+ ?.changes ?? 0
516
+ );
517
+ };