@velum-labs/routekit-eval-store 1.2.0 → 1.3.1

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.
@@ -20,9 +20,10 @@ export declare class RoutingActivationStore {
20
20
  read(): Effect.Effect<PublishedRoutingActivationType | undefined, Error, FileSystem.FileSystem | Path.Path>;
21
21
  readPrevious(): Effect.Effect<PublishedRoutingActivationType | undefined, Error, FileSystem.FileSystem | Path.Path>;
22
22
  readDeployment(): Effect.Effect<RoutingDeploymentStateV1Type, Error, FileSystem.FileSystem | Path.Path>;
23
+ readActivation(activationPath: string): Effect.Effect<AnyPublishedRoutingActivationType, Error, FileSystem.FileSystem | Path.Path>;
23
24
  publish(publication: RoutingActivationPublication): Effect.Effect<PublishedRoutingActivationType, Error, FileSystem.FileSystem | Path.Path>;
24
25
  publishIfCurrent(publication: RoutingActivationPublication, expectedEvidenceDigest: string | undefined): Effect.Effect<PublishedRoutingActivationType, Error, FileSystem.FileSystem | Path.Path>;
25
- installAuthoritative(activation: AnyPublishedRoutingActivationType, expectedAuthoritativeRevisionDigest: string | null, expectedPreviousAuthoritativeRevisionDigest: string | null): Effect.Effect<RoutingDeploymentStateV1Type, Error, FileSystem.FileSystem | Path.Path>;
26
+ installAuthoritative(activation: AnyPublishedRoutingActivationType, activationPath: string, expectedAuthoritativeRevisionDigest: string | null, expectedPreviousAuthoritativeRevisionDigest: string | null): Effect.Effect<RoutingDeploymentStateV1Type, Error, FileSystem.FileSystem | Path.Path>;
26
27
  rollbackAuthoritative(expectedAuthoritativeRevisionDigest: string, expectedPreviousAuthoritativeRevisionDigest: string): Effect.Effect<RoutingDeploymentStateV1Type, Error, FileSystem.FileSystem | Path.Path>;
27
28
  }
28
29
  export declare function makeRoutingActivationStore(root: string): RoutingActivationStore;
@@ -1,11 +1,23 @@
1
- import { activationRevisionDigest, assertPublishedRoutingActivation, assertPublishedRoutingActivationV3, assertRoutingDeploymentStateV1, COMPOSITIONAL_ROUTING_VERSION, PublishedRoutingActivation, RoutingDeploymentStateV1, routingActivationRevision } from "@velum-labs/routekit-eval-contracts";
1
+ import { activationRevisionDigest, AnyPublishedRoutingActivation, assertPublishedRoutingActivation, assertPublishedRoutingActivationV3, assertRoutingDeploymentStateV1, COMPOSITIONAL_ROUTING_VERSION, PublishedRoutingActivation, RoutingDeploymentStateV1, routingActivationRevision } from "@velum-labs/routekit-eval-contracts";
2
2
  import { RouteKitFailure, writeFileAtomicEffect } from "@velum-labs/routekit-runtime/effect";
3
3
  import { Clock, Effect, FileSystem, Path, Schema } from "effect";
4
4
  const SNAPSHOT_FILE = "published-routing.json";
5
5
  const PREVIOUS_SNAPSHOT_FILE = "published-routing.previous.json";
6
6
  const DEPLOYMENT_FILE = "routing-deployment.v1.json";
7
+ const POINTER_DEPLOYMENT_FILE = "routing-deployment.v2.json";
7
8
  export const ROUTING_ACTIVATION_MAX_BYTES = 2 * 1024 * 1024;
8
9
  const publicationTails = new Map();
10
+ const RoutingActivationReference = Schema.Struct({
11
+ activationRevisionDigest: Schema.String,
12
+ activationPath: Schema.String
13
+ });
14
+ const RoutingPointerDeployment = Schema.Struct({
15
+ version: Schema.Literal(2),
16
+ stateRevision: Schema.Finite,
17
+ authoritative: Schema.NullOr(RoutingActivationReference),
18
+ previousAuthoritative: Schema.NullOr(RoutingActivationReference),
19
+ updatedAt: Schema.String
20
+ });
9
21
  export class RoutingActivationConflictError extends Error {
10
22
  expectedEvidenceDigest;
11
23
  actualEvidenceDigest;
@@ -54,7 +66,7 @@ export class RoutingActivationStore {
54
66
  read() {
55
67
  const root = this.root;
56
68
  return Effect.gen(function* () {
57
- const deployment = yield* readOrMigrateDeployment(root);
69
+ const deployment = yield* readResolvedDeployment(root);
58
70
  const activation = deployment.authoritative?.activation;
59
71
  return activation?.version === 2 ? activation : undefined;
60
72
  });
@@ -63,14 +75,17 @@ export class RoutingActivationStore {
63
75
  const root = this.root;
64
76
  return Effect.gen(function* () {
65
77
  const paths = yield* Path.Path;
66
- return yield* readOrMigrateDeployment(root).pipe(Effect.map((deployment) => {
78
+ return yield* readResolvedDeployment(root).pipe(Effect.map((deployment) => {
67
79
  const activation = deployment.previousAuthoritative?.activation;
68
80
  return activation?.version === 2 ? activation : undefined;
69
81
  }), Effect.catch(() => readLegacyActivation(paths.join(root, PREVIOUS_SNAPSHOT_FILE))));
70
82
  });
71
83
  }
72
84
  readDeployment() {
73
- return readOrMigrateDeployment(this.root);
85
+ return readResolvedDeployment(this.root);
86
+ }
87
+ readActivation(activationPath) {
88
+ return readPointedActivation(activationPath);
74
89
  }
75
90
  publish(publication) {
76
91
  return this.#withPublicationLock(this.#publishLegacy(publication));
@@ -78,6 +93,10 @@ export class RoutingActivationStore {
78
93
  publishIfCurrent(publication, expectedEvidenceDigest) {
79
94
  const store = this;
80
95
  return this.#withPublicationLock(Effect.gen(function* () {
96
+ if ((yield* readPointerDeployment(store.root)) !== undefined)
97
+ return yield* new RouteKitFailure({
98
+ message: "legacy routing publication cannot replace a pointer-backed deployment"
99
+ });
81
100
  const current = yield* readOrMigrateDeployment(store.root);
82
101
  const activation = current.authoritative?.activation;
83
102
  const actual = activation?.version === 2 ? activation.evidenceDigest : undefined;
@@ -86,52 +105,90 @@ export class RoutingActivationStore {
86
105
  return yield* store.#publishLegacy(publication);
87
106
  }));
88
107
  }
89
- installAuthoritative(activation, expectedAuthoritativeRevisionDigest, expectedPreviousAuthoritativeRevisionDigest) {
108
+ installAuthoritative(activation, activationPath, expectedAuthoritativeRevisionDigest, expectedPreviousAuthoritativeRevisionDigest) {
90
109
  const store = this;
91
110
  return this.#withPublicationLock(Effect.gen(function* () {
92
111
  yield* validateAnyActivation(activation, "routing activation is invalid");
93
- const current = yield* readOrMigrateDeployment(store.root);
94
- assertSlot("authoritative", expectedAuthoritativeRevisionDigest, current.authoritative?.activationRevisionDigest ?? null);
95
- assertSlot("previousAuthoritative", expectedPreviousAuthoritativeRevisionDigest, current.previousAuthoritative?.activationRevisionDigest ?? null);
96
- if (current.authoritative === null && current.previousAuthoritative !== null)
112
+ const currentPointers = yield* readPointerDeployment(store.root);
113
+ const current = currentPointers === undefined
114
+ ? yield* readOrMigrateDeployment(store.root)
115
+ : undefined;
116
+ assertSlot("authoritative", expectedAuthoritativeRevisionDigest, currentPointers?.authoritative?.activationRevisionDigest ??
117
+ current?.authoritative?.activationRevisionDigest ??
118
+ null);
119
+ assertSlot("previousAuthoritative", expectedPreviousAuthoritativeRevisionDigest, currentPointers?.previousAuthoritative?.activationRevisionDigest ??
120
+ current?.previousAuthoritative?.activationRevisionDigest ??
121
+ null);
122
+ const currentAuthoritative = currentPointers === undefined
123
+ ? current?.authoritative ?? null
124
+ : yield* resolvePointerRevision(currentPointers.authoritative);
125
+ if (currentAuthoritative === null &&
126
+ (currentPointers?.previousAuthoritative ?? current?.previousAuthoritative ?? null) !== null)
97
127
  return yield* new RouteKitFailure({
98
128
  message: "cannot install authority into an empty slot while a previous authoritative revision exists"
99
129
  });
130
+ const activationReference = yield* referenceForActivation(activationPath, activation);
131
+ const authoritativeReference = (currentAuthoritative === null
132
+ ? null
133
+ : yield* materializeDisplacedReference(activationReference.activationPath, currentAuthoritative.activation));
100
134
  const state = {
101
- version: 1,
102
- stateRevision: current.stateRevision + 1,
103
- authoritative: routingActivationRevision(activation),
104
- previousAuthoritative: current.authoritative,
135
+ version: 2,
136
+ stateRevision: (currentPointers?.stateRevision ?? current?.stateRevision ?? 0) + 1,
137
+ authoritative: activationReference,
138
+ previousAuthoritative: authoritativeReference,
105
139
  updatedAt: new Date(yield* Clock.currentTimeMillis).toISOString()
106
140
  };
107
- yield* writeDeployment(store.root, state);
108
- return state;
141
+ yield* writePointerDeployment(store.root, state);
142
+ yield* removeLegacyDeploymentBodies(store.root);
143
+ return yield* resolvePointerDeployment(state);
109
144
  }));
110
145
  }
111
146
  rollbackAuthoritative(expectedAuthoritativeRevisionDigest, expectedPreviousAuthoritativeRevisionDigest) {
112
147
  const store = this;
113
148
  return this.#withPublicationLock(Effect.gen(function* () {
114
- const current = yield* readOrMigrateDeployment(store.root);
149
+ const currentPointers = yield* readPointerDeployment(store.root);
150
+ const current = currentPointers === undefined
151
+ ? yield* readOrMigrateDeployment(store.root)
152
+ : yield* resolvePointerDeployment(currentPointers);
115
153
  assertSlot("authoritative", expectedAuthoritativeRevisionDigest, current.authoritative?.activationRevisionDigest ?? null);
116
154
  assertSlot("previousAuthoritative", expectedPreviousAuthoritativeRevisionDigest, current.previousAuthoritative?.activationRevisionDigest ?? null);
117
155
  if (current.authoritative === null || current.previousAuthoritative === null)
118
156
  return yield* new RouteKitFailure({
119
157
  message: "routing rollback requires both authoritative slots"
120
158
  });
121
- const state = {
122
- version: 1,
159
+ if (currentPointers === undefined) {
160
+ const state = {
161
+ version: 1,
162
+ stateRevision: current.stateRevision + 1,
163
+ authoritative: current.previousAuthoritative,
164
+ previousAuthoritative: current.authoritative,
165
+ updatedAt: new Date(yield* Clock.currentTimeMillis).toISOString()
166
+ };
167
+ yield* writeDeployment(store.root, state);
168
+ return state;
169
+ }
170
+ if (currentPointers.authoritative === null || currentPointers.previousAuthoritative === null)
171
+ return yield* new RouteKitFailure({
172
+ message: "routing rollback requires both pointer-backed authoritative slots"
173
+ });
174
+ const pointerState = {
175
+ version: 2,
123
176
  stateRevision: current.stateRevision + 1,
124
- authoritative: current.previousAuthoritative,
125
- previousAuthoritative: current.authoritative,
177
+ authoritative: currentPointers.previousAuthoritative,
178
+ previousAuthoritative: currentPointers.authoritative,
126
179
  updatedAt: new Date(yield* Clock.currentTimeMillis).toISOString()
127
180
  };
128
- yield* writeDeployment(store.root, state);
129
- return state;
181
+ yield* writePointerDeployment(store.root, pointerState);
182
+ return yield* resolvePointerDeployment(pointerState);
130
183
  }));
131
184
  }
132
185
  #publishLegacy(publication) {
133
186
  const store = this;
134
187
  return Effect.gen(function* () {
188
+ if ((yield* readPointerDeployment(store.root)) !== undefined)
189
+ return yield* new RouteKitFailure({
190
+ message: "legacy routing publication cannot replace a pointer-backed deployment"
191
+ });
135
192
  const snapshot = {
136
193
  version: COMPOSITIONAL_ROUTING_VERSION,
137
194
  generatedAt: new Date(yield* Clock.currentTimeMillis).toISOString(),
@@ -155,6 +212,175 @@ export class RoutingActivationStore {
155
212
  });
156
213
  }
157
214
  }
215
+ function referenceForActivation(activationPath, expected) {
216
+ return Effect.gen(function* () {
217
+ const fs = yield* FileSystem.FileSystem;
218
+ const paths = yield* Path.Path;
219
+ if (!paths.isAbsolute(activationPath))
220
+ return yield* new RouteKitFailure({
221
+ message: "routing activation pointer must be an absolute path"
222
+ });
223
+ const canonicalPath = yield* fs.realPath(activationPath).pipe(Effect.mapError((cause) => new RouteKitFailure({
224
+ message: `failed to resolve routing activation pointer: ${detailOf(cause)}`
225
+ })));
226
+ const activation = yield* readAnyActivation(canonicalPath, "routing activation pointer");
227
+ const expectedDigest = activationRevisionDigest(expected);
228
+ const actualDigest = activationRevisionDigest(activation);
229
+ if (actualDigest !== expectedDigest)
230
+ return yield* new RouteKitFailure({
231
+ message: "routing activation pointer does not match the validated activation"
232
+ });
233
+ return {
234
+ activationRevisionDigest: expectedDigest,
235
+ activationPath: canonicalPath
236
+ };
237
+ });
238
+ }
239
+ function readPointedActivation(activationPath) {
240
+ return Effect.gen(function* () {
241
+ const fs = yield* FileSystem.FileSystem;
242
+ const paths = yield* Path.Path;
243
+ if (!paths.isAbsolute(activationPath))
244
+ return yield* new RouteKitFailure({
245
+ message: "routing activation pointer must be an absolute path"
246
+ });
247
+ const canonicalPath = yield* fs.realPath(activationPath).pipe(Effect.mapError((cause) => new RouteKitFailure({
248
+ message: `failed to resolve routing activation pointer: ${detailOf(cause)}`
249
+ })));
250
+ return yield* readAnyActivation(canonicalPath, "routing activation pointer");
251
+ });
252
+ }
253
+ function materializeDisplacedReference(activationPath, activation) {
254
+ return Effect.gen(function* () {
255
+ const paths = yield* Path.Path;
256
+ if (!paths.isAbsolute(activationPath))
257
+ return yield* new RouteKitFailure({
258
+ message: "routing activation pointer must be an absolute path"
259
+ });
260
+ const revisionDigest = activationRevisionDigest(activation);
261
+ const destination = paths.join(paths.dirname(activationPath), `${revisionDigest}.json`);
262
+ const serialized = `${JSON.stringify(activation, null, 2)}\n`;
263
+ assertBoundedSnapshot(serialized);
264
+ yield* writeFileAtomicEffect(destination, serialized, { mode: 0o600 });
265
+ return {
266
+ activationRevisionDigest: revisionDigest,
267
+ activationPath: destination
268
+ };
269
+ });
270
+ }
271
+ function resolvePointerDeployment(state) {
272
+ return Effect.gen(function* () {
273
+ const authoritative = yield* resolvePointerRevision(state.authoritative);
274
+ const previousAuthoritative = yield* resolvePointerRevision(state.previousAuthoritative);
275
+ const resolved = {
276
+ version: 1,
277
+ stateRevision: state.stateRevision,
278
+ authoritative,
279
+ previousAuthoritative,
280
+ updatedAt: state.updatedAt
281
+ };
282
+ yield* Effect.try({
283
+ try: () => assertRoutingDeploymentStateV1(resolved),
284
+ catch: (cause) => new RouteKitFailure({
285
+ message: `routing pointer deployment is invalid: ${detailOf(cause)}`
286
+ })
287
+ });
288
+ return resolved;
289
+ });
290
+ }
291
+ function resolvePointerRevision(reference) {
292
+ return reference === null
293
+ ? Effect.succeed(null)
294
+ : readAnyActivation(reference.activationPath, "routing activation pointer").pipe(Effect.flatMap((activation) => {
295
+ const actual = activationRevisionDigest(activation);
296
+ return actual === reference.activationRevisionDigest
297
+ ? Effect.succeed({
298
+ activationRevisionDigest: actual,
299
+ activation
300
+ })
301
+ : Effect.fail(new RouteKitFailure({
302
+ message: "routing activation pointer digest does not match its definition"
303
+ }));
304
+ }));
305
+ }
306
+ function readPointerDeployment(root) {
307
+ return Effect.gen(function* () {
308
+ const paths = yield* Path.Path;
309
+ const path = paths.join(root, POINTER_DEPLOYMENT_FILE);
310
+ const json = yield* readJsonDocument(path, "routing pointer deployment is corrupt");
311
+ if (json === undefined)
312
+ return undefined;
313
+ const decoded = yield* Schema.decodeUnknownEffect(RoutingPointerDeployment)(json).pipe(Effect.mapError((cause) => new RouteKitFailure({
314
+ message: `routing pointer deployment is corrupt: ${String(cause)}`
315
+ })));
316
+ if (!Number.isSafeInteger(decoded.stateRevision) ||
317
+ decoded.stateRevision < 0 ||
318
+ !validActivationReference(decoded.authoritative, paths) ||
319
+ !validActivationReference(decoded.previousAuthoritative, paths) ||
320
+ (decoded.authoritative !== null &&
321
+ decoded.authoritative.activationRevisionDigest ===
322
+ decoded.previousAuthoritative?.activationRevisionDigest))
323
+ return yield* new RouteKitFailure({
324
+ message: "routing pointer deployment is invalid"
325
+ });
326
+ return decoded;
327
+ });
328
+ }
329
+ function validActivationReference(reference, paths) {
330
+ return (reference === null ||
331
+ (reference.activationRevisionDigest.length > 0 &&
332
+ reference.activationRevisionDigest === reference.activationRevisionDigest.trim() &&
333
+ paths.isAbsolute(reference.activationPath)));
334
+ }
335
+ function writePointerDeployment(root, state) {
336
+ return Effect.gen(function* () {
337
+ const decoded = yield* Schema.decodeEffect(RoutingPointerDeployment)(state).pipe(Effect.mapError((cause) => new RouteKitFailure({
338
+ message: `routing pointer deployment is invalid: ${String(cause)}`
339
+ })));
340
+ const paths = yield* Path.Path;
341
+ if (!Number.isSafeInteger(decoded.stateRevision) ||
342
+ decoded.stateRevision < 0 ||
343
+ !validActivationReference(decoded.authoritative, paths) ||
344
+ !validActivationReference(decoded.previousAuthoritative, paths) ||
345
+ (decoded.authoritative !== null &&
346
+ decoded.authoritative.activationRevisionDigest ===
347
+ decoded.previousAuthoritative?.activationRevisionDigest))
348
+ return yield* new RouteKitFailure({
349
+ message: "routing pointer deployment is invalid"
350
+ });
351
+ const serialized = `${JSON.stringify(decoded, null, 2)}\n`;
352
+ assertBoundedSnapshot(serialized);
353
+ const fs = yield* FileSystem.FileSystem;
354
+ yield* fs.makeDirectory(root, { recursive: true, mode: 0o700 });
355
+ yield* fs.chmod(root, 0o700).pipe(Effect.ignore);
356
+ yield* writeFileAtomicEffect(paths.join(root, POINTER_DEPLOYMENT_FILE), serialized, {
357
+ mode: 0o600
358
+ });
359
+ });
360
+ }
361
+ function removeLegacyDeploymentBodies(root) {
362
+ return Effect.gen(function* () {
363
+ const fs = yield* FileSystem.FileSystem;
364
+ const paths = yield* Path.Path;
365
+ for (const file of [DEPLOYMENT_FILE, SNAPSHOT_FILE, PREVIOUS_SNAPSHOT_FILE]) {
366
+ yield* fs.remove(paths.join(root, file), { force: true }).pipe(Effect.ignore);
367
+ }
368
+ });
369
+ }
370
+ function readAnyActivation(path, prefix) {
371
+ return Effect.gen(function* () {
372
+ const json = yield* readJsonDocument(path, `${prefix} is corrupt`);
373
+ if (json === undefined)
374
+ return yield* new RouteKitFailure({
375
+ message: `${prefix} does not exist: ${path}`
376
+ });
377
+ const decoded = yield* Schema.decodeUnknownEffect(AnyPublishedRoutingActivation)(json).pipe(Effect.mapError((cause) => new RouteKitFailure({
378
+ message: `${prefix} has the wrong shape: ${String(cause)}`
379
+ })));
380
+ yield* validateAnyActivation(decoded, `${prefix} is invalid`);
381
+ return decoded;
382
+ });
383
+ }
158
384
  function writeLegacyCompatibilitySnapshots(root, current, displaced) {
159
385
  return Effect.gen(function* () {
160
386
  const fs = yield* FileSystem.FileSystem;
@@ -207,6 +433,14 @@ function emptyDeployment(updatedAt) {
207
433
  updatedAt
208
434
  };
209
435
  }
436
+ function readResolvedDeployment(root) {
437
+ return Effect.gen(function* () {
438
+ const pointer = yield* readPointerDeployment(root);
439
+ return pointer === undefined
440
+ ? yield* readOrMigrateDeployment(root)
441
+ : yield* resolvePointerDeployment(pointer);
442
+ });
443
+ }
210
444
  function readOrMigrateDeployment(root) {
211
445
  return Effect.gen(function* () {
212
446
  const paths = yield* Path.Path;
@@ -1,5 +1,5 @@
1
1
  import assert from "node:assert/strict";
2
- import { chmodSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
2
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { test } from "node:test";
@@ -159,17 +159,38 @@ test("legacy V2 files migrate idempotently into cross-version deployment state",
159
159
  rmSync(root, { recursive: true, force: true });
160
160
  }
161
161
  });
162
+ test("legacy deployments roll back before pointer migration", async () => {
163
+ const root = mkdtempSync(join(tmpdir(), "routekit-routing-legacy-rollback-"));
164
+ try {
165
+ const store = makeRoutingActivationStore(root);
166
+ const first = await runRouteKitEffect(store.publish(publication("legacy-first")));
167
+ const second = await runRouteKitEffect(store.publish(publication("legacy-second")));
168
+ const rolledBack = await runRouteKitEffect(store.rollbackAuthoritative(activationRevisionDigest(second), activationRevisionDigest(first)));
169
+ assert.equal(rolledBack.authoritative?.activationRevisionDigest, activationRevisionDigest(first));
170
+ assert.equal(rolledBack.previousAuthoritative?.activationRevisionDigest, activationRevisionDigest(second));
171
+ assert.equal(statSync(join(root, "routing-deployment.v1.json")).mode & 0o777, 0o600);
172
+ }
173
+ finally {
174
+ rmSync(root, { recursive: true, force: true });
175
+ }
176
+ });
162
177
  test("V3 authority installation and rollback compare and swap both exact slots", async () => {
163
178
  const root = mkdtempSync(join(tmpdir(), "routekit-routing-deployment-rollback-"));
179
+ const repositoryRoot = mkdtempSync(join(tmpdir(), "routekit-routing-repository-"));
164
180
  try {
165
181
  const store = makeRoutingActivationStore(root);
166
182
  const v2 = await runRouteKitEffect(store.publish(publication("v2")));
167
183
  const v2Revision = activationRevisionDigest(v2);
168
184
  const v3 = JSON.parse(readFileSync(new URL("../../../../test/fixtures/routing-v3/examples/published-routing-activation-v3.example.json", import.meta.url), "utf8"));
169
- const installed = await runRouteKitEffect(store.installAuthoritative(v3, v2Revision, null));
185
+ const activationPath = join(repositoryRoot, ".routekit", "routing", "activations", `${v3.activationDigest}.json`);
186
+ mkdirSync(join(repositoryRoot, ".routekit", "routing", "activations"), {
187
+ recursive: true
188
+ });
189
+ writeFileSync(activationPath, `${JSON.stringify(v3)}\n`);
190
+ const installed = await runRouteKitEffect(store.installAuthoritative(v3, activationPath, v2Revision, null));
170
191
  assert.equal(installed.authoritative?.activation.version, 3);
171
192
  assert.equal(installed.previousAuthoritative?.activationRevisionDigest, v2Revision);
172
- await assert.rejects(runRouteKitEffect(store.installAuthoritative(v3, v2Revision, null)), (error) => error instanceof RoutingDeploymentConflictError && error.slot === "authoritative");
193
+ await assert.rejects(runRouteKitEffect(store.installAuthoritative(v3, activationPath, v2Revision, null)), (error) => error instanceof RoutingDeploymentConflictError && error.slot === "authoritative");
173
194
  const rolledBack = await runRouteKitEffect(store.rollbackAuthoritative(v3.activationDigest, v2Revision));
174
195
  assert.equal(rolledBack.authoritative?.activation.version, 2);
175
196
  assert.equal(rolledBack.previousAuthoritative?.activation.version, 3);
@@ -177,5 +198,62 @@ test("V3 authority installation and rollback compare and swap both exact slots",
177
198
  finally {
178
199
  chmodSync(root, 0o700);
179
200
  rmSync(root, { recursive: true, force: true });
201
+ rmSync(repositoryRoot, { recursive: true, force: true });
202
+ }
203
+ });
204
+ test("pointer deployments keep definitions repo-local across repositories", async () => {
205
+ const root = mkdtempSync(join(tmpdir(), "routekit-routing-pointer-"));
206
+ const repositoryA = mkdtempSync(join(tmpdir(), "routekit-routing-repo-a-"));
207
+ const repositoryB = mkdtempSync(join(tmpdir(), "routekit-routing-repo-b-"));
208
+ const activation = (repositoryRoot, evidenceDigest) => {
209
+ const definition = {
210
+ version: 2,
211
+ generatedAt: "2026-08-24T00:00:00.000Z",
212
+ ...publication(evidenceDigest)
213
+ };
214
+ const path = join(repositoryRoot, ".routekit", "routing", "activations", `${activationRevisionDigest(definition)}.json`);
215
+ mkdirSync(join(repositoryRoot, ".routekit", "routing", "activations"), {
216
+ recursive: true
217
+ });
218
+ writeFileSync(path, `${JSON.stringify(definition, null, 2)}\n`);
219
+ return { definition, path };
220
+ };
221
+ try {
222
+ const store = makeRoutingActivationStore(root);
223
+ const first = activation(repositoryA, "repository-a");
224
+ const firstRevision = activationRevisionDigest(first.definition);
225
+ await runRouteKitEffect(store.installAuthoritative(first.definition, first.path, null, null));
226
+ const firstDefinitionBefore = readFileSync(first.path, "utf8");
227
+ const second = activation(repositoryB, "repository-b");
228
+ const secondRevision = activationRevisionDigest(second.definition);
229
+ const retainedFirstPath = join(repositoryB, ".routekit", "routing", "activations", `${firstRevision}.json`);
230
+ await runRouteKitEffect(store.installAuthoritative(second.definition, second.path, firstRevision, null));
231
+ assert.equal(readFileSync(first.path, "utf8"), firstDefinitionBefore);
232
+ assert.equal(readFileSync(retainedFirstPath, "utf8"), firstDefinitionBefore);
233
+ assert.equal((await runRouteKitEffect(store.readDeployment())).authoritative
234
+ ?.activationRevisionDigest, secondRevision);
235
+ assert.equal((await runRouteKitEffect(store.readDeployment())).previousAuthoritative
236
+ ?.activationRevisionDigest, firstRevision);
237
+ const globalPointer = readFileSync(join(root, "routing-deployment.v2.json"), "utf8");
238
+ assert.match(globalPointer, /"activationPath"/u);
239
+ assert.equal(globalPointer.includes('"activation":'), false);
240
+ assert.equal(globalPointer.includes('"dimensions":'), false);
241
+ assert.equal(globalPointer.includes(first.path), false);
242
+ assert.equal(globalPointer.includes(retainedFirstPath), true);
243
+ assert.equal(globalPointer.includes(second.path), true);
244
+ assert.equal(readFileSync(second.path, "utf8").includes('"dimensions":'), true);
245
+ rmSync(repositoryA, { recursive: true, force: true });
246
+ assert.equal((await runRouteKitEffect(store.readDeployment())).authoritative
247
+ ?.activationRevisionDigest, secondRevision);
248
+ rmSync(retainedFirstPath, { force: true });
249
+ const third = activation(repositoryB, "repository-c");
250
+ await runRouteKitEffect(store.installAuthoritative(third.definition, third.path, secondRevision, firstRevision));
251
+ assert.equal((await runRouteKitEffect(store.readDeployment())).authoritative
252
+ ?.activationRevisionDigest, activationRevisionDigest(third.definition));
253
+ }
254
+ finally {
255
+ rmSync(root, { recursive: true, force: true });
256
+ rmSync(repositoryA, { recursive: true, force: true });
257
+ rmSync(repositoryB, { recursive: true, force: true });
180
258
  }
181
259
  });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velum-labs/routekit-eval-store",
3
3
  "private": false,
4
- "version": "1.2.0",
4
+ "version": "1.3.1",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/velum-labs/routekit.git",
@@ -31,8 +31,8 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "effect": "4.0.0-rc.108",
34
- "@velum-labs/routekit-eval-contracts": "1.2.0",
35
- "@velum-labs/routekit-runtime": "1.2.0"
34
+ "@velum-labs/routekit-eval-contracts": "1.3.1",
35
+ "@velum-labs/routekit-runtime": "1.3.1"
36
36
  },
37
37
  "keywords": [
38
38
  "routekit",