@ontrails/core 1.0.0-beta.14 → 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.
- package/.turbo/turbo-lint.log +1 -1
- package/CHANGELOG.md +11 -0
- package/README.md +17 -13
- package/dist/branded.d.ts +1 -1
- package/dist/branded.d.ts.map +1 -1
- package/dist/branded.js +1 -1
- package/dist/branded.js.map +1 -1
- package/dist/context.d.ts +13 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +19 -3
- package/dist/context.js.map +1 -1
- package/dist/contour.d.ts +81 -0
- package/dist/contour.d.ts.map +1 -0
- package/dist/contour.js +176 -0
- package/dist/contour.js.map +1 -0
- package/dist/cross-schema.d.ts +18 -0
- package/dist/cross-schema.d.ts.map +1 -0
- package/dist/cross-schema.js +29 -0
- package/dist/cross-schema.js.map +1 -0
- package/dist/draft.d.ts +5 -5
- package/dist/draft.d.ts.map +1 -1
- package/dist/draft.js +28 -27
- package/dist/draft.js.map +1 -1
- package/dist/errors.d.ts +62 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +44 -1
- package/dist/errors.js.map +1 -1
- package/dist/execute.d.ts +26 -9
- package/dist/execute.d.ts.map +1 -1
- package/dist/execute.js +441 -20
- package/dist/execute.js.map +1 -1
- package/dist/fire.d.ts +43 -0
- package/dist/fire.d.ts.map +1 -0
- package/dist/fire.js +185 -0
- package/dist/fire.js.map +1 -0
- package/dist/index.d.ts +31 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +23 -13
- package/dist/index.js.map +1 -1
- package/dist/internal/cross-batch.d.ts +44 -0
- package/dist/internal/cross-batch.d.ts.map +1 -0
- package/dist/internal/cross-batch.js +58 -0
- package/dist/internal/cross-batch.js.map +1 -0
- package/dist/internal/fork-ctx.d.ts +41 -0
- package/dist/internal/fork-ctx.d.ts.map +1 -0
- package/dist/internal/fork-ctx.js +47 -0
- package/dist/internal/fork-ctx.js.map +1 -0
- package/dist/internal/signal-ref.d.ts +15 -0
- package/dist/internal/signal-ref.d.ts.map +1 -0
- package/dist/internal/signal-ref.js +41 -0
- package/dist/internal/signal-ref.js.map +1 -0
- package/dist/internal/topo-saves.d.ts +8 -2
- package/dist/internal/topo-saves.d.ts.map +1 -1
- package/dist/internal/topo-saves.js +162 -23
- package/dist/internal/topo-saves.js.map +1 -1
- package/dist/internal/topo-snapshots.d.ts +57 -0
- package/dist/internal/topo-snapshots.d.ts.map +1 -0
- package/dist/internal/topo-snapshots.js +372 -0
- package/dist/internal/topo-snapshots.js.map +1 -0
- package/dist/internal/topo-store-read.d.ts +23 -20
- package/dist/internal/topo-store-read.d.ts.map +1 -1
- package/dist/internal/topo-store-read.js +82 -83
- package/dist/internal/topo-store-read.js.map +1 -1
- package/dist/internal/topo-store.d.ts +19 -5
- package/dist/internal/topo-store.d.ts.map +1 -1
- package/dist/internal/topo-store.js +174 -124
- package/dist/internal/topo-store.js.map +1 -1
- package/dist/internal/tracing.d.ts +98 -0
- package/dist/internal/tracing.d.ts.map +1 -0
- package/dist/internal/tracing.js +102 -0
- package/dist/internal/tracing.js.map +1 -0
- package/dist/internal/trails-db.d.ts +2 -2
- package/dist/internal/trails-db.d.ts.map +1 -1
- package/dist/internal/trails-db.js +8 -8
- package/dist/internal/trails-db.js.map +1 -1
- package/dist/internal/zod-wrappers.d.ts +40 -0
- package/dist/internal/zod-wrappers.d.ts.map +1 -0
- package/dist/internal/zod-wrappers.js +59 -0
- package/dist/internal/zod-wrappers.js.map +1 -0
- package/dist/layer.d.ts +22 -5
- package/dist/layer.d.ts.map +1 -1
- package/dist/layer.js +20 -3
- package/dist/layer.js.map +1 -1
- package/dist/path-security.d.ts +1 -1
- package/dist/path-security.d.ts.map +1 -1
- package/dist/path-security.js +1 -1
- package/dist/path-security.js.map +1 -1
- package/dist/resilience.d.ts +1 -1
- package/dist/resilience.d.ts.map +1 -1
- package/dist/resilience.js +3 -3
- package/dist/resilience.js.map +1 -1
- package/dist/resource-config.d.ts +22 -0
- package/dist/resource-config.d.ts.map +1 -0
- package/dist/resource-config.js +210 -0
- package/dist/resource-config.js.map +1 -0
- package/dist/resource.d.ts +74 -0
- package/dist/resource.d.ts.map +1 -0
- package/dist/resource.js +61 -0
- package/dist/resource.js.map +1 -0
- package/dist/run.d.ts +4 -3
- package/dist/run.d.ts.map +1 -1
- package/dist/run.js +5 -4
- package/dist/run.js.map +1 -1
- package/dist/serialization.d.ts.map +1 -1
- package/dist/serialization.js +2 -1
- package/dist/serialization.js.map +1 -1
- package/dist/store/accessor-protocol.d.ts +51 -0
- package/dist/store/accessor-protocol.d.ts.map +1 -0
- package/dist/store/accessor-protocol.js +2 -0
- package/dist/store/accessor-protocol.js.map +1 -0
- package/dist/store/index.d.ts +2 -0
- package/dist/store/index.d.ts.map +1 -0
- package/dist/store/index.js +2 -0
- package/dist/store/index.js.map +1 -0
- package/dist/surface-filter.d.ts +13 -0
- package/dist/surface-filter.d.ts.map +1 -0
- package/dist/surface-filter.js +76 -0
- package/dist/surface-filter.js.map +1 -0
- package/dist/topo-store.d.ts +36 -22
- package/dist/topo-store.d.ts.map +1 -1
- package/dist/topo-store.js +215 -71
- package/dist/topo-store.js.map +1 -1
- package/dist/topo.d.ts +23 -9
- package/dist/topo.d.ts.map +1 -1
- package/dist/topo.js +175 -46
- package/dist/topo.js.map +1 -1
- package/dist/trail.d.ts +86 -19
- package/dist/trail.d.ts.map +1 -1
- package/dist/trail.js +59 -4
- package/dist/trail.js.map +1 -1
- package/dist/trails/derive-trail.d.ts +66 -0
- package/dist/trails/derive-trail.d.ts.map +1 -0
- package/dist/trails/derive-trail.js +378 -0
- package/dist/trails/derive-trail.js.map +1 -0
- package/dist/trails/index.d.ts +5 -0
- package/dist/trails/index.d.ts.map +1 -0
- package/dist/trails/index.js +3 -0
- package/dist/trails/index.js.map +1 -0
- package/dist/trails/ingest.d.ts +24 -0
- package/dist/trails/ingest.d.ts.map +1 -0
- package/dist/trails/ingest.js +63 -0
- package/dist/trails/ingest.js.map +1 -0
- package/dist/transport-error-map.d.ts +106 -0
- package/dist/transport-error-map.d.ts.map +1 -0
- package/dist/transport-error-map.js +24 -0
- package/dist/transport-error-map.js.map +1 -0
- package/dist/type-checks.test-d.d.ts +71 -0
- package/dist/type-checks.test-d.d.ts.map +1 -0
- package/dist/type-checks.test-d.js +12 -0
- package/dist/type-checks.test-d.js.map +1 -0
- package/dist/type-utils.d.ts +11 -2
- package/dist/type-utils.d.ts.map +1 -1
- package/dist/type-utils.js.map +1 -1
- package/dist/types.d.ts +113 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/validate-established-topo.js +2 -2
- package/dist/validate-established-topo.js.map +1 -1
- package/dist/validate-topo.d.ts.map +1 -1
- package/dist/validate-topo.js +24 -7
- package/dist/validate-topo.js.map +1 -1
- package/dist/workspace.d.ts +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +1 -1
- package/dist/workspace.js.map +1 -1
- package/package.json +8 -2
- package/src/__tests__/branded.test.ts +5 -5
- package/src/__tests__/context.test.ts +3 -3
- package/src/__tests__/contour.test.ts +263 -0
- package/src/__tests__/derive-trail.test.ts +668 -0
- package/src/__tests__/errors.test.ts +118 -0
- package/src/__tests__/execute.test.ts +1179 -141
- package/src/__tests__/fire.test.ts +1056 -0
- package/src/__tests__/fork-ctx.test.ts +83 -0
- package/src/__tests__/ingest.test.ts +191 -0
- package/src/__tests__/{gate.test.ts → layer.test.ts} +21 -21
- package/src/__tests__/path-security.test.ts +7 -7
- package/src/__tests__/resilience.test.ts +8 -8
- package/src/__tests__/resource.test.ts +204 -0
- package/src/__tests__/run.test.ts +13 -13
- package/src/__tests__/service-config.test.ts +28 -32
- package/src/__tests__/surface-filter.test.ts +196 -0
- package/src/__tests__/topo-store-read.test.ts +41 -36
- package/src/__tests__/topo-store.test.ts +493 -93
- package/src/__tests__/topo.test.ts +350 -54
- package/src/__tests__/trail.test.ts +189 -24
- package/src/__tests__/trails-db.test.ts +32 -25
- package/src/__tests__/transport-error-map.test.ts +82 -0
- package/src/__tests__/type-utils.test.ts +29 -1
- package/src/__tests__/validate-topo.test.ts +147 -25
- package/src/__tests__/workspace.test.ts +6 -6
- package/src/__tests__/zod-wrappers.test.ts +62 -0
- package/src/branded.ts +1 -1
- package/src/context.ts +24 -4
- package/src/contour.ts +344 -0
- package/src/cross-schema.ts +36 -0
- package/src/draft.ts +56 -52
- package/src/errors.ts +62 -18
- package/src/execute.ts +840 -29
- package/src/fire.ts +295 -0
- package/src/index.ts +116 -29
- package/src/internal/cross-batch.ts +77 -0
- package/src/internal/fork-ctx.ts +69 -0
- package/src/internal/signal-ref.ts +87 -0
- package/src/internal/topo-snapshots.ts +517 -0
- package/src/internal/topo-store-read.ts +140 -132
- package/src/internal/topo-store.ts +291 -186
- package/src/internal/tracing.ts +187 -0
- package/src/internal/trails-db.ts +9 -11
- package/src/internal/zod-wrappers.ts +78 -0
- package/src/layer.ts +50 -0
- package/src/path-security.ts +1 -1
- package/src/resilience.ts +3 -3
- package/src/{provision-config.ts → resource-config.ts} +109 -123
- package/src/resource.ts +155 -0
- package/src/run.ts +5 -4
- package/src/serialization.ts +2 -0
- package/src/store/accessor-protocol.ts +56 -0
- package/src/store/index.ts +4 -0
- package/src/surface-filter.ts +182 -0
- package/src/topo-store.ts +313 -104
- package/src/topo.ts +328 -57
- package/src/trail.ts +199 -31
- package/src/trails/derive-trail.ts +842 -0
- package/src/trails/index.ts +9 -0
- package/src/trails/ingest.ts +146 -0
- package/src/transport-error-map.ts +56 -0
- package/src/type-checks.test-d.ts +106 -0
- package/src/type-utils.ts +17 -2
- package/src/types.ts +148 -11
- package/src/validate-established-topo.ts +2 -2
- package/src/validate-topo.ts +32 -8
- package/src/workspace.ts +1 -1
- package/tsconfig.tests.json +10 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/dist/adapters.d.ts +0 -39
- package/dist/adapters.d.ts.map +0 -1
- package/dist/adapters.js +0 -2
- package/dist/adapters.js.map +0 -1
- package/dist/dispatch.d.ts +0 -27
- package/dist/dispatch.d.ts.map +0 -1
- package/dist/dispatch.js +0 -34
- package/dist/dispatch.js.map +0 -1
- package/dist/event.d.ts +0 -8
- package/dist/event.d.ts.map +0 -1
- package/dist/event.js +0 -7
- package/dist/event.js.map +0 -1
- package/dist/gate.d.ts +0 -17
- package/dist/gate.d.ts.map +0 -1
- package/dist/gate.js +0 -21
- package/dist/gate.js.map +0 -1
- package/dist/health.d.ts +0 -18
- package/dist/health.d.ts.map +0 -1
- package/dist/health.js +0 -5
- package/dist/health.js.map +0 -1
- package/dist/job.d.ts +0 -24
- package/dist/job.d.ts.map +0 -1
- package/dist/job.js +0 -17
- package/dist/job.js.map +0 -1
- package/dist/provision-config.d.ts +0 -22
- package/dist/provision-config.d.ts.map +0 -1
- package/dist/provision-config.js +0 -210
- package/dist/provision-config.js.map +0 -1
- package/dist/provision.d.ts +0 -71
- package/dist/provision.d.ts.map +0 -1
- package/dist/provision.js +0 -56
- package/dist/provision.js.map +0 -1
- package/dist/service-config.d.ts +0 -22
- package/dist/service-config.d.ts.map +0 -1
- package/dist/service-config.js +0 -210
- package/dist/service-config.js.map +0 -1
- package/dist/service.d.ts +0 -71
- package/dist/service.d.ts.map +0 -1
- package/dist/service.js +0 -56
- package/dist/service.js.map +0 -1
- package/src/__tests__/service.test.ts +0 -197
- package/src/event.ts +0 -15
- package/src/gate.ts +0 -44
- package/src/internal/topo-saves.ts +0 -429
- package/src/provision.ts +0 -148
package/src/gate.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import type { Trail } from './trail.js';
|
|
2
|
-
import type { Implementation } from './types.js';
|
|
3
|
-
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
// Gate interface
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
|
|
8
|
-
/** A composable gate that wraps trail implementations. */
|
|
9
|
-
export interface Gate {
|
|
10
|
-
readonly name: string;
|
|
11
|
-
readonly description?: string | undefined;
|
|
12
|
-
|
|
13
|
-
/** Wrap a trail's implementation, returning a new implementation. */
|
|
14
|
-
wrap<I, O>(
|
|
15
|
-
trail: Trail<I, O>,
|
|
16
|
-
implementation: Implementation<I, O>
|
|
17
|
-
): Implementation<I, O>;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// ---------------------------------------------------------------------------
|
|
21
|
-
// Composition
|
|
22
|
-
// ---------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Apply gates outermost-first: gates[0] wraps gates[1] wraps ... wraps
|
|
26
|
-
* the base implementation.
|
|
27
|
-
*
|
|
28
|
-
* An empty gates array returns the implementation unchanged.
|
|
29
|
-
*/
|
|
30
|
-
export const composeGates = <I, O>(
|
|
31
|
-
gates: readonly Gate[],
|
|
32
|
-
trail: Trail<I, O>,
|
|
33
|
-
implementation: Implementation<I, O>
|
|
34
|
-
): Implementation<I, O> => {
|
|
35
|
-
// Fold right so gates[0] is the outermost wrapper.
|
|
36
|
-
let result = implementation;
|
|
37
|
-
for (let i = gates.length - 1; i >= 0; i -= 1) {
|
|
38
|
-
const gate = gates[i];
|
|
39
|
-
if (gate) {
|
|
40
|
-
result = gate.wrap(trail, result);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
return result;
|
|
44
|
-
};
|
|
@@ -1,429 +0,0 @@
|
|
|
1
|
-
import type { Database } 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_saves (
|
|
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
|
-
provision_count INTEGER NOT NULL DEFAULT 0,
|
|
14
|
-
created_at TEXT NOT NULL
|
|
15
|
-
)`,
|
|
16
|
-
`CREATE TABLE IF NOT EXISTS topo_pins (
|
|
17
|
-
name TEXT PRIMARY KEY,
|
|
18
|
-
save_id TEXT NOT NULL,
|
|
19
|
-
created_at TEXT NOT NULL,
|
|
20
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id)
|
|
21
|
-
)`,
|
|
22
|
-
`CREATE TABLE IF NOT EXISTS topo_trails (
|
|
23
|
-
id TEXT NOT NULL,
|
|
24
|
-
intent TEXT,
|
|
25
|
-
idempotent INTEGER NOT NULL DEFAULT 0,
|
|
26
|
-
has_output INTEGER NOT NULL DEFAULT 0,
|
|
27
|
-
has_examples INTEGER NOT NULL DEFAULT 0,
|
|
28
|
-
example_count INTEGER NOT NULL DEFAULT 0,
|
|
29
|
-
description TEXT,
|
|
30
|
-
meta TEXT,
|
|
31
|
-
save_id TEXT NOT NULL,
|
|
32
|
-
PRIMARY KEY (id, save_id),
|
|
33
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
34
|
-
)`,
|
|
35
|
-
`CREATE TABLE IF NOT EXISTS topo_crossings (
|
|
36
|
-
source_id TEXT NOT NULL,
|
|
37
|
-
target_id TEXT NOT NULL,
|
|
38
|
-
save_id TEXT NOT NULL,
|
|
39
|
-
PRIMARY KEY (source_id, target_id, save_id),
|
|
40
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
41
|
-
)`,
|
|
42
|
-
`CREATE TABLE IF NOT EXISTS topo_trail_provisions (
|
|
43
|
-
trail_id TEXT NOT NULL,
|
|
44
|
-
provision_id TEXT NOT NULL,
|
|
45
|
-
save_id TEXT NOT NULL,
|
|
46
|
-
PRIMARY KEY (trail_id, provision_id, save_id),
|
|
47
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
48
|
-
)`,
|
|
49
|
-
`CREATE TABLE IF NOT EXISTS topo_provisions (
|
|
50
|
-
id TEXT NOT NULL,
|
|
51
|
-
has_mock INTEGER NOT NULL DEFAULT 0,
|
|
52
|
-
has_health INTEGER NOT NULL DEFAULT 0,
|
|
53
|
-
save_id TEXT NOT NULL,
|
|
54
|
-
PRIMARY KEY (id, save_id),
|
|
55
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
56
|
-
)`,
|
|
57
|
-
`CREATE TABLE IF NOT EXISTS topo_signals (
|
|
58
|
-
id TEXT NOT NULL,
|
|
59
|
-
description TEXT,
|
|
60
|
-
save_id TEXT NOT NULL,
|
|
61
|
-
PRIMARY KEY (id, save_id),
|
|
62
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
63
|
-
)`,
|
|
64
|
-
`CREATE TABLE IF NOT EXISTS topo_trail_signals (
|
|
65
|
-
trail_id TEXT NOT NULL,
|
|
66
|
-
signal_id TEXT NOT NULL,
|
|
67
|
-
save_id TEXT NOT NULL,
|
|
68
|
-
PRIMARY KEY (trail_id, signal_id, save_id),
|
|
69
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
70
|
-
)`,
|
|
71
|
-
`CREATE TABLE IF NOT EXISTS topo_trailheads (
|
|
72
|
-
trail_id TEXT NOT NULL,
|
|
73
|
-
trailhead TEXT NOT NULL,
|
|
74
|
-
derived_name TEXT NOT NULL,
|
|
75
|
-
method TEXT,
|
|
76
|
-
save_id TEXT NOT NULL,
|
|
77
|
-
PRIMARY KEY (trail_id, trailhead, save_id),
|
|
78
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
79
|
-
)`,
|
|
80
|
-
`CREATE TABLE IF NOT EXISTS topo_examples (
|
|
81
|
-
id TEXT PRIMARY KEY,
|
|
82
|
-
trail_id TEXT NOT NULL,
|
|
83
|
-
ordinal INTEGER NOT NULL,
|
|
84
|
-
name TEXT NOT NULL,
|
|
85
|
-
description TEXT,
|
|
86
|
-
input TEXT NOT NULL,
|
|
87
|
-
expected TEXT,
|
|
88
|
-
error TEXT,
|
|
89
|
-
save_id TEXT NOT NULL,
|
|
90
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
91
|
-
)`,
|
|
92
|
-
`CREATE TABLE IF NOT EXISTS topo_schemas (
|
|
93
|
-
owner_id TEXT NOT NULL,
|
|
94
|
-
owner_kind TEXT NOT NULL,
|
|
95
|
-
schema_kind TEXT NOT NULL,
|
|
96
|
-
zod_hash TEXT NOT NULL,
|
|
97
|
-
json_schema TEXT NOT NULL,
|
|
98
|
-
save_id TEXT NOT NULL,
|
|
99
|
-
PRIMARY KEY (owner_id, owner_kind, schema_kind, save_id),
|
|
100
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
101
|
-
)`,
|
|
102
|
-
`CREATE TABLE IF NOT EXISTS topo_exports (
|
|
103
|
-
save_id TEXT PRIMARY KEY,
|
|
104
|
-
trailhead_map TEXT NOT NULL,
|
|
105
|
-
trailhead_hash TEXT NOT NULL,
|
|
106
|
-
serialized_lock TEXT NOT NULL,
|
|
107
|
-
FOREIGN KEY (save_id) REFERENCES topo_saves(id) ON DELETE CASCADE
|
|
108
|
-
)`,
|
|
109
|
-
] as const;
|
|
110
|
-
const TOPO_INDEX_STATEMENTS = [
|
|
111
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_saves_created_at ON topo_saves(created_at DESC)',
|
|
112
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_pins_save_id ON topo_pins(save_id)',
|
|
113
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_trails_save_id ON topo_trails(save_id)',
|
|
114
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_crossings_save_id ON topo_crossings(save_id)',
|
|
115
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_trail_provisions_save_id ON topo_trail_provisions(save_id)',
|
|
116
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_provisions_save_id ON topo_provisions(save_id)',
|
|
117
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_signals_save_id ON topo_signals(save_id)',
|
|
118
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_trail_signals_save_id ON topo_trail_signals(save_id)',
|
|
119
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_trailheads_save_id ON topo_trailheads(save_id)',
|
|
120
|
-
'CREATE UNIQUE INDEX IF NOT EXISTS idx_topo_examples_save_trail_ordinal ON topo_examples(save_id, trail_id, ordinal)',
|
|
121
|
-
'CREATE INDEX IF NOT EXISTS idx_topo_schemas_save_id ON topo_schemas(save_id)',
|
|
122
|
-
`CREATE INDEX IF NOT EXISTS idx_topo_schemas_lookup
|
|
123
|
-
ON topo_schemas(owner_id, owner_kind, schema_kind, zod_hash)`,
|
|
124
|
-
] as const;
|
|
125
|
-
|
|
126
|
-
interface TopoSaveRow {
|
|
127
|
-
readonly created_at: string;
|
|
128
|
-
readonly git_dirty: number;
|
|
129
|
-
readonly git_sha: string | null;
|
|
130
|
-
readonly id: string;
|
|
131
|
-
readonly provision_count: number;
|
|
132
|
-
readonly signal_count: number;
|
|
133
|
-
readonly trail_count: number;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
interface TopoPinRow {
|
|
137
|
-
readonly created_at: string;
|
|
138
|
-
readonly name: string;
|
|
139
|
-
readonly save_id: string;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
export interface TopoSaveRecord {
|
|
143
|
-
readonly createdAt: string;
|
|
144
|
-
readonly gitDirty: boolean;
|
|
145
|
-
readonly gitSha?: string;
|
|
146
|
-
readonly id: string;
|
|
147
|
-
readonly provisionCount: number;
|
|
148
|
-
readonly signalCount: number;
|
|
149
|
-
readonly trailCount: number;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export interface CreateTopoSaveInput {
|
|
153
|
-
readonly createdAt?: string;
|
|
154
|
-
readonly gitDirty?: boolean;
|
|
155
|
-
readonly gitSha?: string;
|
|
156
|
-
readonly id?: string;
|
|
157
|
-
readonly provisionCount?: number;
|
|
158
|
-
readonly signalCount?: number;
|
|
159
|
-
readonly trailCount?: number;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
export interface TopoPinRecord {
|
|
163
|
-
readonly createdAt: string;
|
|
164
|
-
readonly name: string;
|
|
165
|
-
readonly saveId: string;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
export interface CreateTopoPinInput {
|
|
169
|
-
readonly createdAt?: string;
|
|
170
|
-
readonly name: string;
|
|
171
|
-
readonly saveId: string;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const rowToSave = (row: TopoSaveRow): TopoSaveRecord => ({
|
|
175
|
-
createdAt: row.created_at,
|
|
176
|
-
gitDirty: row.git_dirty === 1,
|
|
177
|
-
id: row.id,
|
|
178
|
-
provisionCount: row.provision_count,
|
|
179
|
-
signalCount: row.signal_count,
|
|
180
|
-
trailCount: row.trail_count,
|
|
181
|
-
...(row.git_sha === null ? {} : { gitSha: row.git_sha }),
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
const rowToPin = (row: TopoPinRow): TopoPinRecord => ({
|
|
185
|
-
createdAt: row.created_at,
|
|
186
|
-
name: row.name,
|
|
187
|
-
saveId: row.save_id,
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const tableExists = (db: Database, tableName: string): boolean => {
|
|
191
|
-
const row = db
|
|
192
|
-
.query<{ name: string }, [string]>(
|
|
193
|
-
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?"
|
|
194
|
-
)
|
|
195
|
-
.get(tableName);
|
|
196
|
-
return row?.name === tableName;
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
const runStatements = (db: Database, statements: readonly string[]): void => {
|
|
200
|
-
for (const statement of statements) {
|
|
201
|
-
db.run(statement);
|
|
202
|
-
}
|
|
203
|
-
};
|
|
204
|
-
|
|
205
|
-
export const ensureTopoHistorySchema = (db: Database): void => {
|
|
206
|
-
ensureSubsystemSchema(db, {
|
|
207
|
-
migrate: (currentVersion) => {
|
|
208
|
-
if (currentVersion < 2) {
|
|
209
|
-
runStatements(db, TOPO_TABLE_STATEMENTS);
|
|
210
|
-
runStatements(db, TOPO_INDEX_STATEMENTS);
|
|
211
|
-
}
|
|
212
|
-
if (currentVersion === 2) {
|
|
213
|
-
// v2→v3: add schema cache and export tables
|
|
214
|
-
runStatements(db, TOPO_TABLE_STATEMENTS.slice(10));
|
|
215
|
-
runStatements(db, TOPO_INDEX_STATEMENTS.slice(10));
|
|
216
|
-
}
|
|
217
|
-
},
|
|
218
|
-
subsystem: TOPO_SUBSYSTEM,
|
|
219
|
-
version: 3,
|
|
220
|
-
});
|
|
221
|
-
};
|
|
222
|
-
|
|
223
|
-
export const insertTopoSaveRecord = (
|
|
224
|
-
db: Database,
|
|
225
|
-
input?: CreateTopoSaveInput
|
|
226
|
-
): TopoSaveRecord => {
|
|
227
|
-
const record: TopoSaveRecord = {
|
|
228
|
-
createdAt: input?.createdAt ?? new Date().toISOString(),
|
|
229
|
-
gitDirty: input?.gitDirty ?? false,
|
|
230
|
-
id: input?.id ?? Bun.randomUUIDv7(),
|
|
231
|
-
provisionCount: input?.provisionCount ?? 0,
|
|
232
|
-
signalCount: input?.signalCount ?? 0,
|
|
233
|
-
trailCount: input?.trailCount ?? 0,
|
|
234
|
-
...(input?.gitSha === undefined ? {} : { gitSha: input.gitSha }),
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
db.run(
|
|
238
|
-
`INSERT INTO topo_saves (
|
|
239
|
-
id, git_sha, git_dirty, trail_count, signal_count, provision_count, created_at
|
|
240
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
241
|
-
[
|
|
242
|
-
record.id,
|
|
243
|
-
record.gitSha ?? null,
|
|
244
|
-
record.gitDirty ? 1 : 0,
|
|
245
|
-
record.trailCount,
|
|
246
|
-
record.signalCount,
|
|
247
|
-
record.provisionCount,
|
|
248
|
-
record.createdAt,
|
|
249
|
-
]
|
|
250
|
-
);
|
|
251
|
-
|
|
252
|
-
return record;
|
|
253
|
-
};
|
|
254
|
-
|
|
255
|
-
export const createTopoSave = (
|
|
256
|
-
db: Database,
|
|
257
|
-
input?: CreateTopoSaveInput
|
|
258
|
-
): TopoSaveRecord => {
|
|
259
|
-
ensureTopoHistorySchema(db);
|
|
260
|
-
return insertTopoSaveRecord(db, input);
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
export const pinTopoSave = (
|
|
264
|
-
db: Database,
|
|
265
|
-
input: CreateTopoPinInput
|
|
266
|
-
): TopoPinRecord => {
|
|
267
|
-
ensureTopoHistorySchema(db);
|
|
268
|
-
|
|
269
|
-
const record: TopoPinRecord = {
|
|
270
|
-
createdAt: input.createdAt ?? new Date().toISOString(),
|
|
271
|
-
name: input.name,
|
|
272
|
-
saveId: input.saveId,
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
db.run(
|
|
276
|
-
`INSERT INTO topo_pins (name, save_id, created_at) VALUES (?, ?, ?)
|
|
277
|
-
ON CONFLICT(name) DO UPDATE SET
|
|
278
|
-
save_id = excluded.save_id,
|
|
279
|
-
created_at = excluded.created_at`,
|
|
280
|
-
[record.name, record.saveId, record.createdAt]
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
return record;
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
export const listTopoSaves = (db: Database): readonly TopoSaveRecord[] => {
|
|
287
|
-
if (!tableExists(db, 'topo_saves')) {
|
|
288
|
-
return [];
|
|
289
|
-
}
|
|
290
|
-
const rows = db
|
|
291
|
-
.query<TopoSaveRow, []>(
|
|
292
|
-
`SELECT id, git_sha, git_dirty, trail_count, signal_count, provision_count, created_at
|
|
293
|
-
FROM topo_saves
|
|
294
|
-
ORDER BY created_at DESC, id DESC`
|
|
295
|
-
)
|
|
296
|
-
.all();
|
|
297
|
-
return rows.map(rowToSave);
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
export const listTopoPins = (db: Database): readonly TopoPinRecord[] => {
|
|
301
|
-
if (!tableExists(db, 'topo_pins')) {
|
|
302
|
-
return [];
|
|
303
|
-
}
|
|
304
|
-
const rows = db
|
|
305
|
-
.query<TopoPinRow, []>(
|
|
306
|
-
`SELECT name, save_id, created_at
|
|
307
|
-
FROM topo_pins
|
|
308
|
-
ORDER BY created_at ASC, name ASC`
|
|
309
|
-
)
|
|
310
|
-
.all();
|
|
311
|
-
return rows.map(rowToPin);
|
|
312
|
-
};
|
|
313
|
-
|
|
314
|
-
const countSaves = (db: Database): number => {
|
|
315
|
-
const row = db
|
|
316
|
-
.query<{ count: number }, []>('SELECT COUNT(*) as count FROM topo_saves')
|
|
317
|
-
.get();
|
|
318
|
-
return row?.count ?? 0;
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
export const countTopoSaves = (db: Database): number => {
|
|
322
|
-
if (!tableExists(db, 'topo_saves')) {
|
|
323
|
-
return 0;
|
|
324
|
-
}
|
|
325
|
-
return countSaves(db);
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
export const countTopoPins = (db: Database): number => {
|
|
329
|
-
if (!tableExists(db, 'topo_pins')) {
|
|
330
|
-
return 0;
|
|
331
|
-
}
|
|
332
|
-
const row = db
|
|
333
|
-
.query<{ count: number }, []>('SELECT COUNT(*) as count FROM topo_pins')
|
|
334
|
-
.get();
|
|
335
|
-
return row?.count ?? 0;
|
|
336
|
-
};
|
|
337
|
-
|
|
338
|
-
export const getTopoSave = (
|
|
339
|
-
db: Database,
|
|
340
|
-
id: string
|
|
341
|
-
): TopoSaveRecord | undefined => {
|
|
342
|
-
if (!tableExists(db, 'topo_saves')) {
|
|
343
|
-
return undefined;
|
|
344
|
-
}
|
|
345
|
-
const row = db
|
|
346
|
-
.query<TopoSaveRow, [string]>(
|
|
347
|
-
`SELECT id, git_sha, git_dirty, trail_count, signal_count, provision_count, created_at
|
|
348
|
-
FROM topo_saves
|
|
349
|
-
WHERE id = ?`
|
|
350
|
-
)
|
|
351
|
-
.get(id);
|
|
352
|
-
return row === null || row === undefined ? undefined : rowToSave(row);
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
export const getTopoPin = (
|
|
356
|
-
db: Database,
|
|
357
|
-
name: string
|
|
358
|
-
): TopoPinRecord | undefined => {
|
|
359
|
-
if (!tableExists(db, 'topo_pins')) {
|
|
360
|
-
return undefined;
|
|
361
|
-
}
|
|
362
|
-
const row = db
|
|
363
|
-
.query<TopoPinRow, [string]>(
|
|
364
|
-
`SELECT name, save_id, created_at
|
|
365
|
-
FROM topo_pins
|
|
366
|
-
WHERE name = ?`
|
|
367
|
-
)
|
|
368
|
-
.get(name);
|
|
369
|
-
return row === null || row === undefined ? undefined : rowToPin(row);
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
export const countPrunableTopoSaves = (
|
|
373
|
-
db: Database,
|
|
374
|
-
options: { readonly keep: number }
|
|
375
|
-
): number => {
|
|
376
|
-
if (!tableExists(db, 'topo_saves') || !tableExists(db, 'topo_pins')) {
|
|
377
|
-
return 0;
|
|
378
|
-
}
|
|
379
|
-
const row = db
|
|
380
|
-
.query<{ count: number }, [number]>(
|
|
381
|
-
`SELECT COUNT(*) as count
|
|
382
|
-
FROM (
|
|
383
|
-
SELECT save.id
|
|
384
|
-
FROM topo_saves save
|
|
385
|
-
LEFT JOIN topo_pins pin ON pin.save_id = save.id
|
|
386
|
-
WHERE pin.save_id IS NULL
|
|
387
|
-
ORDER BY save.created_at DESC, save.id DESC
|
|
388
|
-
LIMIT -1 OFFSET ?
|
|
389
|
-
)`
|
|
390
|
-
)
|
|
391
|
-
.get(options.keep);
|
|
392
|
-
return row?.count ?? 0;
|
|
393
|
-
};
|
|
394
|
-
|
|
395
|
-
export const pruneUnpinnedTopoSaves = (
|
|
396
|
-
db: Database,
|
|
397
|
-
options: { readonly keep: number }
|
|
398
|
-
): number => {
|
|
399
|
-
if (!tableExists(db, 'topo_saves') || !tableExists(db, 'topo_pins')) {
|
|
400
|
-
return 0;
|
|
401
|
-
}
|
|
402
|
-
if (countPrunableTopoSaves(db, options) === 0) {
|
|
403
|
-
return 0;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
db.run(
|
|
407
|
-
`DELETE FROM topo_saves
|
|
408
|
-
WHERE id IN (
|
|
409
|
-
SELECT save.id
|
|
410
|
-
FROM topo_saves save
|
|
411
|
-
LEFT JOIN topo_pins pin ON pin.save_id = save.id
|
|
412
|
-
WHERE pin.save_id IS NULL
|
|
413
|
-
ORDER BY save.created_at DESC, save.id DESC
|
|
414
|
-
LIMIT -1 OFFSET ?
|
|
415
|
-
)`,
|
|
416
|
-
[options.keep]
|
|
417
|
-
);
|
|
418
|
-
|
|
419
|
-
return (
|
|
420
|
-
db.query<{ changes: number }, []>('SELECT changes() as changes').get()
|
|
421
|
-
?.changes ?? 0
|
|
422
|
-
);
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
export const unpinTopoSave = (db: Database, name: string): boolean => {
|
|
426
|
-
ensureTopoHistorySchema(db);
|
|
427
|
-
const result = db.run('DELETE FROM topo_pins WHERE name = ?', [name]);
|
|
428
|
-
return result.changes > 0;
|
|
429
|
-
};
|
package/src/provision.ts
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
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
|
-
};
|