The failure modes that make this problem class break:
The solution is complete and verified. The full write-up is saved at ~/bitcask/SOLUTION.md, with the working project under ~/bitcask/.
The failure modes that make this problem class break:
readdir order (or trusting a hint) lets an older record overwrite a newer one. The keydir must be rebuilt in strict (fileId asc, offset asc) order; only the highest position wins.valueSize === 0 record; it must remove the key only when it is the winning record.now() or emitting keys in Map order or using a fresh file id makes a second run differ. Fix: preserve the last-committed timestamp, sort keys deterministically, reuse the highest data-file id.fsync, rename(2), then unlink superseded files.Data record (HEADER_SIZE=20): crc32c(4) over [4, end), timestamp(8), keySize(4), valueSize(4) (0 = tombstone), key, value.
Hint record (HINT_HEADER_SIZE=28): crc32c(4), timestamp(8), keySize(4), valueSize(4), valuePos(8), key.
Core recovery logic (src/store.js):
recover() {
this.dataFileIds = listFileIds(this.dir, 'data'); // numeric ascending
for (let i = 0; i < this.dataFileIds.length; i++) {
this.replayDataFile(this.dataFileIds[i], i === this.dataFileIds.length - 1);
}
// active file = highest id (append there), else 1
}
replayDataFile(fileId, isNewest) {
const buf = fs.readFileSync(path.join(this.dir, `${fileId}.data`));
let off = 0;
while (off < buf.length) {
if (off + HEADER_SIZE > buf.length) return this.handleTail(fileId, path, buf, off, isNewest);
const dec = decodeRecord(buf, off);
if (!dec) return this.handleTail(fileId, path, buf, off, isNewest);
if (crc32c(buf, off + 4, dec.end) !== dec.crc) return this.handleTail(fileId, path, buf, off, isNewest);
this.applyRecord(fileId, buf, off, dec); // tombstone deletes, value sets
off = dec.end;
}
}
handleTail(fileId, filePath, buf, off, isNewest) {
if (!isNewest) throw new CorruptionError('...', { offset: off, fileId });
// A valid record *after* the bad one ⇒ mid-file corruption, not a torn tail.
if (findNextValidRecord(buf, off + 1) !== -1)
throw new CorruptionError('...', { offset: off, fileId });
fs.truncateSync(filePath, off); // torn append: cut to last good boundary
}
Compaction (Store.compact) snapshots live {key, value, timestamp} triples, sorts by Buffer.compare(key), writes tmpData/tmpHint, fsync, renames over <maxId>.data/<maxId>.hint, then unlinks older files. Because timestamps are preserved, ordering is deterministic, and the max id is reused, a second run is byte-identical.
CLI exits 3 and prints the file-relative offset:
catch (err) {
if (err instanceof CorruptionError || err.exitCode === 3) {
process.stderr.write(`${err.offset}\n`); // exact byte offset
process.exitCode = 3;
} else throw err;
}
cd ~/bitcask
npm test # 24 tests, all pass
npm run verify # end-to-end guarantees
node src/cli.js put /tmp/db a 1
node src/cli.js del /tmp/db b
node src/cli.js recover /tmp/db # ok N
node src/cli.js compact /tmp/db # compacted fileId=... before=... after=...
$ npm test
# tests 24
# pass 24
# fail 0
$ node scripts/verify.js
PASS recovery returns last-committed value for every key
PASS torn append at tail is truncated rather than fatal
PASS mid-file corruption exits 3 and prints the exact byte offset
PASS compaction reclaims dead space and second run is a byte-level no-op
ALL VERIFICATION CHECKS PASSED
Coverage includes: last-committed value per key after recovery; repeated reopens; (fileId, offset) ordering; winning tombstones; binary keys/values; stale hint ignored; short header / truncated body / CRC mismatch at newest tail truncated; mid-file corruption in older and newest files → exitCode 3 with exact offset; CLI exit 3 + exact offset; compaction reclaiming space, valid hint, sorted order, timestamp preservation; second compaction byte-identical; compaction after torn append; fully-tombstoned store → 0 bytes; and an 8-round randomized property test against a reference Map (including simulated interrupted appends) with per-round idempotency.
Files: src/crc32c.js, src/format.js, src/store.js, src/cli.js, src/index.js, scripts/verify.js, test/{recovery,corruption,compaction,fuzz}.test.js, plus the full write-up SOLUTION.md. All embedded code blocks in the write-up were extracted and passed node --check.
# Evidence - Problem class: node-bitcask-keydir-hint-rebuild-torn-append-recovery - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-18T16:43:57.593Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Given a directory of Bitcask-style append-only data files (record = crc32c, timestamp, key size, value size, key, value) plus a possibly stale hint file, rebuild the in-memory keydir by replaying the log, honoring tombstones and monotonic file-id/offset write ordering, and treating a checksum mismatch or short read at the tail of the newest file as a torn append to truncate rather than as fatal corruption. Implement offline merge/compaction that rewrites only live keys in monotonic order, emits a fresh hint file, and fully reclaims space for dead keys, while a mid-file (non-tail) corruption must exit 3 and print the exact byte offset of the first bad record. Ship tests proving every key reads back its last-committed value after recovery and that a second compaction run is a byte-level no-op.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "node-bitcask-keydir-hint-rebuild-torn-append-recovery", "provider": "openrouter", "solved_at": "2026-09-18T16:43:57.593Z", "version": "20"}The solution is complete and verified. The full write-up is saved at ~/bitcask/SOLUTION.md, with the working project under ~/bitcask/.
The failure modes that make this problem class break:
readdir order (or trusting a hint) lets an older record overwrite a newer one. The keydir must be rebuilt in strict (fileId asc, offset asc) order; only the highest position wins.valueSize === 0 record; it must remove the key only when it is the winning record.now() or emitting keys in Map order or using a fresh file id makes a second run differ. Fix: preserve the last-committed timestamp, sort keys deterministically, reuse the highest data-file id.fsync, rename(2), then unlink superseded files.Data record (HEADER_SIZE=20): crc32c(4) over [4, end), timestamp(8), keySize(4), valueSize(4) (0 = tombstone), key, value.
Hint record (HINT_HEADER_SIZE=28): crc32c(4), timestamp(8), keySize(4), valueSize(4), valuePos(8), key.
Core recovery logic (src/store.js):
recover() {
this.dataFileIds = listFileIds(this.dir, 'data'); // numeric ascending
for (let i = 0; i < this.dataFileIds.length; i++) {
this.replayDataFile(this.dataFileIds[i], i === this.dataFileIds.length - 1);
}
// active file = highest id (append there), else 1
}
replayDataFile(fileId, isNewest) {
const buf = fs.readFileSync(path.join(this.dir, `${fileId}.data`));
let off = 0;
while (off < buf.length) {
if (off + HEADER_SIZE > buf.length) return this.handleTail(fileId, path, buf, off, isNewest);
const dec = decodeRecord(buf, off);
if (!dec) return this.handleTail(fileId, path, buf, off, isNewest);
if (crc32c(buf, off + 4, dec.end) !== dec.crc) return this.handleTail(fileId, path, buf, off, isNewest);
this.applyRecord(fileId, buf, off, dec); // tombstone deletes, value sets
off = dec.end;
}
}
handleTail(fileId, filePath, buf, off, isNewest) {
if (!isNewest) throw new CorruptionError('...', { offset: off, fileId });
// A valid record *after* the bad one ⇒ mid-file corruption, not a torn tail.
if (findNextValidRecord(buf, off + 1) !== -1)
throw new CorruptionError('...', { offset: off, fileId });
fs.truncateSync(filePath, off); // torn append: cut to last good boundary
}
Compaction (Store.compact) snapshots live {key, value, timestamp} triples, sorts by Buffer.compare(key), writes tmpData/tmpHint, fsync, renames over <maxId>.data/<maxId>.hint, then unlinks older files. Because timestamps are preserved, ordering is deterministic, and the max id is reused, a second run is byte-identical.
CLI exits 3 and prints the file-relative offset:
catch (err) {
if (err instanceof CorruptionError || err.exitCode === 3) {
process.stderr.write(`${err.offset}\n`); // exact byte offset
process.exitCode = 3;
} else throw err;
}
cd ~/bitcask
npm test # 24 tests, all pass
npm run verify # end-to-end guarantees
node src/cli.js put /tmp/db a 1
node src/cli.js del /tmp/db b
node src/cli.js recover /tmp/db # ok N
node src/cli.js compact /tmp/db # compacted fileId=... before=... after=...
$ npm test
# tests 24
# pass 24
# fail 0
$ node scripts/verify.js
PASS recovery returns last-committed value for every key
PASS torn append at tail is truncated rather than fatal
PASS mid-file corruption exits 3 and prints the exact byte offset
PASS compaction reclaims dead space and second run is a byte-level no-op
ALL VERIFICATION CHECKS PASSED
Coverage includes: last-committed value per key after recovery; repeated reopens; (fileId, offset) ordering; winning tombstones; binary keys/values; stale hint ignored; short header / truncated body / CRC mismatch at newest tail truncated; mid-file corruption in older and newest files → exitCode 3 with exact offset; CLI exit 3 + exact offset; compaction reclaiming space, valid hint, sorted order, timestamp preservation; second compaction byte-identical; compaction after torn append; fully-tombstoned store → 0 bytes; and an 8-round randomized property test against a reference Map (including simulated interrupted appends) with per-round idempotency.
Files: src/crc32c.js, src/format.js, src/store.js, src/cli.js, src/index.js, scripts/verify.js, test/{recovery,corruption,compaction,fuzz}.test.js, plus the full write-up SOLUTION.md. All embedded code blocks in the write-up were extracted and passed node --check.
# Evidence - Problem class: node-bitcask-keydir-hint-rebuild-torn-append-recovery - Model: openrouter/deepseek/deepseek-v4.1-flash - Solved: 2026-09-18T16:43:57.593Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "Given a directory of Bitcask-style append-only data files (record = crc32c, timestamp, key size, value size, key, value) plus a possibly stale hint file, rebuild the in-memory keydir by replaying the log, honoring tombstones and monotonic file-id/offset write ordering, and treating a checksum mismatch or short read at the tail of the newest file as a torn append to truncate rather than as fatal corruption. Implement offline merge/compaction that rewrites only live keys in monotonic order, emits a fresh hint file, and fully reclaims space for dead keys, while a mid-file (non-tail) corruption must exit 3 and print the exact byte offset of the first bad record. Ship tests proving every key reads back its last-committed value after recovery and that a second compaction run is a byte-level no-op.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4.1-flash", "problem_class": "node-bitcask-keydir-hint-rebuild-torn-append-recovery", "provider": "openrouter", "solved_at": "2026-09-18T16:43:57.593Z", "version": "20"}