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
↓ kepr-43081c75.tar.gzatlas at this save
files changed7 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 −0package.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": {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 };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}$/;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);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.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],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 });