S00-005: Record blocked task and remediation handoff

Changes: Preserve incomplete work and blocker metadata for S00-005. This checkpoint does not mark implementation accepted.
Validation: Session ended with status blocked.
Blocker: Previous runner session was interrupted before submit/escalate; processes stopped. User now explicitly requests resuming development. Preserve partial artifacts, verify them in a fresh bounded remediation, and return original to a new executor only after independent review.

Task: S00-005
Agent: auto-storage-88c3286c6641
Role: storage
Action: claim
Status: blocked

Task title: Выбрать IndexedDB wrapper и migration policy
This commit is contained in:
wt committed 2026-10-15 16:32:52 +07:00
1 parent 55d9c69a21
commit 5f4d044444
16 files changed
+363 -11

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
registry=https://registry.npmjs.org/
strict-ssl=true
+1
View File
@@ -0,0 +1 @@
24.21.0
+28
View File
@@ -0,0 +1,28 @@
{
"name": "0004-storage-design",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "0004-storage-design",
"version": "0.0.0",
"dependencies": {
"fake-indexeddb": "6.2.5"
},
"engines": {
"node": "24.21.0",
"npm": "11.19.0"
}
},
"node_modules/fake-indexeddb": {
"version": "6.2.5",
"resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz",
"integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
}
}
}
@@ -0,0 +1,16 @@
{
"name": "0004-storage-design",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": { "node": "24.21.0", "npm": "11.19.0" },
"packageManager": "npm@11.19.0",
"scripts": {
"check:rollback": "node src/rollback.mjs",
"check:quota": "node src/quota-blocked.mjs",
"check": "npm run check:rollback && npm run check:quota"
},
"dependencies": {
"fake-indexeddb": "6.2.5"
}
}
@@ -0,0 +1,18 @@
import "fake-indexeddb/auto";
function open(name, version, upgrade) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(name, version);
const ev = { upgraded:false, blocked:false };
req.onupgradeneeded = () => { ev.upgraded = true; if (upgrade) upgrade(req.result); };
req.onsuccess = () => resolve({db:req.result, ev});
req.onerror = () => reject(new Error(`open ${name} v${version}: ${req.error?.name}`));
req.onblocked = () => { ev.blocked=true; console.log("BLOCKED", name); };
});
}
// fresh DB clears to v2 cleanly
const o = await open("fresh", 2, (db)=>{ const a=db.createObjectStore("accounts",{keyPath:"accountId"}); a.add({accountId:"A1",displayName:"Alice"}); });
console.log("fresh v2 ok", o.db.version, [...o.db.objectStoreNames]);
o.db.close();
// not deleting cleanly - check delete
await new Promise((res,rej)=>{ const r=indexedDB.deleteDatabase("fresh"); r.onsuccess=res; r.onerror=()=>rej(r.error); });
console.log("after delete", await indexedDB.databases());
@@ -0,0 +1,108 @@
import "fake-indexeddb/auto";
import assert from "node:assert/strict";
// Bounded storage experiment (S00-005, focused check "Rollback experiment").
// Uses fake-indexeddb 6.2.5 (in-memory, pure-JS IndexedDB implementation) to exercise
// the transaction/schema-upgrade rollback guarantees that a Web storage layer relies
// on for STORE-001 (transactional saves or verifiable equivalent recovery).
// This is an API-semantics probe, not a browser-durability claim.
const DB = "rollback-probe";
function open(version, upgrade) {
return new Promise((resolve, reject) => {
const req = indexedDB.open(DB, version);
const events = { upgraded: false, blocked: false };
req.onupgradeneeded = () => {
events.upgraded = true;
if (upgrade) upgrade(req.result);
};
req.onsuccess = () => resolve({ db: req.result, events });
req.onerror = () => reject(new Error(`open(v${version}): ${req.error?.name}`));
req.onblocked = () => { events.blocked = true; };
});
}
function getAll(db, store) {
return new Promise((resolve, reject) => {
const tx = db.transaction(store, "readonly");
tx.objectStore(store).getAll().onsuccess = (e) => resolve(e.target.result);
tx.onerror = () => reject(tx.error);
});
}
async function seedV1() {
const { db } = await open(1, (db) => {
db.createObjectStore("accounts", { keyPath: "accountId" });
});
db.close();
// write a durable per-account record (configuration, non-secret fixture)
const w = await open(1);
await new Promise((resolve, reject) => {
const tx = w.db.transaction("accounts", "readwrite");
tx.objectStore("accounts").add({ accountId: "A1", displayName: "Alice" });
tx.oncomplete = resolve;
tx.onerror = () => reject(tx.error);
});
w.db.close();
}
// Scenario 1 (rollback): upgrade v1 -> v2 that fails mid-way because a data migration
// violates a new unique index inside the versionchange transaction. The whole upgrade
// must abort and revert: schema version stays at v1, the new index/store is absent,
// and pre-existing v1 data is untouched.
async function failedUpgradeRollsBack() {
const req = indexedDB.open(DB, 2);
let aborted = true;
req.onupgradeneeded = () => {
const db = req.result;
const accounts = db.createObjectStore("accounts", { keyPath: "accountId" });
accounts.createIndex("displayName", "displayName", { unique: true });
// Both rows share the same unique displayName -> the second write fails and
// aborts the versionchange transaction (organic failure, not a manual abort).
accounts.add({ accountId: "A1", displayName: "Shared" });
accounts.add({ accountId: "A2", displayName: "Shared" });
};
req.onsuccess = () => { aborted = false; req.result.close(); };
req.onerror = () => { /* default abort */ };
await new Promise((resolve) => { req.onsuccess = resolve; req.onerror = resolve; });
assert.equal(aborted, true, "failing data migration must abort the upgrade (no fake success)");
// Schema version must remain at v1 after the abort.
const meta = await indexedDB.databases().then((l) => l.find((d) => d.name === DB));
assert.equal(meta.version, 1, "failed upgrade must keep persisted schema version at v1");
const { db } = await open(1);
assert.ok(db.objectStoreNames.contains("accounts"), "v1 accounts store survives");
const rows = await getAll(db, "accounts");
assert.equal(rows.length, 1, "no partial migration rows persisted");
assert.equal(rows[0].accountId, "A1", "pre-existing v1 data intact after rollback");
db.close();
}
// Scenario 2 (retry after rollback): after a clean rollback a corrected migration can be
// retried and commits fully to v2 without a stale schema or lost data.
async function retryAfterRollbackSucceeds() {
const { db, events } = await open(2, (db) => {
const accounts = db.createObjectStore("accounts", { keyPath: "accountId" });
accounts.createIndex("displayName", "displayName", { unique: true });
accounts.add({ accountId: "A1", displayName: "Alice" });
accounts.add({ accountId: "A2", displayName: "Bob" });
});
assert.equal(events.upgraded, true, "retry upgrade runs");
assert.equal(db.version, 2, "retry commits to v2");
const rows = await getAll(db, "accounts");
assert.equal(rows.length, 2, "both accounts migrated");
db.close();
}
async function main() {
await seedV1();
await failedUpgradeRollsBack();
await retryAfterRollbackSucceeds();
console.log("rollback.mjs PASS: failed v1->v2 upgrade rolled back atomically (schema v1 kept, no partial data); corrected retry committed cleanly to v2.");
}
await main();