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

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 (139) hide show
  1. package/.turbo/turbo-lint.log +1 -1
  2. package/CHANGELOG.md +65 -35
  3. package/README.md +21 -20
  4. package/dist/context.d.ts +1 -1
  5. package/dist/context.d.ts.map +1 -1
  6. package/dist/context.js +5 -4
  7. package/dist/context.js.map +1 -1
  8. package/dist/derive.d.ts +14 -3
  9. package/dist/derive.d.ts.map +1 -1
  10. package/dist/derive.js +36 -12
  11. package/dist/derive.js.map +1 -1
  12. package/dist/draft.d.ts +28 -0
  13. package/dist/draft.d.ts.map +1 -0
  14. package/dist/draft.js +156 -0
  15. package/dist/draft.js.map +1 -0
  16. package/dist/event.d.ts +4 -41
  17. package/dist/event.d.ts.map +1 -1
  18. package/dist/event.js +4 -14
  19. package/dist/event.js.map +1 -1
  20. package/dist/execute.d.ts +10 -10
  21. package/dist/execute.d.ts.map +1 -1
  22. package/dist/execute.js +35 -28
  23. package/dist/execute.js.map +1 -1
  24. package/dist/gate.d.ts +17 -0
  25. package/dist/gate.d.ts.map +1 -0
  26. package/dist/gate.js +21 -0
  27. package/dist/gate.js.map +1 -0
  28. package/dist/index.d.ts +16 -9
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +14 -9
  31. package/dist/index.js.map +1 -1
  32. package/dist/internal/topo-saves.d.ts +47 -0
  33. package/dist/internal/topo-saves.d.ts.map +1 -0
  34. package/dist/internal/topo-saves.js +310 -0
  35. package/dist/internal/topo-saves.js.map +1 -0
  36. package/dist/internal/topo-store-read.d.ts +67 -0
  37. package/dist/internal/topo-store-read.d.ts.map +1 -0
  38. package/dist/internal/topo-store-read.js +222 -0
  39. package/dist/internal/topo-store-read.js.map +1 -0
  40. package/dist/internal/topo-store.d.ts +12 -0
  41. package/dist/internal/topo-store.d.ts.map +1 -0
  42. package/dist/internal/topo-store.js +571 -0
  43. package/dist/internal/topo-store.js.map +1 -0
  44. package/dist/internal/trails-db.d.ts +16 -0
  45. package/dist/internal/trails-db.d.ts.map +1 -0
  46. package/dist/internal/trails-db.js +118 -0
  47. package/dist/internal/trails-db.js.map +1 -0
  48. package/dist/layer.d.ts +5 -16
  49. package/dist/layer.d.ts.map +1 -1
  50. package/dist/layer.js +3 -20
  51. package/dist/layer.js.map +1 -1
  52. package/dist/provision-config.d.ts +22 -0
  53. package/dist/provision-config.d.ts.map +1 -0
  54. package/dist/provision-config.js +210 -0
  55. package/dist/provision-config.js.map +1 -0
  56. package/dist/provision.d.ts +71 -0
  57. package/dist/provision.d.ts.map +1 -0
  58. package/dist/provision.js +56 -0
  59. package/dist/provision.js.map +1 -0
  60. package/dist/run.d.ts +27 -0
  61. package/dist/run.d.ts.map +1 -0
  62. package/dist/run.js +34 -0
  63. package/dist/run.js.map +1 -0
  64. package/dist/service-config.d.ts +7 -7
  65. package/dist/service-config.d.ts.map +1 -1
  66. package/dist/service-config.js +75 -73
  67. package/dist/service-config.js.map +1 -1
  68. package/dist/service.d.ts +32 -36
  69. package/dist/service.d.ts.map +1 -1
  70. package/dist/service.js +21 -21
  71. package/dist/service.js.map +1 -1
  72. package/dist/signal.d.ts +33 -0
  73. package/dist/signal.d.ts.map +1 -0
  74. package/dist/signal.js +17 -0
  75. package/dist/signal.js.map +1 -0
  76. package/dist/topo-store.d.ts +48 -0
  77. package/dist/topo-store.d.ts.map +1 -0
  78. package/dist/topo-store.js +175 -0
  79. package/dist/topo-store.js.map +1 -0
  80. package/dist/topo.d.ts +10 -10
  81. package/dist/topo.d.ts.map +1 -1
  82. package/dist/topo.js +48 -35
  83. package/dist/topo.js.map +1 -1
  84. package/dist/trail.d.ts +16 -16
  85. package/dist/trail.d.ts.map +1 -1
  86. package/dist/trail.js +5 -4
  87. package/dist/trail.js.map +1 -1
  88. package/dist/types.d.ts +10 -10
  89. package/dist/types.d.ts.map +1 -1
  90. package/dist/types.js +2 -2
  91. package/dist/types.js.map +1 -1
  92. package/dist/validate-established-topo.d.ts +76 -0
  93. package/dist/validate-established-topo.d.ts.map +1 -0
  94. package/dist/validate-established-topo.js +43 -0
  95. package/dist/validate-established-topo.js.map +1 -0
  96. package/dist/validate-topo.d.ts +2 -2
  97. package/dist/validate-topo.d.ts.map +1 -1
  98. package/dist/validate-topo.js +34 -32
  99. package/dist/validate-topo.js.map +1 -1
  100. package/package.json +4 -1
  101. package/src/__tests__/context.test.ts +8 -8
  102. package/src/__tests__/derive.test.ts +58 -1
  103. package/src/__tests__/execute.test.ts +124 -122
  104. package/src/__tests__/{layer.test.ts → gate.test.ts} +26 -26
  105. package/src/__tests__/{dispatch.test.ts → run.test.ts} +39 -39
  106. package/src/__tests__/service-config.test.ts +41 -37
  107. package/src/__tests__/service.test.ts +71 -71
  108. package/src/__tests__/{event.test.ts → signal.test.ts} +15 -15
  109. package/src/__tests__/topo-store-read.test.ts +251 -0
  110. package/src/__tests__/topo-store.test.ts +469 -0
  111. package/src/__tests__/topo.test.ts +54 -54
  112. package/src/__tests__/trail-permit.test.ts +4 -4
  113. package/src/__tests__/trail.test.ts +58 -58
  114. package/src/__tests__/trails-db.test.ts +191 -0
  115. package/src/__tests__/type-utils.test.ts +3 -3
  116. package/src/__tests__/validate-topo.test.ts +205 -38
  117. package/src/context.ts +5 -4
  118. package/src/derive.ts +47 -16
  119. package/src/draft.ts +334 -0
  120. package/src/event.ts +11 -73
  121. package/src/execute.ts +56 -40
  122. package/src/{layer.ts → gate.ts} +13 -13
  123. package/src/index.ts +54 -23
  124. package/src/internal/topo-saves.ts +429 -0
  125. package/src/internal/topo-store-read.ts +473 -0
  126. package/src/internal/topo-store.ts +1087 -0
  127. package/src/internal/trails-db.ts +189 -0
  128. package/src/{service-config.ts → provision-config.ts} +124 -105
  129. package/src/provision.ts +148 -0
  130. package/src/{dispatch.ts → run.ts} +8 -8
  131. package/src/signal.ts +65 -0
  132. package/src/topo-store.ts +301 -0
  133. package/src/topo.ts +74 -52
  134. package/src/trail.ts +23 -22
  135. package/src/types.ts +11 -11
  136. package/src/validate-established-topo.ts +63 -0
  137. package/src/validate-topo.ts +38 -34
  138. package/tsconfig.tsbuildinfo +1 -1
  139. package/src/service.ts +0 -145
@@ -0,0 +1,148 @@
1
+ import { NotFoundError } from './errors.js';
2
+ import type { Result } from './result.js';
3
+ import type { ProvisionLookup, TrailContext } from './types.js';
4
+ import type { z } from 'zod';
5
+
6
+ /**
7
+ * Stable process-scoped fields available when constructing a provision.
8
+ *
9
+ * Provisions are app-level singletons, so they intentionally do not receive
10
+ * the full per-request TrailContext. When a provision declares a `config` schema,
11
+ * the validated config is passed as `svc.config`.
12
+ */
13
+ export type ProvisionContext<C = unknown> = Pick<
14
+ TrailContext,
15
+ 'cwd' | 'env' | 'workspaceRoot'
16
+ > & {
17
+ readonly config: C;
18
+ };
19
+
20
+ /**
21
+ * Everything needed to describe a provision before a factory is introduced.
22
+ *
23
+ * When `config` is a Zod schema, the `create` callback receives
24
+ * `ProvisionContext<C>` with the validated config value.
25
+ */
26
+ export interface ProvisionSpec<T, C = unknown> {
27
+ /** Create the provision instance from stable process-scoped context. */
28
+ readonly create: (
29
+ svc: ProvisionContext<C>
30
+ ) => Result<T, Error> | Promise<Result<T, Error>>;
31
+ /** Config schema — when present, config is validated and passed to `create`. */
32
+ readonly config?: z.ZodType<C> | undefined;
33
+ /** Optional cleanup performed when the hosting trailhead shuts down. */
34
+ readonly dispose?: ((provision: T) => void | Promise<void>) | undefined;
35
+ /** Optional operational readiness probe for introspection tooling. */
36
+ readonly health?:
37
+ | ((
38
+ provision: T
39
+ ) => Result<unknown, Error> | Promise<Result<unknown, Error>>)
40
+ | undefined;
41
+ /** Optional test factory used by higher-level helpers. */
42
+ readonly mock?: (() => T | Promise<T>) | undefined;
43
+ /** Human-readable description. */
44
+ readonly description?: string | undefined;
45
+ /** Arbitrary meta for tooling and filtering. */
46
+ readonly meta?: Readonly<Record<string, unknown>> | undefined;
47
+ }
48
+
49
+ /** A typed provision definition. */
50
+ export interface Provision<T> extends ProvisionSpec<T> {
51
+ readonly kind: 'provision';
52
+ readonly id: string;
53
+ /** Read the resolved provision instance from a trail context. */
54
+ from(ctx: TrailContext): T;
55
+ }
56
+
57
+ /**
58
+ * Existential type for heterogeneous provision collections.
59
+ *
60
+ * `Provision<T>` includes function parameters in `dispose`/`health`, so
61
+ * `unknown` is too narrow for mixed provision arrays. `any` is the correct
62
+ * existential here.
63
+ */
64
+ // oxlint-disable-next-line no-explicit-any -- existential type for heterogeneous provision collections
65
+ export type AnyProvision = Provision<any>;
66
+
67
+ /** Explicit runtime overrides keyed by provision ID. */
68
+ export type ProvisionOverrideMap = Readonly<Record<string, unknown>>;
69
+
70
+ const getProvisionId = <T>(
71
+ provisionOrId: string | Pick<Provision<T>, 'id'>
72
+ ): string =>
73
+ typeof provisionOrId === 'string' ? provisionOrId : provisionOrId.id;
74
+
75
+ const getProvisionInstance = <T>(
76
+ ctx: Pick<TrailContext, 'extensions'>,
77
+ provisionOrId: string | Pick<Provision<T>, 'id'>
78
+ ): T => {
79
+ const id = getProvisionId(provisionOrId);
80
+ return ctx.extensions?.[id] as T;
81
+ };
82
+
83
+ const hasProvisionInstance = (
84
+ ctx: Pick<TrailContext, 'extensions'>,
85
+ provisionOrId: string | Pick<AnyProvision, 'id'>
86
+ ): boolean =>
87
+ Object.hasOwn(ctx.extensions ?? {}, getProvisionId(provisionOrId));
88
+
89
+ /** Create a `ctx.provision(...)` accessor bound to a concrete context snapshot. */
90
+ export const createProvisionLookup = (
91
+ getContext: () => Pick<TrailContext, 'extensions'>
92
+ ): ProvisionLookup =>
93
+ ((provisionOrId: string | Pick<AnyProvision, 'id'>) => {
94
+ const id = getProvisionId(provisionOrId);
95
+ const ctx = getContext();
96
+ if (!hasProvisionInstance(ctx, id)) {
97
+ throw new NotFoundError(`Provision "${id}" not found in trail context`);
98
+ }
99
+ return getProvisionInstance(ctx, id);
100
+ }) as ProvisionLookup;
101
+
102
+ /**
103
+ * Create a typed provision definition.
104
+ *
105
+ * The provision object is inert until a later execution branch resolves concrete
106
+ * instances into TrailContext extensions.
107
+ */
108
+ export const provision = <T>(
109
+ id: string,
110
+ spec: ProvisionSpec<T>
111
+ ): Provision<T> =>
112
+ Object.freeze({
113
+ ...spec,
114
+ from(ctx: TrailContext): T {
115
+ const lookup = ctx.provision ?? createProvisionLookup(() => ctx);
116
+ return lookup(this);
117
+ },
118
+ id,
119
+ kind: 'provision' as const,
120
+ });
121
+
122
+ /** Narrow unknown values to provision definitions during topo discovery. */
123
+ export const isProvision = (value: unknown): value is AnyProvision => {
124
+ if (typeof value !== 'object' || value === null) {
125
+ return false;
126
+ }
127
+ const v = value as { kind?: unknown; id?: unknown };
128
+ return v.kind === 'provision' && typeof v.id === 'string';
129
+ };
130
+
131
+ /**
132
+ * Return the first duplicate provision ID in a collection, if any.
133
+ *
134
+ * This supports later topo registration without each caller duplicating the
135
+ * same scan logic.
136
+ */
137
+ export const findDuplicateProvisionId = (
138
+ provisions: readonly Pick<AnyProvision, 'id'>[]
139
+ ): string | undefined => {
140
+ const seen = new Set<string>();
141
+ for (const candidate of provisions) {
142
+ if (seen.has(candidate.id)) {
143
+ return candidate.id;
144
+ }
145
+ seen.add(candidate.id);
146
+ }
147
+ return undefined;
148
+ };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Headless trail execution the "no-surface" surface.
2
+ * Headless trail execution without mounting a trailhead.
3
3
  *
4
4
  * Looks up a trail by ID in a topo, then delegates to `executeTrail`.
5
5
  * Returns a `Result` and never throws.
@@ -15,15 +15,15 @@ import { Result } from './result.js';
15
15
  // Options
16
16
  // ---------------------------------------------------------------------------
17
17
 
18
- /** Options forwarded to `executeTrail` from `dispatch`. */
19
- export type DispatchOptions = ExecuteTrailOptions;
18
+ /** Options forwarded to `executeTrail` from `run`. */
19
+ export type RunOptions = ExecuteTrailOptions;
20
20
 
21
21
  // ---------------------------------------------------------------------------
22
- // dispatch()
22
+ // run()
23
23
  // ---------------------------------------------------------------------------
24
24
 
25
25
  /**
26
- * Execute a trail by ID from a topo without mounting a surface.
26
+ * Execute a trail by ID from a topo without mounting a trailhead.
27
27
  *
28
28
  * Resolves the trail from the topo, then runs it through the standard
29
29
  * `executeTrail` pipeline. Returns `Result.err(NotFoundError)` if the
@@ -32,15 +32,15 @@ export type DispatchOptions = ExecuteTrailOptions;
32
32
  *
33
33
  * @example
34
34
  * ```typescript
35
- * const result = await dispatch(myTopo, 'greet', { name: 'Alice' });
35
+ * const result = await run(myTopo, 'greet', { name: 'Alice' });
36
36
  * if (result.isOk()) console.log(result.value);
37
37
  * ```
38
38
  */
39
- export const dispatch = (
39
+ export const run = (
40
40
  topo: Topo,
41
41
  id: string,
42
42
  input: unknown,
43
- options?: DispatchOptions
43
+ options?: RunOptions
44
44
  ): Promise<Result<unknown, Error>> => {
45
45
  const trail = topo.get(id);
46
46
  if (trail === undefined) {
package/src/signal.ts ADDED
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Signal — a named payload schema with optional provenance meta.
3
+ */
4
+
5
+ import type { z } from 'zod';
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Spec (input to the factory)
9
+ // ---------------------------------------------------------------------------
10
+
11
+ export interface SignalSpec<T> {
12
+ readonly payload: z.ZodType<T>;
13
+ readonly description?: string | undefined;
14
+ readonly meta?: Readonly<Record<string, unknown>> | undefined;
15
+ /** Trail IDs that produce this signal (e.g. the trails it originates from). */
16
+ readonly from?: readonly string[] | undefined;
17
+ }
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Shape (output of the factory)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ export interface Signal<T> {
24
+ readonly id: string;
25
+ readonly kind: 'signal';
26
+ readonly payload: z.ZodType<T>;
27
+ readonly description?: string | undefined;
28
+ readonly meta?: Readonly<Record<string, unknown>> | undefined;
29
+ /** Trail IDs that produce this signal (e.g. the trails it originates from). */
30
+ readonly from?: readonly string[] | undefined;
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Factory
35
+ // ---------------------------------------------------------------------------
36
+
37
+ /**
38
+ * Create a signal definition.
39
+ *
40
+ * A signal is a named payload schema describing something that happened.
41
+ * Returns a frozen object with `kind: "signal"` and all spec fields.
42
+ */
43
+ export function signal<T>(id: string, spec: SignalSpec<T>): Signal<T>;
44
+ export function signal<T>(
45
+ spec: SignalSpec<T> & { readonly id: string }
46
+ ): Signal<T>;
47
+ export function signal<T>(
48
+ idOrSpec: string | (SignalSpec<T> & { readonly id: string }),
49
+ maybeSpec?: SignalSpec<T>
50
+ ): Signal<T> {
51
+ const resolvedId = typeof idOrSpec === 'string' ? idOrSpec : idOrSpec.id;
52
+ // oxlint-disable-next-line no-non-null-assertion -- overload guarantees maybeSpec when idOrSpec is string
53
+ const resolvedSpec = typeof idOrSpec === 'string' ? maybeSpec! : idOrSpec;
54
+ return Object.freeze({
55
+ description: resolvedSpec.description,
56
+ from: resolvedSpec.from ? Object.freeze([...resolvedSpec.from]) : undefined,
57
+ id: resolvedId,
58
+ kind: 'signal' as const,
59
+ meta: resolvedSpec.meta,
60
+ payload: resolvedSpec.payload,
61
+ });
62
+ }
63
+
64
+ /** Existential type for heterogeneous signal collections */
65
+ export type AnySignal = Signal<unknown>;
@@ -0,0 +1,301 @@
1
+ import type { SQLQueryBindings } from 'bun:sqlite';
2
+ import { existsSync } from 'node:fs';
3
+
4
+ import { NotFoundError } from './errors.js';
5
+ import { provision } from './provision.js';
6
+ import { Result } from './result.js';
7
+ import type { TopoPinRecord, TopoSaveRecord } from './internal/topo-saves.js';
8
+ import { getTopoPin } from './internal/topo-saves.js';
9
+ import type {
10
+ TopoStoreExportRecord,
11
+ TopoStoreProvisionRecord,
12
+ TopoStoreRef,
13
+ TopoStoreTrailDetailRecord,
14
+ TopoStoreTrailRecord,
15
+ } from './internal/topo-store-read.js';
16
+ import {
17
+ getTopoStoreExport,
18
+ getTopoStoreProvision,
19
+ getTopoStoreTrail,
20
+ listTopoStorePins,
21
+ listTopoStoreProvisions,
22
+ listTopoStoreSaves,
23
+ listTopoStoreTrails,
24
+ queryTopoStore,
25
+ resolveTopoStoreSave,
26
+ } from './internal/topo-store-read.js';
27
+ import { openReadTrailsDb, resolveTrailsDbPath } from './internal/trails-db.js';
28
+ import type { TrailsDbLocationOptions } from './internal/trails-db.js';
29
+
30
+ export type {
31
+ TopoStoreExportRecord,
32
+ TopoStoreProvisionRecord,
33
+ TopoStoreRef,
34
+ TopoStoreTrailDetailRecord,
35
+ TopoStoreTrailRecord,
36
+ } from './internal/topo-store-read.js';
37
+
38
+ export interface ReadOnlyTopoStore {
39
+ readonly exports: {
40
+ get(ref?: TopoStoreRef): TopoStoreExportRecord | undefined;
41
+ };
42
+ readonly pins: {
43
+ get(name: string): TopoPinRecord | undefined;
44
+ list(): readonly TopoPinRecord[];
45
+ };
46
+ query<TRow extends Record<string, unknown>>(
47
+ sql: string,
48
+ bindings?: readonly SQLQueryBindings[]
49
+ ): readonly TRow[];
50
+ readonly provisions: {
51
+ get(
52
+ id: string,
53
+ options?: { readonly save?: TopoStoreRef }
54
+ ): TopoStoreProvisionRecord | undefined;
55
+ list(options?: {
56
+ readonly save?: TopoStoreRef;
57
+ }): readonly TopoStoreProvisionRecord[];
58
+ };
59
+ readonly saves: {
60
+ get(ref?: TopoStoreRef): TopoSaveRecord | undefined;
61
+ latest(): TopoSaveRecord | undefined;
62
+ list(): readonly TopoSaveRecord[];
63
+ };
64
+ readonly trails: {
65
+ get(
66
+ id: string,
67
+ options?: { readonly save?: TopoStoreRef }
68
+ ): TopoStoreTrailDetailRecord | undefined;
69
+ list(options?: {
70
+ readonly intent?: TopoStoreTrailRecord['intent'];
71
+ readonly save?: TopoStoreRef;
72
+ }): readonly TopoStoreTrailRecord[];
73
+ };
74
+ }
75
+
76
+ export interface MockTopoStoreSeed {
77
+ readonly exports?: readonly TopoStoreExportRecord[];
78
+ readonly pins?: readonly TopoPinRecord[];
79
+ readonly provisions?: readonly TopoStoreProvisionRecord[];
80
+ readonly saves?: readonly TopoSaveRecord[];
81
+ readonly trails?: readonly TopoStoreTrailDetailRecord[];
82
+ }
83
+
84
+ const missingStoreMessage =
85
+ 'No saved topo state found. Populate trails.db first or run a topo-backed surface.';
86
+
87
+ const resolveStoreRootDir = (options?: TrailsDbLocationOptions): string =>
88
+ options?.rootDir ?? process.cwd();
89
+
90
+ const requireReadDb = (
91
+ options?: TrailsDbLocationOptions
92
+ ): ReturnType<typeof openReadTrailsDb> => {
93
+ const dbPath = resolveTrailsDbPath(options);
94
+ if (!existsSync(dbPath)) {
95
+ throw new NotFoundError(missingStoreMessage);
96
+ }
97
+ return openReadTrailsDb(options);
98
+ };
99
+
100
+ const requireSavedTopoState = (
101
+ db: ReturnType<typeof openReadTrailsDb>
102
+ ): void => {
103
+ if (resolveTopoStoreSave(db) === undefined) {
104
+ throw new NotFoundError(missingStoreMessage);
105
+ }
106
+ };
107
+
108
+ const withStoredTopoState = <T>(
109
+ options: TrailsDbLocationOptions | undefined,
110
+ run: (db: ReturnType<typeof openReadTrailsDb>) => T
111
+ ): T => {
112
+ const db = requireReadDb(options);
113
+ try {
114
+ requireSavedTopoState(db);
115
+ return run(db);
116
+ } finally {
117
+ db.close();
118
+ }
119
+ };
120
+
121
+ const createSeedResolver = (seed?: MockTopoStoreSeed) => {
122
+ const saves = [...(seed?.saves ?? [])];
123
+ const pins = [...(seed?.pins ?? [])];
124
+ const trails = [...(seed?.trails ?? [])];
125
+ const provisions = [...(seed?.provisions ?? [])];
126
+ const exports = [...(seed?.exports ?? [])];
127
+
128
+ const resolveSave = (ref?: TopoStoreRef): TopoSaveRecord | undefined => {
129
+ if (ref?.saveId !== undefined) {
130
+ return saves.find((save) => save.id === ref.saveId);
131
+ }
132
+ if (ref?.pin !== undefined) {
133
+ const pin = pins.find((candidate) => candidate.name === ref.pin);
134
+ return pin === undefined
135
+ ? undefined
136
+ : saves.find((save) => save.id === pin.saveId);
137
+ }
138
+ return saves[0];
139
+ };
140
+
141
+ return {
142
+ exports,
143
+ pins,
144
+ provisions,
145
+ resolveSave,
146
+ saves,
147
+ trails,
148
+ };
149
+ };
150
+
151
+ export const createMockTopoStore = (
152
+ seed?: MockTopoStoreSeed
153
+ ): ReadOnlyTopoStore => {
154
+ const resolved = createSeedResolver(seed);
155
+
156
+ return {
157
+ exports: {
158
+ get(ref?: TopoStoreRef) {
159
+ const save = resolved.resolveSave(ref);
160
+ return save === undefined
161
+ ? undefined
162
+ : resolved.exports.find((entry) => entry.save.id === save.id);
163
+ },
164
+ },
165
+ pins: {
166
+ get(name: string) {
167
+ return resolved.pins.find((pin) => pin.name === name);
168
+ },
169
+ list() {
170
+ return resolved.pins;
171
+ },
172
+ },
173
+ provisions: {
174
+ get(id, options) {
175
+ const save = resolved.resolveSave(options?.save);
176
+ return resolved.provisions.find(
177
+ (item) =>
178
+ item.id === id && (save === undefined || item.saveId === save.id)
179
+ );
180
+ },
181
+ list(options) {
182
+ const save = resolved.resolveSave(options?.save);
183
+ return save === undefined
184
+ ? []
185
+ : resolved.provisions.filter((item) => item.saveId === save.id);
186
+ },
187
+ },
188
+ query() {
189
+ throw new NotFoundError(
190
+ 'Mock topoStore.query() is unsupported. Seed typed accessors instead.'
191
+ );
192
+ },
193
+ saves: {
194
+ get(ref?: TopoStoreRef) {
195
+ return resolved.resolveSave(ref);
196
+ },
197
+ latest() {
198
+ return resolved.saves[0];
199
+ },
200
+ list() {
201
+ return resolved.saves;
202
+ },
203
+ },
204
+ trails: {
205
+ get(id, options) {
206
+ const save = resolved.resolveSave(options?.save);
207
+ if (save === undefined) {
208
+ return;
209
+ }
210
+ return resolved.trails.find(
211
+ (trail) => trail.id === id && trail.saveId === save.id
212
+ );
213
+ },
214
+ list(options) {
215
+ const save = resolved.resolveSave(options?.save);
216
+ if (save === undefined) {
217
+ return [];
218
+ }
219
+ return resolved.trails.filter(
220
+ (trail) =>
221
+ trail.saveId === save.id &&
222
+ (options?.intent === undefined || trail.intent === options.intent)
223
+ );
224
+ },
225
+ },
226
+ };
227
+ };
228
+
229
+ export const createTopoStore = (
230
+ options?: TrailsDbLocationOptions
231
+ ): ReadOnlyTopoStore => ({
232
+ exports: {
233
+ get(ref?: TopoStoreRef) {
234
+ return withStoredTopoState(options, (db) => getTopoStoreExport(db, ref));
235
+ },
236
+ },
237
+ pins: {
238
+ get(name: string) {
239
+ return withStoredTopoState(options, (db) => getTopoPin(db, name));
240
+ },
241
+ list() {
242
+ return withStoredTopoState(options, (db) => listTopoStorePins(db));
243
+ },
244
+ },
245
+ provisions: {
246
+ get(id, queryOptions) {
247
+ return withStoredTopoState(options, (db) =>
248
+ getTopoStoreProvision(db, id, queryOptions)
249
+ );
250
+ },
251
+ list(queryOptions) {
252
+ return withStoredTopoState(options, (db) =>
253
+ listTopoStoreProvisions(db, queryOptions)
254
+ );
255
+ },
256
+ },
257
+ query<TRow extends Record<string, unknown>>(
258
+ sql: string,
259
+ bindings?: readonly SQLQueryBindings[]
260
+ ) {
261
+ return withStoredTopoState(options, (db) =>
262
+ queryTopoStore<TRow>(db, sql, bindings)
263
+ );
264
+ },
265
+ saves: {
266
+ get(ref?: TopoStoreRef) {
267
+ return withStoredTopoState(options, (db) =>
268
+ resolveTopoStoreSave(db, ref)
269
+ );
270
+ },
271
+ latest() {
272
+ return withStoredTopoState(options, (db) => resolveTopoStoreSave(db));
273
+ },
274
+ list() {
275
+ return withStoredTopoState(options, (db) => listTopoStoreSaves(db));
276
+ },
277
+ },
278
+ trails: {
279
+ get(id, queryOptions) {
280
+ return withStoredTopoState(options, (db) =>
281
+ getTopoStoreTrail(db, id, queryOptions)
282
+ );
283
+ },
284
+ list(queryOptions) {
285
+ return withStoredTopoState(options, (db) =>
286
+ listTopoStoreTrails(db, queryOptions)
287
+ );
288
+ },
289
+ },
290
+ });
291
+
292
+ export const topoStore = provision('topo.store', {
293
+ create: (svc) =>
294
+ Result.ok(
295
+ createTopoStore({
296
+ rootDir: svc.workspaceRoot ?? svc.cwd ?? resolveStoreRootDir(),
297
+ })
298
+ ),
299
+ description: 'Read-only query access to saved topo state in trails.db',
300
+ mock: () => createMockTopoStore(),
301
+ });