43081c75fd34

accept the six-character save IDs Koh 2's early saves keep through migration, an exact ID winning over a longer one it begins (v3.0.182)

time
2026-10-01 19:39 UTC · 1 week ago
machine
Junes-MacBook-Pro.local
agent
claude-opus-5-5
session
tylee kepr 3 cutover
parent
12218adb0fdf
7 files package.json+1 −1src/admin/storage.ts+1 −1src/bucket/repo.ts+3 −1src/mirror/queries.ts+5 −0src/mirror/remote.ts+1 −2tests/bucket.test.ts+6 −0tests/sync.test.ts+11 −0
package.json+1 −1
@@ -1,6 +1,6 @@11 {22 	"name": "kepr",3-	"version": "3.0.181",3+	"version": "3.0.182",44 	"private": true,55 	"type": "module",66 	"scripts": {

view file

src/admin/storage.ts+1 −1
@@ -7,7 +7,7 @@77 import { formatSize, plural } from "../pages/layout.ts";88 import { type AdminContext, adminPage, formField, notice, textFields } from "./console.ts";99 10-const TARBALL = /^\.kepr\/tarballs\/([^/]+)\/([^/]+)\/([0-9a-f]{12})\.tar\.gz$/;10+const TARBALL = /^\.kepr\/tarballs\/([^/]+)\/([^/]+)\/([0-9a-f]{6}|[0-9a-f]{12})\.tar\.gz$/;1111 const HIDDEN = /^\.kepr\/hidden\/([^/]+)$/;1212 1313 type RepoUse = { history: number; builds: number; tarballs: number };

view file

src/bucket/repo.ts+3 −1
@@ -77,7 +77,9 @@7777 7878 const MAX_OBJECT = 512 * 1024 * 1024;7979 80-const SAVE_ID = /^[0-9a-f]{12}$/;80+// Twelve hex characters, or six: Koh 2's early saves keep their shorter IDs through migration81+// (§30, 2026-09-23).82+export const SAVE_ID = /^(?:[0-9a-f]{6}|[0-9a-f]{12})$/;8183 const REPO_NAME = /^[A-Za-z0-9._-]+$/;8284 const CONTROL_CHAR = /\p{Cc}/u;8385 const HEX64 = /^[0-9a-f]{64}$/;

view file

src/mirror/queries.ts+5 −0
@@ -58,6 +58,11 @@5858  * several do. */5959 export function saveByPrefix(db: Database, prefix: string): Save | null {6060 	if (!ID_PREFIX.test(prefix)) return null;61+	// A migrated save's whole six-character ID can also begin a twelve-character one.62+	const exact = db63+		.query<Save, [string]>(`SELECT ${SAVE_COLUMNS} FROM saves WHERE id = ?`)64+		.get(prefix);65+	if (exact) return asSave(exact);6166 	const rows = db6267 		.query<Save, [string]>(`SELECT ${SAVE_COLUMNS} FROM saves WHERE id LIKE ? || '%' LIMIT 2`)6368 		.all(prefix);

view file

src/mirror/remote.ts+1 −2
@@ -1,6 +1,6 @@11 import type { Database } from "bun:sqlite";22 import type { Context, Hono } from "hono";3-import type { EventRecord, SaveFile, SaveRecord } from "../bucket/repo.ts";3+import { type EventRecord, SAVE_ID, type SaveFile, type SaveRecord } from "../bucket/repo.ts";44 import { openPublicRepo, type PageContext } from "../pages/layout.ts";55 import { filesAt, headId } from "./queries.ts";66 @@ -11,7 +11,6 @@1111 const FORMAT = "koh-remote 1";1212 // A private repo and an unknown one answer alike, to a person and to Koh (§20).1313 const NOT_FOUND = '{"error":"repo_not_found","message":"repo not found"}';14-const SAVE_ID = /^[0-9a-f]{12}$/;1514 const OBJECT_HASH = /^[0-9a-f]{64}$/;1615 // The addendum's lifetimes — a save's record can still be rewritten by a strip, so a minute, and an1716 // object never changes once written but a strip can take it away, so a year rather than immutable.

view file

tests/bucket.test.ts+6 −0
@@ -237,8 +237,14 @@237237 		await expect(readTampered(dupe)).rejects.toThrow("Koh 3 schema");238238 	});239239 240+	test("accepts the six-character IDs that Koh 2's early saves keep through migration", async () => {241+		expect((await readTampered((record) => (record.parent = "af96c8"))).parent).toBe("af96c8");242+	});243+240244 	test.each([241245 		["parent", "not-an-id"],246+		["parent", "af96c"],247+		["parent", "af96c8a"],242248 		["restores", "ABCDEF123456"],243249 		["replayed_from", "../../../x"],244250 		["created_at", -1],

view file

tests/sync.test.ts+11 −0
@@ -125,6 +125,17 @@125125 	});126126 });127127 128+test("a whole six-character ID finds its own save, not a longer one it begins", async () => {129+	const mirror = openMirror(await mkdtemp(join(tmpdir(), "kepr-mirror-")), "ids");130+	const add = mirror.query(131+		"INSERT INTO saves (id, parent, message, created_at, machine) VALUES (?, NULL, ?, 1, 'm')",132+	);133+	add.run("af96c8", "migrated from Koh 2");134+	add.run("af96c8123456", "made by Koh 3");135+	expect(saveByPrefix(mirror, "af96c8")?.message).toBe("migrated from Koh 2");136+	expect(saveByPrefix(mirror, "af96c81")?.message).toBe("made by Koh 3");137+});138+128139 test("a sync lands history in batches, so one cut short resumes where it stopped", async () => {129140 	const fx = await makeKohBucket("batched");130141 	const bucket = openBucket({ kind: "folder", path: fx.bucketDir });

view file