diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 8bfe7b4..81d3dc1 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -121,17 +121,18 @@ test/
└── performance/ # Performance benchmarks
docs/ # VitePress documentation site
-specs/ # Technical specifications and ADRs
+├── specs/ # Technical specifications
+└── adr/ # Architectural decision records
```
## Types of Contributions
- **Bug fixes**: Always welcome (include test case demonstrating the bug)
-- **New backends**: Follow `specs/interface.md` and existing patterns (Redis/PostgreSQL/Firestore)
+- **New backends**: Follow `docs/specs/interface.md` and existing patterns (Redis/PostgreSQL/Firestore)
- **Performance improvements**: Include benchmarks showing improvement
- **Documentation**: Especially examples, edge cases, and troubleshooting
- **Tests**: Better coverage is always good
-- **Spec reviews & improvements**: Review `specs/` directory and propose architectural improvements
+- **Spec reviews & improvements**: Review `docs/specs/` and `docs/adr/` directories and propose architectural improvements
- Identify inconsistencies or ambiguities in specs
- Suggest new ADRs for design decisions
- Improve spec clarity and completeness
@@ -143,7 +144,7 @@ When contributing, follow these key principles from CLAUDE.md:
- **No over-engineering** - Keep it simple and pragmatic
- **Design APIs that are predictable, composable, and hard to misuse**
-- **Record decisions in ADRs** (specs/adrs.md) as you go, not retroactively
+- **Record decisions in ADRs** (`docs/adr/`) as you go, not retroactively
- **Make testability a first-class design constraint**
- **Prioritize correctness and safety over micro-optimizations**
- **Expose the smallest possible public API that solves the problem**
@@ -152,14 +153,14 @@ When contributing, follow these key principles from CLAUDE.md:
If contributing a new backend, ensure:
-- [ ] Implements full `LockBackend` interface (specs/interface.md)
+- [ ] Implements full `LockBackend` interface (`docs/specs/interface.md`)
- [ ] Uses `isLive()` from `common/time-predicates.ts` (no custom time logic)
-- [ ] Uses `makeStorageKey()` for key generation with two-step fence pattern (specs/interface.md#fence-key-derivation, ADR-006)
+- [ ] Uses `makeStorageKey()` for key generation with two-step fence pattern (`docs/specs/interface.md#fence-key-derivation`, ADR-006)
- [ ] Uses `formatFence()` for 15-digit zero-padded fence tokens (ADR-004)
- [ ] Implements TOCTOU protection for release/extend (ADR-003)
- [ ] Explicit ownership verification after reverse mapping
- [ ] Comprehensive unit and integration tests
-- [ ] Backend-specific spec document (follow specs/redis-backend.md, specs/postgres-backend.md, or specs/firestore-backend.md pattern)
+- [ ] Backend-specific spec document (follow `docs/specs/redis-backend.md`, `docs/specs/postgres-backend.md`, or `docs/specs/firestore-backend.md` pattern)
## Getting Help
@@ -167,7 +168,7 @@ If contributing a new backend, ensure:
- **Discord**: Join [Kriasoft Discord](https://discord.gg/EnbEa7Gsxg) #syncguard channel
- **Bugs**: Check [existing issues](https://github.com/kriasoft/syncguard/issues) first
- **Ideas**: Start with a discussion before coding to align on approach
-- **Documentation**: See [docs site](https://kriasoft.com/syncguard/) and specs/ directory
+- **Documentation**: See [docs site](https://kriasoft.com/syncguard/) and `docs/specs/` directory
## Code of Conduct
diff --git a/CLAUDE.md b/CLAUDE.md
index cdeda8d..5b773ce 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,92 +1,53 @@
-# SyncGuard - Distributed Lock Library
+# SyncGuard — Distributed Lock Library
-TypeScript library for preventing race conditions across microservices using Redis, PostgreSQL, or Firestore backends.
+TypeScript locks for Redis, PostgreSQL, and Firestore.
-## Commands
+Docs:
-```bash
-bun run build # Build to dist/
-bun run typecheck # Type check without emit
-bun run format # Prettier auto-format
-bun run dev # Watch mode
-```
-
-## Architecture
-
-### Core API Design
-
-See `specs/interface.md` for complete API examples, usage patterns, LockBackend interface specification, and type definitions.
+- Specs in `docs/specs/` (start with `interface.md`, then backend deltas, ADRs in `docs/adr/000-template` and `003`–`016`) — see `docs/specs/README.md`
+- VitePress site:
-### Project Structure
+Commands:
-```text
-Core:
- index.ts → Public API exports for custom backends
- common/ → Shared utilities, types, and core functionality
- (See common/README.md for detailed structure)
-
-Backends:
- redis/ → Redis backend implementation (see redis/README.md)
- postgres/ → PostgreSQL backend implementation (see postgres/README.md)
- firestore/ → Firestore backend implementation (see firestore/README.md)
-
-Documentation:
- specs/ → Technical specifications
- README.md → Spec navigation & reading order
- interface.md → LockBackend API contracts & usage examples
- redis-backend.md → Redis backend specification
- postgres-backend.md → PostgreSQL backend specification
- firestore-backend.md → Firestore backend specification
- adrs.md → Architectural decision records
- docs/ → Documentation site (https://kriasoft.com/syncguard/)
+```bash
+bun run build # build dist/
+bun run typecheck # type check
+bun run format # prettier
+bun run dev # watch mode
```
-## Implementation Requirements
-
-**Backend-specific requirements**: See `specs/interface.md`, `specs/redis-backend.md`, `specs/postgres-backend.md`, and `specs/firestore-backend.md`
-
-### Key Design Principles
+Architecture:
-- No over-engineering - keep it simple and pragmatic.
-- Design APIs that are predictable, composable, and hard to misuse.
-- Record decisions in lightweight ADRs as you go, not retroactively.
-- Make testability a first-class design constraint, not an afterthought.
-- Performance: O(1) acquire/isLocked, O(log n) release/extend acceptable
-- Prioritize correctness and safety over micro-optimizations.
-- Expose the smallest possible public API that solves the problem.
-- Prioritize optimal, simple and elegant API over backwards compatibility.
+- Core API and types → `docs/specs/interface.md`
+- Project layout: `index.ts` (public API), `common/` (shared utilities), `redis/`, `postgres/`, `firestore/` (backends; each has README), `docs/` (specs + ADRs)
-### Module Exports
+Implementation guardrails:
-- Main: `syncguard` → Core types/utilities
-- Submodules: `syncguard/redis`, `syncguard/postgres`, `syncguard/firestore`, `syncguard/common`
-- All exports use ES modules with TypeScript declarations
+- Follow `docs/specs/interface.md` plus backend specs (`redis-backend.md`, `postgres-backend.md`, `firestore-backend.md`)
+- Backend specs restate inherited requirements (ADR-012) and add storage schema, atomicity, error mapping, TTL, and perf notes
+- Atomic mutations; TOCTOU-safe release/extend; key- and lockId-based `lookup()`; reuse `common/time-predicates.ts:isLive`
-## Testing Approach
+Principles:
-**Hybrid testing strategy** - See `test/README.md` for details
+- Predictable, composable APIs; smallest practical surface
+- Correctness and safety over micro-optimizations; testability first
+- Record decisions as ADRs while working, not after
-Assume that:
+Module exports:
-- Redis server is already running on localhost:6379
-- PostgreSQL server is already running on localhost:5432
-- Firestore emulator is already running on localhost:8080
+- `syncguard` main types/utilities; subpaths: `syncguard/redis`, `syncguard/postgres`, `syncguard/firestore`, `syncguard/common` (ESM with d.ts)
-### Development workflow
+Testing:
-1. **Unit tests**: `bun run test:unit` (fast, mocked dependencies)
-2. **Build/typecheck**: `bun run build && bun run typecheck`
-3. **Integration tests**: `bun run test:integration` (requires Redis, PostgreSQL, and Firestore emulator)
-4. **Performance validation**: `bun run test:performance` (optional)
+- Unit: `bun run test:unit`
+- Build/typecheck: `bun run build && bun run typecheck`
+- Integration: `bun run test:integration` (needs Redis 6379, Postgres 5432, Firestore emulator 8080)
+- Performance: `bun run test:performance` (optional)
-## Code Standards
+Code standards:
-- **Functional style**: Pure functions, immutable data, `const` over `let`, avoid classes
-- **TypeScript**: Strict mode, ESNext target, noUncheckedIndexedAccess
-- **Formatting**: Prettier with default config
-- **Headers**: SPDX license identifiers required
-- **Exports**: Named exports preferred, tree-shakable modules
-- **Error handling**: Primary API throws `LockError`, manual ops return `LockResult`
-- **Error messages**: Include context (key, lockId) in all errors
-- **Peer dependencies**: Optional - users install only what they need
-- **JSDoc**: Required for all public APIs
+- Functional style; strict TypeScript (ESNext, `noUncheckedIndexedAccess`)
+- Prettier formatting; SPDX headers required
+- Named exports; tree-shakable
+- Errors: public API throws `LockError`; manual ops return `LockResult`; include key/lockId in messages
+- Peer deps optional; JSDoc on public APIs
diff --git a/README.md b/README.md
index 1750518..f5b584f 100644
--- a/README.md
+++ b/README.md
@@ -577,7 +577,7 @@ acquisition: {
We welcome contributions! Here's how you can help:
- 🐛 **Bug fixes** - Include test cases
-- 🚀 **New backends** - Follow [specs/interface.md](./specs/interface.md)
+- 🚀 **New backends** - Follow [docs/specs/interface.md](./docs/specs/interface.md)
- 📖 **Documentation** - Examples, guides, troubleshooting
- 📋 **Spec reviews** - Validate specs match implementation, propose improvements
- ✅ **Tests** - Improve coverage
@@ -587,7 +587,7 @@ See [CONTRIBUTING.md](.github/CONTRIBUTING.md) for detailed guidelines.
## Support & Documentation
- **Docs**: [Full documentation](https://kriasoft.com/syncguard/)
-- **Specs**: [Technical specifications](./specs/) - Architecture decisions and backend requirements
+- **Specs**: [Technical specifications](./docs/specs/) - Architecture decisions and backend requirements
- **Discord**: [Join our community](https://discord.gg/EnbEa7Gsxg)
- **Issues**: [GitHub Issues](https://github.com/kriasoft/syncguard/issues)
diff --git a/common/README.md b/common/README.md
index f280e27..09eb2e5 100644
--- a/common/README.md
+++ b/common/README.md
@@ -228,10 +228,5 @@ bun run test:unit
## Implementation References
-- **Specification**: See `specs/interface.md` for complete requirements
-- **ADRs**: See `specs/adrs.md` for design decisions
- - ADR-003: Explicit Ownership Verification
- - ADR-004: Fence Token Format
- - ADR-005: Unified Time Tolerance
- - ADR-006: Standardized Storage Key Generation
- - ADR-007: Opt-in Telemetry
+- **Specification**: See `docs/specs/interface.md` for complete requirements
+- **ADRs**: See [`docs/adr/`](../docs/adr/) for design decisions
diff --git a/common/auto-lock.ts b/common/auto-lock.ts
index 8cd8ee1..5c2abff 100644
--- a/common/auto-lock.ts
+++ b/common/auto-lock.ts
@@ -46,7 +46,6 @@ const defaultDisposalErrorHandler: OnReleaseError = (err, ctx) => {
/**
* Auto-managed lock with retry logic for acquisition contention.
* Backends perform single-attempt operations (ADR-009), retries handled here.
- * @see specs/adrs.md
*/
/**
@@ -94,7 +93,7 @@ function calculateRetryDelay(
* @returns Result of fn execution
* @throws {LockError} AcquisitionTimeout, NetworkTimeout, or Internal
* @see common/types.ts for LockConfig
- * @see specs/interface.md for usage examples
+ * @see docs/specs/interface.md for usage examples
*/
/**
* Creates a curried lock function bound to a specific backend.
diff --git a/common/backend-semantics.ts b/common/backend-semantics.ts
index 3421a8c..3c84209 100644
--- a/common/backend-semantics.ts
+++ b/common/backend-semantics.ts
@@ -89,7 +89,7 @@ export function mapFirestoreConditions(conditions: {
/**
* Unified backend observation mapper for Redis codes or Firestore conditions.
* @param observation - Redis script code or Firestore query analysis
- * @see specs/interface.md
+ * @see docs/specs/interface.md
*/
export function mapBackendObservation(
observation:
diff --git a/common/backend.ts b/common/backend.ts
index 99c6189..4c11684 100644
--- a/common/backend.ts
+++ b/common/backend.ts
@@ -5,7 +5,7 @@
* Core module exports for LockBackend implementations.
* Import from `syncguard/common` to build custom backends.
*
- * @see specs/interface.md - LockBackend API contracts
+ * @see docs/specs/interface.md - LockBackend API contracts
*/
// Export createAutoLock for internal use by backend modules only
diff --git a/common/constants.ts b/common/constants.ts
index a904707..e1ede56 100644
--- a/common/constants.ts
+++ b/common/constants.ts
@@ -82,9 +82,9 @@ export const BACKEND_LIMITS = {
* const fenceDocId = makeStorageKey("", `fence:${baseKey}`, BACKEND_LIMITS.FIRESTORE, RESERVE_BYTES.FIRESTORE);
* ```
*
- * @see specs/redis-backend.md#dual-key-storage-pattern - Redis reserve bytes calculation
- * @see specs/postgres-backend.md#lock-table-requirements - PostgreSQL reserve bytes (0) rationale
- * @see specs/firestore-backend.md#lock-documents - Firestore reserve bytes (0) rationale
+ * @see docs/specs/redis-backend.md#dual-key-storage-pattern - Redis reserve bytes calculation
+ * @see docs/specs/postgres-backend.md#lock-table-requirements - PostgreSQL reserve bytes (0) rationale
+ * @see docs/specs/firestore-backend.md#lock-documents - Firestore reserve bytes (0) rationale
*/
export const RESERVE_BYTES = {
/**
@@ -137,8 +137,6 @@ export const LOCK_DEFAULTS = {
* and precision safety within Lua's 53-bit float (2^53-1 ≈ 9.007e15).
*
* **Capacity**: 10^15 fence tokens = ~31.7 years at 1M locks/sec.
- *
- * @see specs/adrs.md ADR-004 - Fence token format and overflow handling
*/
export const FENCE_THRESHOLDS = {
/**
diff --git a/common/crypto.ts b/common/crypto.ts
index d058034..3f5dff6 100644
--- a/common/crypto.ts
+++ b/common/crypto.ts
@@ -134,9 +134,9 @@ function sha256Sync(data: Uint8Array): Uint8Array {
* @param reserveBytes - Bytes reserved for suffixes in derived keys (e.g., Redis index/fence keys)
* @returns Storage key, truncated/hashed if necessary
* @throws {LockError} "InvalidArgument" if prefix + reserve exceeds limit, or if even hashed form exceeds limit
- * @see specs/interface.md#storage-key-generation - Normative specification
- * @see specs/interface.md#fence-key-derivation - Two-step fence key pattern
- * @see specs/adrs.md ADR-006 - Mandatory uniform key truncation rationale
+ * @see docs/specs/interface.md#storage-key-generation - Normative specification
+ * @see docs/specs/interface.md#fence-key-derivation - Two-step fence key pattern
+ * @see docs/adr/006-mandatory-key-truncation.md - Mandatory uniform key truncation rationale
*/
export function makeStorageKey(
prefix: string,
diff --git a/common/disposable.ts b/common/disposable.ts
index 59e6375..177270b 100644
--- a/common/disposable.ts
+++ b/common/disposable.ts
@@ -72,9 +72,9 @@
* **Important**: disposeTimeoutMs bounds the async disposal wait time, not the actual
* backend cleanup. Use for responsiveness guarantees in high-reliability contexts.
*
- * @see specs/interface.md#resource-management - Normative specification
- * @see specs/adrs.md#adr-015-async-raii-for-locks - ADR-015: Async RAII for Locks
- * @see specs/adrs.md#adr-016-opt-in-disposal-timeout - ADR-016: Opt-In Disposal Timeout
+ * @see docs/specs/interface.md#resource-management - Normative specification
+ * @see docs/adr/015-async-raii-locks.md - Async RAII for Locks
+ * @see docs/adr/016-disposal-timeout.md - Opt-In Disposal Timeout
*/
import type {
@@ -128,8 +128,8 @@ export type { OnReleaseError } from "./types.js";
* });
* ```
*
- * @see specs/interface.md#error-handling - Error handling best practices
- * @see specs/adrs.md#adr-015-async-raii-for-locks - Disposal error semantics
+ * @see docs/specs/interface.md#error-handling - Error handling best practices
+ * @see docs/adr/015-async-raii-locks.md - Disposal error semantics
*/
const defaultDisposalErrorHandler: OnReleaseError = (err, ctx) => {
// Only log in development or when explicitly enabled via env var
diff --git a/common/telemetry.ts b/common/telemetry.ts
index b3ac969..feac1cb 100644
--- a/common/telemetry.ts
+++ b/common/telemetry.ts
@@ -21,8 +21,8 @@ import type {
* @param backend - Base backend to instrument
* @param options - Telemetry configuration with event callback
* @returns Instrumented backend with same capabilities
- * @see specs/interface.md Usage patterns and examples
- * @see specs/adrs.md ADR-007 for opt-in design rationale
+ * @see docs/specs/interface.md Usage patterns and examples
+ * @see docs/adr/007-opt-in-telemetry.md for opt-in design rationale
*/
export function withTelemetry(
backend: LockBackend,
diff --git a/common/time-predicates.ts b/common/time-predicates.ts
index 1d02856..cd8eedb 100644
--- a/common/time-predicates.ts
+++ b/common/time-predicates.ts
@@ -4,7 +4,7 @@
/**
* Canonical time authority predicates for cross-backend consistency.
* ALL backends MUST use these functions - custom time logic is forbidden.
- * @see specs/interface.md
+ * @see docs/specs/interface.md
*/
/**
diff --git a/common/types.ts b/common/types.ts
index 5dc4b51..dbda733 100644
--- a/common/types.ts
+++ b/common/types.ts
@@ -61,7 +61,7 @@ export type Fence = string;
/**
* Hash identifier for observability (SHA-256 truncated to 96 bits, 24 hex chars).
*
- * @see specs/interface.md#hash-identifier-format - Normative specification
+ * @see docs/specs/interface.md#hash-identifier-format - Normative specification
*/
export type HashId = string;
@@ -124,7 +124,7 @@ export type ExtendResult = { ok: true; expiresAtMs: number } | { ok: false };
/**
* Sanitized lock info from lookup(). Hashed identifiers prevent accidental logging.
*
- * @see specs/interface.md#lock-information-types - Normative specification
+ * @see docs/specs/interface.md#lock-information-types - Normative specification
*/
export type LockInfo = {
/** SHA-256 hash of key (96-bit truncated) */
@@ -140,7 +140,7 @@ export type LockInfo = {
/**
* Debug variant with raw identifiers (via getByKeyRaw/getByIdRaw helpers). SECURITY: Contains sensitive data.
*
- * @see specs/interface.md#lock-information-types - Normative specification
+ * @see docs/specs/interface.md#lock-information-types - Normative specification
*/
export type LockInfoDebug = LockInfo & {
/** Raw key for debugging */
@@ -207,7 +207,7 @@ export type LockInfoDebug = LockInfo & {
* @param error Normalized error that occurred during release (LockError or Error)
* @param context Error context with lock identifiers and source (always "disposal")
*
- * @see specs/interface.md#error-handling-patterns - Complete error handling guide
+ * @see docs/specs/interface.md#error-handling-patterns - Complete error handling guide
*/
export type OnReleaseError = (
error: Error,
@@ -244,7 +244,7 @@ export interface BackendConfig {
* ```
*
* @see OnReleaseError for error handling patterns
- * @see specs/interface.md#error-handling-patterns
+ * @see docs/specs/interface.md#error-handling-patterns
*/
onReleaseError?: OnReleaseError;
@@ -305,7 +305,7 @@ export interface LockConfig {
* ```
*
* @see OnReleaseError for error handling patterns
- * @see specs/interface.md#error-handling-patterns
+ * @see docs/specs/interface.md#error-handling-patterns
*/
onReleaseError?: OnReleaseError;
}
@@ -377,7 +377,7 @@ export interface LockBackend<
/**
* Minimal event structure for telemetry. Hashes computed on-demand.
*
- * @see specs/interface.md#telemetry-event-types - Normative specification
+ * @see docs/specs/interface.md#telemetry-event-types - Normative specification
*/
export type LockEvent = {
/** Operation type (acquire, release, extend, isLocked, lookup) */
diff --git a/common/validation.ts b/common/validation.ts
index 7a86632..82805d4 100644
--- a/common/validation.ts
+++ b/common/validation.ts
@@ -11,8 +11,8 @@ import { LockError } from "./errors.js";
*
* @param lockId - Lock identifier to validate
* @throws {LockError} InvalidArgument for format violations (empty, wrong length, invalid characters)
- * @see specs/interface.md#acquire-operation-requirements - Normative lockId validation requirement
- * @see specs/interface.md#security-considerations - Lock ID security and CSPRNG requirements
+ * @see docs/specs/interface.md#acquire-operation-requirements - Normative lockId validation requirement
+ * @see docs/specs/interface.md#security-considerations - Lock ID security and CSPRNG requirements
*/
export function validateLockId(lockId: string): void {
if (
@@ -34,7 +34,7 @@ export function validateLockId(lockId: string): void {
* @param key - User-provided lock key
* @returns Normalized key safe for backend storage
* @throws {LockError} InvalidArgument for empty/oversized keys (max 512 bytes after NFC normalization)
- * @see specs/interface.md#core-constants - Normative MAX_KEY_LENGTH_BYTES requirement
+ * @see docs/specs/interface.md#core-constants - Normative MAX_KEY_LENGTH_BYTES requirement
* @see common/constants.ts - MAX_KEY_LENGTH_BYTES constant definition
*/
export function normalizeAndValidateKey(key: string): string {
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index 5278f02..3d0162a 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -88,11 +88,11 @@ export default defineConfig({
items: [
{
text: "Specifications",
- link: "https://github.com/kriasoft/syncguard/tree/main/specs",
+ link: "https://github.com/kriasoft/syncguard/tree/main/docs/specs",
},
{
text: "Architecture Decisions",
- link: "https://github.com/kriasoft/syncguard/blob/main/specs/adrs.md",
+ link: "https://github.com/kriasoft/syncguard/tree/main/docs/adr",
},
{
text: "Contributing",
diff --git a/docs/adr/000-template.md b/docs/adr/000-template.md
new file mode 100644
index 0000000..8617dcd
--- /dev/null
+++ b/docs/adr/000-template.md
@@ -0,0 +1,28 @@
+# ADR-NNN Title
+
+**Status:** Proposed | Accepted | Deprecated | Superseded
+**Date:** YYYY-MM-DD
+**Tags:** tag1, tag2
+
+## Problem
+
+- One or two sentences on the decision trigger or constraint.
+
+## Decision
+
+- The chosen approach in a short paragraph.
+
+## Alternatives (brief)
+
+- Option A — why not.
+- Option B — why not.
+
+## Impact
+
+- Positive:
+- Negative/Risks:
+
+## Links
+
+- Code/Docs:
+- Related ADRs:
diff --git a/docs/adr/003-explicit-ownership-verification.md b/docs/adr/003-explicit-ownership-verification.md
new file mode 100644
index 0000000..5eba89e
--- /dev/null
+++ b/docs/adr/003-explicit-ownership-verification.md
@@ -0,0 +1,42 @@
+# ADR-003 Explicit Ownership Re-Verification in Mutations
+
+**Status:** Accepted
+**Date:** 2025-09
+**Tags:** security, toctou, mutations
+
+## Problem
+
+Backend implementations perform release/extend operations by reverse mapping `lockId → key` then querying/mutating the lock. While the atomic transaction/script pattern already provides TOCTOU protection, explicit ownership verification adds defense-in-depth.
+
+## Decision
+
+ALL backends MUST perform explicit ownership verification after reverse mapping lookup:
+
+```typescript
+// After fetching document via reverse mapping
+if (data?.lockId !== lockId) {
+ return { ok: false, reason: "not-found" };
+}
+```
+
+This provides:
+
+- **Defense-in-depth**: Additional safety layer with negligible performance cost
+- **Cross-backend consistency**: Ensures Redis and Firestore implement identical ownership checking
+- **TOCTOU protection**: Guards against edge cases in the atomic resolve→validate→mutate flow
+- **Code clarity**: Makes ownership verification explicit rather than implicit
+
+## Alternatives (brief)
+
+- Trust index lookup alone — insufficient defense-in-depth against edge cases
+- Transaction-only protection — doesn't make ownership check explicit in code
+
+## Impact
+
+- Positive: Protection against rare but catastrophic wrong-lock mutations; cross-backend consistency
+- Negative/Risks: Negligible—single field comparison has no measurable overhead
+
+## Links
+
+- Code/Docs: `redis/scripts.ts`, `firestore/operations/*.ts`, `postgres/operations/*.ts`
+- Related ADRs: ADR-013 (index retrieval pattern)
diff --git a/docs/adr/004-lexicographic-fence-comparison.md b/docs/adr/004-lexicographic-fence-comparison.md
new file mode 100644
index 0000000..866c645
--- /dev/null
+++ b/docs/adr/004-lexicographic-fence-comparison.md
@@ -0,0 +1,35 @@
+# ADR-004 Lexicographic Fence Comparison
+
+**Status:** Accepted
+**Date:** 2025-09
+**Tags:** fence-tokens, api-design, precision
+
+## Problem
+
+The original fence design claimed tokens were "opaque" while mandating specific formatting and comparison helpers. More critically, the initial 19-digit format created a **precision safety issue**: Lua numbers use IEEE 754 doubles (~53 bits precision), so fence values exceeding 2^53-1 (~9e15) would suffer precision loss, breaking monotonicity guarantees.
+
+## Decision
+
+Fence tokens are **fixed-width 15-digit zero-padded decimal strings with lexicographic ordering**.
+
+- Direct string comparison (`fenceA > fenceB`) replaces helper functions
+- 15 digits stays within Lua's 53-bit precision limit (2^53-1 ≈ 9.007e15)
+- 10^15 capacity = ~31.7 years at 1M locks/sec
+- All backends return identical format for cross-backend consistency
+
+## Alternatives (brief)
+
+- 19-digit format — exceeds Lua precision, breaks monotonicity
+- BigInt format — not JSON-safe, poor cross-language support
+- Variable-width strings — lexicographic comparison fails ("9" > "10")
+- Helper functions — unnecessary complexity when strings work natively
+
+## Impact
+
+- Positive: Simpler API, precision safety, JSON-safe, cross-language compatible
+- Negative/Risks: Breaking change for existing fence values (acceptable pre-1.0)
+
+## Links
+
+- Code/Docs: `docs/specs/interface.md` (Fence Token Format), `common/constants.ts`
+- Related ADRs: None
diff --git a/docs/adr/005-unified-time-tolerance.md b/docs/adr/005-unified-time-tolerance.md
new file mode 100644
index 0000000..7c9b22f
--- /dev/null
+++ b/docs/adr/005-unified-time-tolerance.md
@@ -0,0 +1,33 @@
+# ADR-005 Unified Time Tolerance
+
+**Status:** Accepted
+**Date:** 2025-09
+**Tags:** time, consistency, cross-backend
+
+## Problem
+
+The original `timeMode` design created inconsistent semantics: `timeMode: "strict"` meant 0ms tolerance on Redis (server-time) but 1000ms on Firestore (client-time minimum safe). This violated predictable cross-backend behavior and created operational risks when switching backends.
+
+## Decision
+
+Remove `timeMode` configuration entirely and use unified 1000ms tolerance across all backends:
+
+- Single `TIME_TOLERANCE_MS` constant in interface.md
+- Same configuration produces identical liveness semantics
+- Backend switching preserves lock behavior
+- No conditional tolerance mapping needed
+
+## Alternatives (brief)
+
+- Keep timeMode with per-backend semantics — confusing, unpredictable behavior
+- Zero tolerance for all backends — unrealistic for client-time systems
+
+## Impact
+
+- Positive: Predictable behavior, testing simplicity, operational safety during backend migration
+- Negative/Risks: Removes fine-grained control (deemed unnecessary complexity)
+
+## Links
+
+- Code/Docs: `docs/specs/interface.md` (TIME_TOLERANCE_MS), `common/time-predicates.ts`
+- Related ADRs: None
diff --git a/docs/adr/006-mandatory-key-truncation.md b/docs/adr/006-mandatory-key-truncation.md
new file mode 100644
index 0000000..46e6907
--- /dev/null
+++ b/docs/adr/006-mandatory-key-truncation.md
@@ -0,0 +1,33 @@
+# ADR-006 Mandatory Uniform Key Truncation
+
+**Status:** Accepted
+**Date:** 2025-09
+**Tags:** keys, consistency, cross-backend
+
+## Problem
+
+Original specs allowed backends to either truncate or throw when prefixed storage keys exceeded limits, creating inconsistent cross-backend behavior. The same user key could produce different outcomes on different backends.
+
+## Decision
+
+Make truncation **mandatory** when `prefix:userKey` exceeds backend storage limits:
+
+- All backends MUST apply standardized hash-truncation via `makeStorageKey()`
+- Throw `InvalidArgument` only when truncated form still exceeds absolute limits
+- Universal application to main lock keys, reverse index keys, and fence counter keys
+- Two-step fence key derivation ensures 1:1 mapping between user keys and fence counters
+
+## Alternatives (brief)
+
+- Allow throw or truncate — unpredictable cross-backend behavior
+- Always throw on long keys — poor DX, prevents valid use cases
+
+## Impact
+
+- Positive: Predictable behavior, testable cross-backend, composable applications
+- Negative/Risks: Requires common utility implementation across backends
+
+## Links
+
+- Code/Docs: `docs/specs/interface.md` (Storage Key Generation, Fence Key Derivation), `common/crypto.ts`
+- Related ADRs: ADR-013 (reverse index storage)
diff --git a/docs/adr/007-opt-in-telemetry.md b/docs/adr/007-opt-in-telemetry.md
new file mode 100644
index 0000000..7d8fae3
--- /dev/null
+++ b/docs/adr/007-opt-in-telemetry.md
@@ -0,0 +1,33 @@
+# ADR-007 Opt-In Telemetry
+
+**Status:** Accepted
+**Date:** 2025-09
+**Tags:** telemetry, api-design, performance
+
+## Problem
+
+Original specification mandated telemetry for all operations, requiring backends to compute hashes and emit events even when no consumer existed. This created unnecessary overhead, complicated the API with redaction policies, and made testing difficult due to side effects.
+
+## Decision
+
+Make telemetry **opt-in** via decorator pattern:
+
+- Telemetry OFF by default—backends don't compute hashes or emit events
+- `withTelemetry(backend, options)` wraps backends to add observability
+- `lookup()` always returns sanitized data; `getByKeyRaw()`/`getByIdRaw()` provide raw access
+- Async isolation—event callbacks never block operations or propagate errors
+
+## Alternatives (brief)
+
+- Mandatory telemetry — unnecessary overhead, testing complexity
+- Per-operation telemetry flags — API clutter, inconsistent behavior
+
+## Impact
+
+- Positive: Zero-cost abstraction, cleaner separation, better testing, tree-shakable
+- Negative/Risks: Breaking change—applications using `onEvent` must wrap backends
+
+## Links
+
+- Code/Docs: `common/telemetry.ts`, `docs/specs/interface.md`
+- Related ADRs: None
diff --git a/docs/adr/008-compile-time-fencing.md b/docs/adr/008-compile-time-fencing.md
new file mode 100644
index 0000000..8ac7cfe
--- /dev/null
+++ b/docs/adr/008-compile-time-fencing.md
@@ -0,0 +1,39 @@
+# ADR-008 Compile-Time Fencing Contract
+
+**Status:** Accepted
+**Date:** 2025-10
+**Tags:** typescript, api-design, type-safety
+
+## Problem
+
+The specification claimed "TypeScript knows fence exists" for Redis/Firestore, yet the type system required optional fence fields, forcing runtime assertions (`expectFence`/`hasFence`) even when backends guaranteed fencing support.
+
+## Decision
+
+Parameterize result types by capabilities so fence is **required** when `supportsFencing: true`:
+
+```typescript
+type AcquireOk = {
+ ok: true;
+ lockId: string;
+ expiresAtMs: number;
+} & (C["supportsFencing"] extends true ? { fence: Fence } : {});
+```
+
+- Direct access to `result.fence` for fencing backends—no assertions needed
+- Keep only `hasFence()` for generic code accepting unknown backends
+
+## Alternatives (brief)
+
+- Optional fence everywhere — runtime assertions required, poor DX
+- Separate backend types — API complexity, code duplication
+
+## Impact
+
+- Positive: Zero boilerplate, type safety, cleaner API, delivers promised ergonomics
+- Negative/Risks: Breaking change—`AcquireResult` becomes generic
+
+## Links
+
+- Code/Docs: `common/types.ts`, `docs/specs/interface.md`
+- Related ADRs: None
diff --git a/docs/adr/009-retries-in-helpers.md b/docs/adr/009-retries-in-helpers.md
new file mode 100644
index 0000000..7d75819
--- /dev/null
+++ b/docs/adr/009-retries-in-helpers.md
@@ -0,0 +1,32 @@
+# ADR-009 Retries Live in Helpers, Core Backends are Single-Attempt
+
+**Status:** Accepted
+**Date:** 2025-10
+**Tags:** api-design, layering, retries
+
+## Problem
+
+Users expect transparent retry on contention, but the initial spec included retry configuration in core constants, creating confusion about where retry logic lives. Backends should stay minimal and composable.
+
+## Decision
+
+- `lock()` helper handles all retry logic and is the primary export
+- Backends perform single-attempt operations only—no retry logic
+- Split constants: `BACKEND_DEFAULTS` (ttlMs) from `LOCK_DEFAULTS` (retry config)
+- Default strategy: exponential backoff with equal jitter (50% randomization)
+- Removed `retryAfterMs` field—no backends can provide meaningful hints
+
+## Alternatives (brief)
+
+- Retry in backends — mixed responsibilities, harder to customize
+- No retry support — poor DX for common use case
+
+## Impact
+
+- Positive: Clear layering, predictable API, composable, smaller core API
+- Negative/Risks: Breaking change—removed `retryAfterMs` from `AcquireResult`
+
+## Links
+
+- Code/Docs: `common/auto-lock.ts`, `common/constants.ts`
+- Related ADRs: None
diff --git a/docs/adr/010-authoritative-expiresat.md b/docs/adr/010-authoritative-expiresat.md
new file mode 100644
index 0000000..20d2334
--- /dev/null
+++ b/docs/adr/010-authoritative-expiresat.md
@@ -0,0 +1,35 @@
+# ADR-010 Authoritative ExpiresAtMs from Mutations
+
+**Status:** Accepted
+**Date:** 2025-10
+**Tags:** time, consistency, heartbeat
+
+## Problem
+
+Redis acquire/extend Lua scripts returned only success indicators, forcing TypeScript to approximate `expiresAtMs` using client-side calculations (`Date.now() + ttlMs`). This created:
+
+1. **Time authority inconsistency**: Redis uses server time, but expiresAtMs came from client time
+2. **Heartbeat scheduling inaccuracy**: Clock skew caused missed windows or wasted extensions
+
+## Decision
+
+All backend mutation operations (acquire, extend) MUST return authoritative `expiresAtMs` computed from the backend's designated time source—no client-side approximation permitted.
+
+- Single source of truth for all timestamps
+- Eliminates skew-induced bugs
+- Enables reliable auto-extend patterns
+
+## Alternatives (brief)
+
+- Client-side approximation with buffer — still drifts, band-aids the problem
+- Separate getExpiry() operation — extra round-trip, doesn't solve race
+
+## Impact
+
+- Positive: Consistent timestamps, accurate heartbeat scheduling, reliable auto-extend
+- Negative/Risks: Trivial—8 bytes added to return payload
+
+## Links
+
+- Code/Docs: `docs/specs/interface.md` (Time Authority), backend specs (acquire/extend sections)
+- Related ADRs: ADR-012 (backend restatement)
diff --git a/docs/adr/011-relaxed-lookup-atomicity.md b/docs/adr/011-relaxed-lookup-atomicity.md
new file mode 100644
index 0000000..abf6690
--- /dev/null
+++ b/docs/adr/011-relaxed-lookup-atomicity.md
@@ -0,0 +1,32 @@
+# ADR-011 Relaxed Atomicity for Diagnostic Lookup
+
+**Status:** Accepted
+**Date:** 2025-10
+**Tags:** lookup, atomicity, diagnostics
+
+## Problem
+
+interface.md required atomic lookup (`MUST use atomic script/transaction`), but Firestore used non-atomic indexed queries. This created spec contradiction despite lookup being explicitly diagnostic-only—NOT a correctness guard.
+
+## Decision
+
+Relax atomicity requirement to match diagnostic nature:
+
+- **SHOULD be atomic** for multi-key stores (Redis via Lua script)
+- **MAY be non-atomic** for indexed stores (Firestore single indexed query)
+- Strong warning: lookup is for diagnostics/UI/monitoring ONLY—never gate mutations on it
+
+## Alternatives (brief)
+
+- Require atomicity everywhere — unnecessary overhead for Firestore
+- Remove lookup entirely — loses valuable diagnostic capability
+
+## Impact
+
+- Positive: Removes spec contradiction, simplifies Firestore, preserves Redis atomicity
+- Negative/Risks: None—no implementation changes needed
+
+## Links
+
+- Code/Docs: `docs/specs/interface.md` (Lookup Operation), backend specs
+- Related ADRs: ADR-003 (ownership verification for mutations)
diff --git a/docs/adr/012-backend-restatement-pattern.md b/docs/adr/012-backend-restatement-pattern.md
new file mode 100644
index 0000000..dccbd29
--- /dev/null
+++ b/docs/adr/012-backend-restatement-pattern.md
@@ -0,0 +1,32 @@
+# ADR-012 Explicit Restatement of Requirements in Backend Specs
+
+**Status:** Accepted
+**Date:** 2025-10
+**Tags:** documentation, machine-readability, consistency
+
+## Problem
+
+ADR-010 and interface.md established authoritative `expiresAtMs` requirement, but backend specs didn't explicitly restate it as MUST bullets. Agents could miss critical requirements during compliance checks.
+
+## Decision
+
+Backend specifications MUST restate key inherited requirements in operation sections:
+
+- Add explicit MUST bullets to Acquire and Extend sections
+- Reference ADRs for rationale to avoid redundant prose
+- Backend Delta Pattern guidance in interface.md
+
+## Alternatives (brief)
+
+- Cross-reference only — agents miss requirements, drift risk
+- Full duplication — maintenance burden, inconsistency risk
+
+## Impact
+
+- Positive: Machine-parseable, agents verify from backend tables alone, prevents drift
+- Negative/Risks: Minor duplication (mitigated by cross-references)
+
+## Links
+
+- Code/Docs: `docs/specs/README.md` (Backend Delta Pattern), backend specs
+- Related ADRs: ADR-010 (authoritative expiresAtMs)
diff --git a/docs/adr/013-full-storage-key-in-index.md b/docs/adr/013-full-storage-key-in-index.md
new file mode 100644
index 0000000..257d7b8
--- /dev/null
+++ b/docs/adr/013-full-storage-key-in-index.md
@@ -0,0 +1,33 @@
+# ADR-013 Store Full Storage Key in Reverse Index
+
+**Status:** Accepted
+**Date:** 2025-10
+**Tags:** redis, correctness, truncation
+
+## Problem
+
+Redis reverse mapping stored the original user key, but release/extend reconstructed the lock key via `{prefix}:{originalKey}`. When key truncation occurred (per ADR-006), reconstruction produced a different key than the truncated form used during acquire—breaking TOCTOU protection.
+
+## Decision
+
+The reverse index MUST store the full computed storage key (post-truncation), not the original user key. Eliminate key reconstruction entirely.
+
+- Index always returns exactly the key used during acquire
+- Works under all conditions—truncated or not
+- Removes reconstruction logic from scripts
+
+## Alternatives (brief)
+
+- Fix reconstruction logic — still fragile, future changes risk re-breaking
+- Disable truncation for index — doesn't solve mismatch
+- Separate truncation for index — complexity explosion
+
+## Impact
+
+- Positive: Eliminates correctness bug, simplifies scripts, testable truncation path
+- Negative/Risks: Breaking change for index format (acceptable pre-1.0)
+
+## Links
+
+- Code/Docs: `redis/scripts.ts`, `redis/operations/*.ts`
+- Related ADRs: ADR-003 (ownership verification), ADR-006 (key truncation)
diff --git a/docs/adr/014-firestore-duplicate-detection.md b/docs/adr/014-firestore-duplicate-detection.md
new file mode 100644
index 0000000..d179827
--- /dev/null
+++ b/docs/adr/014-firestore-duplicate-detection.md
@@ -0,0 +1,34 @@
+# ADR-014 Defensive Detection of Duplicate LockId Documents (Firestore)
+
+**Status:** Accepted
+**Date:** 2025-10
+**Tags:** firestore, defensive-programming, consistency
+
+## Problem
+
+Firestore lacks database-level unique indexes. The library queries by lockId using `.limit(1)`, which returns an arbitrary document when duplicates exist. Bugs, migrations, or manual interventions could create duplicates that go undetected.
+
+## Decision
+
+Add defensive duplicate detection for Firestore lockId queries:
+
+- Remove `.limit(1)` from lockId queries to enable detection
+- When `querySnapshot.docs.length > 1`, treat as internal inconsistency
+- Log warning with context (not error—defensive measure)
+- MAY delete expired duplicates; SHOULD fail-safe on live duplicates
+- Applies to release, extend, and lookup operations
+
+## Alternatives (brief)
+
+- Keep .limit(1) — duplicates remain invisible
+- Fail hard on duplicates — too aggressive for defensive check
+
+## Impact
+
+- Positive: Catches data inconsistencies, operational visibility, safe cleanup
+- Negative/Risks: Negligible—indexed queries are fast, duplicates shouldn't exist
+
+## Links
+
+- Code/Docs: `firestore/operations/*.ts`, `docs/specs/firestore-backend.md`
+- Related ADRs: ADR-003 (ownership verification)
diff --git a/docs/adr/015-async-raii-locks.md b/docs/adr/015-async-raii-locks.md
new file mode 100644
index 0000000..0746b33
--- /dev/null
+++ b/docs/adr/015-async-raii-locks.md
@@ -0,0 +1,36 @@
+# ADR-015 Async RAII for Locks
+
+**Status:** Accepted
+**Date:** 2025-10
+**Tags:** api-design, disposal, typescript
+
+## Problem
+
+Lock management requires cleanup on all code paths. Manual `try/finally` is error-prone. JavaScript's `await using` (AsyncDisposable, Node.js ≥20) provides RAII for automatic cleanup, but integration required design decisions around error handling, signal propagation, and state management.
+
+## Decision
+
+Integrate AsyncDisposable into all `acquire()` results:
+
+- All results implement `Symbol.asyncDispose` for `await using` compatibility
+- Two config patterns: backend-level for `await using`, lock-level for `lock()` helper
+- Stateless handle design—delegate idempotency to backend
+- Handle methods accept optional `AbortSignal` for per-operation cancellation
+- `onReleaseError` callback for disposal failures (disposal never throws)
+- Manual `release()`/`extend()` throw on system errors (consistent with backend API)
+
+## Alternatives (brief)
+
+- Separate disposable wrapper — extra API surface
+- Mutable released flag — race conditions, complexity
+- Throw from disposal — violates AsyncDisposable contract
+
+## Impact
+
+- Positive: Correctness guarantee, ergonomic API, error resilience, composable
+- Negative/Risks: None—additive to existing API
+
+## Links
+
+- Code/Docs: `common/disposable.ts`, `docs/specs/interface.md` (Resource Management)
+- Related ADRs: ADR-016 (disposal timeout)
diff --git a/docs/adr/016-disposal-timeout.md b/docs/adr/016-disposal-timeout.md
new file mode 100644
index 0000000..ca0b6bc
--- /dev/null
+++ b/docs/adr/016-disposal-timeout.md
@@ -0,0 +1,35 @@
+# ADR-016 Opt-In Disposal Timeout
+
+**Status:** Accepted
+**Date:** 2025-10
+**Tags:** disposal, timeout, reliability
+
+## Problem
+
+`Symbol.asyncDispose` calls `release()` without timeout. If backend hangs (network latency, slow queries), disposal blocks indefinitely. Manual `release()` supports `AbortSignal`, but automatic disposal doesn't—inconsistent cancellation behavior.
+
+## Decision
+
+Add **opt-in** `disposeTimeoutMs` configuration:
+
+- Optional field in `BackendConfig` (no default)
+- When configured, disposal creates internal `AbortController` with timeout
+- Timeout errors flow through `onReleaseError` callback
+- Backend-agnostic—applies to Redis, PostgreSQL, Firestore
+- Manual `release()` unaffected—uses caller-provided signal
+
+## Alternatives (brief)
+
+- Default timeout (e.g., 5s) — might cause false timeouts
+- Global signal configuration — too complex, no per-lock control
+- Do nothing — ignores legitimate reliability concerns
+
+## Impact
+
+- Positive: Responsiveness guarantee, observable failures, defense-in-depth
+- Negative/Risks: None—opt-in with no default
+
+## Links
+
+- Code/Docs: `common/disposable.ts`, `common/types.ts`
+- Related ADRs: ADR-015 (Async RAII)
diff --git a/docs/adr/index.md b/docs/adr/index.md
new file mode 100644
index 0000000..61209dd
--- /dev/null
+++ b/docs/adr/index.md
@@ -0,0 +1,22 @@
+# Architecture Decision Records
+
+Design decisions for SyncGuard, recorded as they were made.
+
+| ADR | Title |
+| ----------------------------------------------- | ---------------------------------- |
+| [003](./003-explicit-ownership-verification.md) | Explicit Ownership Re-Verification |
+| [004](./004-lexicographic-fence-comparison.md) | Lexicographic Fence Comparison |
+| [005](./005-unified-time-tolerance.md) | Unified Time Tolerance |
+| [006](./006-mandatory-key-truncation.md) | Mandatory Key Truncation |
+| [007](./007-opt-in-telemetry.md) | Opt-in Telemetry |
+| [008](./008-compile-time-fencing.md) | Compile-time Fencing |
+| [009](./009-retries-in-helpers.md) | Retries in Helpers |
+| [010](./010-authoritative-expiresat.md) | Authoritative expiresAt |
+| [011](./011-relaxed-lookup-atomicity.md) | Relaxed Lookup Atomicity |
+| [012](./012-backend-restatement-pattern.md) | Backend Restatement Pattern |
+| [013](./013-full-storage-key-in-index.md) | Full Storage Key in Index |
+| [014](./014-firestore-duplicate-detection.md) | Firestore Duplicate Detection |
+| [015](./015-async-raii-locks.md) | Async RAII Locks |
+| [016](./016-disposal-timeout.md) | Disposal Timeout |
+
+See [000-template.md](./000-template.md) for writing new ADRs.
diff --git a/docs/api.md b/docs/api.md
index d2f628a..be26500 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -381,10 +381,10 @@ if (owned) {
- For raw data access, use `getByKeyRaw()` or `getByIdRaw()` helpers
- Read-only operation (no side effects)
- Includes `fence` for fencing-capable backends
-- **Atomicity:** Redis uses atomic Lua scripts for lockId lookup (multi-key reads); Firestore uses non-atomic indexed queries with post-read verification (per ADR-011, both approaches ensure portability for diagnostic use)
+- **Atomicity:** Redis uses atomic Lua scripts for lockId lookup (multi-key reads); Firestore uses non-atomic indexed queries with post-read verification (per [ADR-011](https://github.com/kriasoft/syncguard/blob/main/docs/adr/011-relaxed-lookup-atomicity.md), both approaches ensure portability for diagnostic use)
::: warning Diagnostic Use Only
-Lookup is for **diagnostics, UI, and monitoring** — NOT a correctness guard. Never use `lookup() → mutate` patterns. Correctness relies on atomic ownership verification built into `release()` and `extend()` operations (ADR-003).
+Lookup is for **diagnostics, UI, and monitoring** — NOT a correctness guard. Never use `lookup() → mutate` patterns. Correctness relies on atomic ownership verification built into `release()` and `extend()` operations ([ADR-003](https://github.com/kriasoft/syncguard/blob/main/docs/adr/003-explicit-ownership-verification.md)).
:::
---
@@ -520,7 +520,7 @@ interface LockInfoDebug extends LockInfo {
### `Fence`
-Fencing token type (15-digit zero-padded string per ADR-004).
+Fencing token type (15-digit zero-padded string per [ADR-004](https://github.com/kriasoft/syncguard/blob/main/docs/adr/004-lexicographic-fence-comparison.md)).
```typescript
type Fence = string; // e.g., "000000000000001"
@@ -821,7 +821,7 @@ function owns(
```
::: warning Diagnostic Use Only
-This is for **diagnostics, UI, and monitoring** — NOT a correctness guard. Never use `owns() → mutate` patterns. Correctness relies on atomic ownership verification built into `release()` and `extend()` operations (ADR-003).
+This is for **diagnostics, UI, and monitoring** — NOT a correctness guard. Never use `owns() → mutate` patterns. Correctness relies on atomic ownership verification built into `release()` and `extend()` operations ([ADR-003](https://github.com/kriasoft/syncguard/blob/main/docs/adr/003-explicit-ownership-verification.md)).
:::
**Usage:**
@@ -1221,5 +1221,5 @@ console.log(RESERVE_BYTES.REDIS); // 26
```
::: info Internal Constant
-`TIME_TOLERANCE_MS = 1000` is an internal constant used by all backends for consistent liveness checks. It's not exported or user-configurable (ADR-005). Redis, PostgreSQL, and Firestore use the same 1000ms tolerance automatically.
+`TIME_TOLERANCE_MS = 1000` is an internal constant used by all backends for consistent liveness checks. It's not exported or user-configurable ([ADR-005](https://github.com/kriasoft/syncguard/blob/main/docs/adr/005-unified-time-tolerance.md)). Redis, PostgreSQL, and Firestore use the same 1000ms tolerance automatically.
:::
diff --git a/docs/core-concepts.md b/docs/core-concepts.md
index 3bce408..8ded8a7 100644
--- a/docs/core-concepts.md
+++ b/docs/core-concepts.md
@@ -3,7 +3,7 @@
Understand how distributed locks work in SyncGuard.
::: info Architecture & Design Decisions
-Curious about _why_ things work this way? See [specs/adrs.md](https://github.com/kriasoft/syncguard/blob/main/specs/adrs.md) for architectural decision records explaining the rationale behind key design choices.
+Curious about _why_ things work this way? See [ADRs](https://github.com/kriasoft/syncguard/tree/main/docs/adr) for architectural decision records explaining the rationale behind key design choices.
:::
## Lock Lifecycle
diff --git a/docs/fencing.md b/docs/fencing.md
index 5ae0626..aa3aec9 100644
--- a/docs/fencing.md
+++ b/docs/fencing.md
@@ -3,7 +3,7 @@
Monotonic counters that protect against stale lock holders corrupting data.
::: tip Technical Deep Dive
-For the complete fence token specification including format contracts, overflow handling, and cross-backend consistency requirements, see [specs/interface.md § Fence Token Types](https://github.com/kriasoft/syncguard/blob/main/specs/interface.md#fence-token-types).
+For the complete fence token specification including format contracts, overflow handling, and cross-backend consistency requirements, see [docs/specs/interface.md § Fence Token Types](https://github.com/kriasoft/syncguard/blob/main/docs/specs/interface.md#fence-token-types).
:::
## What are Fencing Tokens?
diff --git a/docs/firestore.md b/docs/firestore.md
index b61c3d3..168901c 100644
--- a/docs/firestore.md
+++ b/docs/firestore.md
@@ -9,7 +9,7 @@ See [Fence Counter Lifecycle](#fence-counter-lifecycle) section for complete det
:::
::: tip Technical Specifications
-For backend implementers: See [specs/firestore-backend.md](https://github.com/kriasoft/syncguard/blob/main/specs/firestore-backend.md) for complete implementation requirements, transaction patterns, and architecture decisions.
+For backend implementers: See [docs/specs/firestore-backend.md](https://github.com/kriasoft/syncguard/blob/main/docs/specs/firestore-backend.md) for complete implementation requirements, transaction patterns, and architecture decisions.
:::
## Installation
@@ -125,7 +125,7 @@ Firestore uses **client time** for expiration checks. NTP synchronization is **r
- **Health Checks (REQUIRED)**: Add application-level health checks that detect and alert on clock skew
- **Non-configurable**: Tolerance is internal and cannot be changed to prevent semantic drift
-**Operational Policy**: See [specs/firestore-backend.md § Clock Synchronization Requirements](https://github.com/kriasoft/syncguard/blob/main/specs/firestore-backend.md#firestore-clock-sync-requirements) for the complete operational policy ladder (target/warn/block thresholds) and their relationship to TIME_TOLERANCE_MS.
+**Operational Policy**: See [docs/specs/firestore-backend.md § Clock Synchronization Requirements](https://github.com/kriasoft/syncguard/blob/main/docs/specs/firestore-backend.md#firestore-clock-sync-requirements) for the complete operational policy ladder (target/warn/block thresholds) and their relationship to TIME_TOLERANCE_MS.
### Checking Time Sync
@@ -138,7 +138,7 @@ timedatectl status
```
::: danger Production Requirement
-If reliable time synchronization cannot be guaranteed, **use Redis backend instead**. See the [Clock Synchronization Requirements](https://github.com/kriasoft/syncguard/blob/main/specs/firestore-backend.md#firestore-clock-sync-requirements) spec for specific deployment and monitoring thresholds.
+If reliable time synchronization cannot be guaranteed, **use Redis backend instead**. See the [Clock Synchronization Requirements](https://github.com/kriasoft/syncguard/blob/main/docs/specs/firestore-backend.md#firestore-clock-sync-requirements) spec for specific deployment and monitoring thresholds.
:::
### Why Client Time?
@@ -224,7 +224,7 @@ await db.collection("fence_counters").doc(docId).delete(); // Violates fencing s
**Configuration Safety**: The backend validates that `fenceCollection` differs from `collection` to prevent accidental deletion. Attempting to use the same collection for both will throw `LockError("InvalidArgument")`.
::: info Dual Document Pattern
-See [specs/firestore-backend.md § Fencing Token Implementation](https://github.com/kriasoft/syncguard/blob/main/specs/firestore-backend.md#fencing-token-implementation-pattern) for the complete dual-document pattern specification and atomic transaction requirements.
+See [docs/specs/firestore-backend.md § Fencing Token Implementation](https://github.com/kriasoft/syncguard/blob/main/docs/specs/firestore-backend.md#fencing-token-implementation-pattern) for the complete dual-document pattern specification and atomic transaction requirements.
:::
## Common Patterns
@@ -361,7 +361,7 @@ RUN apt-get update && apt-get install -y ntpdate
- Locks never expire despite TTL passing
- `extend()` operations fail with "expired" errors
-**Solution**: Verify all servers have NTP sync within operational thresholds. See [Clock Synchronization Requirements](https://github.com/kriasoft/syncguard/blob/main/specs/firestore-backend.md#firestore-clock-sync-requirements) for deployment policy (target/warn/block thresholds).
+**Solution**: Verify all servers have NTP sync within operational thresholds. See [Clock Synchronization Requirements](https://github.com/kriasoft/syncguard/blob/main/docs/specs/firestore-backend.md#firestore-clock-sync-requirements) for deployment policy (target/warn/block thresholds).
### Transaction Conflicts
diff --git a/docs/getting-started.md b/docs/getting-started.md
index cf0a0d0..c8cf6db 100644
--- a/docs/getting-started.md
+++ b/docs/getting-started.md
@@ -281,5 +281,5 @@ try {
- `InvalidArgument`: Invalid parameters (malformed key/lockId)
::: tip
-See [API Reference](/api#lockerror) for all error codes. For backend error mapping specifications, see [specs/interface.md § Error Handling](https://github.com/kriasoft/syncguard/blob/main/specs/interface.md#error-handling-standards).
+See [API Reference](/api#lockerror) for all error codes. For backend error mapping specifications, see [docs/specs/interface.md § Error Handling](https://github.com/kriasoft/syncguard/blob/main/docs/specs/interface.md#error-handling-standards).
:::
diff --git a/docs/postgres.md b/docs/postgres.md
index 304f392..e40d510 100644
--- a/docs/postgres.md
+++ b/docs/postgres.md
@@ -9,7 +9,7 @@ See [Fence Counter Lifecycle](#fence-counter-lifecycle) section for complete det
:::
::: tip Technical Specifications
-For backend implementers: See [specs/postgres-backend.md](https://github.com/kriasoft/syncguard/blob/main/specs/postgres-backend.md) for complete implementation requirements, transaction patterns, and architecture decisions.
+For backend implementers: See [docs/specs/postgres-backend.md](https://github.com/kriasoft/syncguard/blob/main/docs/specs/postgres-backend.md) for complete implementation requirements, transaction patterns, and architecture decisions.
:::
## Installation
@@ -328,7 +328,7 @@ DELETE FROM syncguard_fence_counters WHERE fence_key = $1; -- Violates fencing s
**Configuration Safety**: The backend validates that `fenceTableName` differs from `tableName` to prevent accidental deletion. Attempting to use the same table for both will throw `LockError("InvalidArgument")`.
::: info Dual Table Pattern
-See [specs/postgres-backend.md § Fencing Token Implementation](https://github.com/kriasoft/syncguard/blob/main/specs/postgres-backend.md#fencing-token-implementation) for the complete dual-table pattern specification and atomic transaction requirements.
+See [docs/specs/postgres-backend.md § Fencing Token Implementation](https://github.com/kriasoft/syncguard/blob/main/docs/specs/postgres-backend.md#fencing-token-implementation) for the complete dual-table pattern specification and atomic transaction requirements.
:::
## Common Patterns
@@ -530,7 +530,7 @@ SELECT COUNT(*) FROM syncguard_fence_counters;
For applications generating >10M unique lock keys annually, consider key normalization or periodic fence counter archival (if monotonicity can be guaranteed through other means).
::: info Fence Counter Persistence
-Fence counters are intentionally persistent. See [specs/postgres-backend.md § Fence Counter Table Requirements](https://github.com/kriasoft/syncguard/blob/main/specs/postgres-backend.md#fence-counter-table-requirements) for the complete rationale and operational guidance.
+Fence counters are intentionally persistent. See [docs/specs/postgres-backend.md § Fence Counter Table Requirements](https://github.com/kriasoft/syncguard/blob/main/docs/specs/postgres-backend.md#fence-counter-table-requirements) for the complete rationale and operational guidance.
:::
::: tip Performance Tip
diff --git a/docs/redis.md b/docs/redis.md
index 83c4cfa..d73b31e 100644
--- a/docs/redis.md
+++ b/docs/redis.md
@@ -3,7 +3,7 @@
High-performance distributed locking using Redis as the backend. Ideal for sub-millisecond latency requirements and high-throughput scenarios.
::: tip Technical Specifications
-For backend implementers: See [specs/redis-backend.md](https://github.com/kriasoft/syncguard/blob/main/specs/redis-backend.md) for complete implementation requirements, Lua script patterns, and architecture decisions.
+For backend implementers: See [docs/specs/redis-backend.md](https://github.com/kriasoft/syncguard/blob/main/docs/specs/redis-backend.md) for complete implementation requirements, Lua script patterns, and architecture decisions.
:::
## Installation
@@ -268,7 +268,7 @@ redis-cli --scan --pattern "syncguard:fence:*" | wc -l
For applications generating >10M unique lock keys annually, consider key normalization.
::: info Fence Counter Lifecycle
-Fence counters are intentionally persistent. See [specs/redis-backend.md § Fence Key Lifecycle](https://github.com/kriasoft/syncguard/blob/main/specs/redis-backend.md#fence-key-lifecycle-and-memory-considerations) for the complete rationale and operational guidance.
+Fence counters are intentionally persistent. See [docs/specs/redis-backend.md § Fence Key Lifecycle](https://github.com/kriasoft/syncguard/blob/main/docs/specs/redis-backend.md#fence-key-lifecycle-and-memory-considerations) for the complete rationale and operational guidance.
:::
::: tip Performance Tip
diff --git a/docs/specs/README.md b/docs/specs/README.md
new file mode 100644
index 0000000..e568d87
--- /dev/null
+++ b/docs/specs/README.md
@@ -0,0 +1,23 @@
+# SyncGuard Specs
+
+Specifications for the distributed lock system. Read in this order:
+
+1. `interface.md` — core LockBackend contract and required diagnostics
+2. Backend deltas — `redis-backend.md`, `postgres-backend.md`, `firestore-backend.md`
+3. ADRs — `../adr/` for decision history
+
+Quick lookup:
+
+- API contract, errors, lookup requirements → `interface.md`
+- Redis/Postgres/Firestore implementation notes → backend files
+- Architecture decisions → `../adr/` (000-template, 003-explicit-ownership-verification, 004-lexicographic-fence-comparison, 005-unified-time-tolerance, 006-mandatory-key-truncation, 007-opt-in-telemetry, 008-compile-time-fencing, 009-retries-in-helpers, 010-authoritative-expiresat, 011-relaxed-lookup-atomicity, 012-backend-restatement-pattern, 013-full-storage-key-in-index, 014-firestore-duplicate-detection, 015-async-raii-locks, 016-disposal-timeout)
+
+Developer Notes:
+
+- Requirements live in **Requirements** subsections; rationale stays in **Rationale & Notes**. MUST/SHOULD/MAY/NEVER appear only in Requirements.
+- Backend specs restate inherited requirements (ADR-012) and add storage schema, atomicity, error mapping, TTL, and performance notes.
+- When implementing a new backend, ensure atomic mutations, TOCTOU protection for release/extend, both key- and lockId-based `lookup()`, and reuse `isLive()` from `common/time-predicates.ts`.
+
+Keywords:
+
+- MUST = required for contract, SHOULD = strong default, MAY = optional, NEVER = forbidden. Use only in Requirements sections.
diff --git a/specs/firestore-backend.md b/docs/specs/firestore-backend.md
similarity index 100%
rename from specs/firestore-backend.md
rename to docs/specs/firestore-backend.md
diff --git a/specs/interface.md b/docs/specs/interface.md
similarity index 99%
rename from specs/interface.md
rename to docs/specs/interface.md
index bdf4e5e..f05eec1 100644
--- a/specs/interface.md
+++ b/docs/specs/interface.md
@@ -1547,4 +1547,4 @@ When implementing a new backend, ensure:
## Architecture Decision Records
-See [specs/adrs.md](adrs.md) for architectural decisions and design rationale.
+See [ADRs](../adr/) for architectural decisions and design rationale.
diff --git a/specs/postgres-backend.md b/docs/specs/postgres-backend.md
similarity index 100%
rename from specs/postgres-backend.md
rename to docs/specs/postgres-backend.md
diff --git a/specs/redis-backend.md b/docs/specs/redis-backend.md
similarity index 100%
rename from specs/redis-backend.md
rename to docs/specs/redis-backend.md
diff --git a/docs/what-is-syncguard.md b/docs/what-is-syncguard.md
index 31868b4..d1634df 100644
--- a/docs/what-is-syncguard.md
+++ b/docs/what-is-syncguard.md
@@ -146,5 +146,5 @@ SyncGuard uses atomic operations (Lua scripts for Redis, transactions for Postgr
- Ownership verified before release/extend (see ADR-003)
- Monotonic fence tokens for stale lock protection
-**Deep dive**: See [specs/interface.md](https://github.com/kriasoft/syncguard/blob/main/specs/interface.md) for TOCTOU protection requirements and atomicity guarantees.
+**Deep dive**: See [docs/specs/interface.md](https://github.com/kriasoft/syncguard/blob/main/docs/specs/interface.md) for TOCTOU protection requirements and atomicity guarantees.
:::
diff --git a/example/firestore.ts b/example/firestore.ts
index bd38378..3459ec9 100644
--- a/example/firestore.ts
+++ b/example/firestore.ts
@@ -4,7 +4,7 @@
/**
* Example usage patterns for distributed locks with Firestore backend.
* Demonstrates auto-managed locks, manual control, and common patterns.
- * @see specs/interface.md for complete API reference
+ * @see docs/specs/interface.md for complete API reference
*/
import { Firestore } from "@google-cloud/firestore";
diff --git a/firestore/README.md b/firestore/README.md
index 3b60fee..2300bad 100644
--- a/firestore/README.md
+++ b/firestore/README.md
@@ -133,7 +133,7 @@ const backend = createFirestoreBackend(db, {
3. **Add application health checks to detect clock skew**
4. **Fail deployments if NTP sync quality is poor**
-See `specs/firestore-backend.md#firestore-clock-sync-requirements` for complete operational requirements.
+See `docs/specs/firestore-backend.md#firestore-clock-sync-requirements` for complete operational requirements.
## Common Patterns
@@ -211,7 +211,7 @@ Firestore lacks database-level unique indexes on fields. The library relies on:
## Implementation References
-- **Specification**: See `specs/firestore-backend.md` for complete implementation requirements
-- **Common Interface**: See `specs/interface.md` for shared LockBackend contract
-- **ADRs**: See `specs/adrs.md` for architectural decisions
-- **Clock Sync Requirements**: See `specs/firestore-backend.md#firestore-clock-sync-requirements`
+- **Specification**: See `docs/specs/firestore-backend.md` for complete implementation requirements
+- **Common Interface**: See `docs/specs/interface.md` for shared LockBackend contract
+- **ADRs**: See `docs/adr/` for architectural decisions
+- **Clock Sync Requirements**: See `docs/specs/firestore-backend.md#firestore-clock-sync-requirements`
diff --git a/firestore/backend.ts b/firestore/backend.ts
index 2f774de..74a9a3f 100644
--- a/firestore/backend.ts
+++ b/firestore/backend.ts
@@ -25,7 +25,7 @@ import type {
* @param db - Firestore instance from @google-cloud/firestore
* @param options - Backend configuration (collection, ttl, tolerance)
* @returns LockBackend with client-side time authority
- * @see specs/firestore-backend.md
+ * @see docs/specs/firestore-backend.md
*/
export function createFirestoreBackend(
db: Firestore,
diff --git a/firestore/config.ts b/firestore/config.ts
index 9b374ab..e667699 100644
--- a/firestore/config.ts
+++ b/firestore/config.ts
@@ -6,7 +6,7 @@ import type { FirestoreBackendOptions, FirestoreConfig } from "./types.js";
/**
* Default configuration for Firestore backend.
- * @see specs/firestore-backend.md
+ * @see docs/specs/firestore-backend.md
*/
export const FIRESTORE_DEFAULTS = {
/** Collection name for lock documents */
@@ -31,7 +31,7 @@ export function createFirestoreConfig(
options.fenceCollection ?? FIRESTORE_DEFAULTS.fenceCollection;
// CRITICAL: Prevent fence counter deletion by ensuring separate collections
- // Per specs/firestore-backend.md: Fence counters MUST be independent of lock lifecycle
+ // Per docs/specs/firestore-backend.md: Fence counters MUST be independent of lock lifecycle
if (collection === fenceCollection) {
throw new LockError(
"InvalidArgument",
diff --git a/firestore/errors.ts b/firestore/errors.ts
index 9fd15d3..afaa1c2 100644
--- a/firestore/errors.ts
+++ b/firestore/errors.ts
@@ -38,7 +38,7 @@ export function checkAbortedForTransaction(signal?: AbortSignal): void {
*
* @param error - Firestore SDK error or string
* @returns LockError with appropriate code and context
- * @see specs/interface.md
+ * @see docs/specs/interface.md
*/
export function mapFirestoreError(error: any): LockError {
// Handle internal abort error (check multiple properties for maximum robustness)
diff --git a/firestore/operations/extend.ts b/firestore/operations/extend.ts
index 3b1bf54..425ed09 100644
--- a/firestore/operations/extend.ts
+++ b/firestore/operations/extend.ts
@@ -32,11 +32,8 @@ import type { FirestoreConfig, LockDocument } from "../types.js";
* Omits `.limit(1)` to detect duplicate lockIds (ADR-014). Expired duplicates cleaned,
* live duplicates fail safely.
*
- * @see specs/interface.md#extend-operation-requirements - Normative TOCTOU, ownership, and expiresAtMs requirements
- * @see specs/firestore-backend.md#extend-operation-requirements - Firestore transaction pattern
- * @see specs/adrs.md ADR-003 - Explicit ownership verification rationale
- * @see specs/adrs.md ADR-010 - Authoritative expiresAtMs from mutations rationale
- * @see specs/adrs.md ADR-014 - Defensive duplicate detection rationale
+ * @see docs/specs/interface.md#extend-operation-requirements - Normative TOCTOU, ownership, and expiresAtMs requirements
+ * @see docs/specs/firestore-backend.md#extend-operation-requirements - Firestore transaction pattern
*/
export function createExtendOperation(
db: Firestore,
diff --git a/firestore/operations/lookup.ts b/firestore/operations/lookup.ts
index 3606907..ba3e1f4 100644
--- a/firestore/operations/lookup.ts
+++ b/firestore/operations/lookup.ts
@@ -27,7 +27,7 @@ import type {
*
* @remarks
* Non-atomic queries acceptable for diagnostic lookups (ADR-011). Omits `.limit(1)`
- * to detect duplicate lockIds (ADR-014). See: specs/adrs.md (ADR-011, ADR-014)
+ * to detect duplicate lockIds (ADR-014).
*/
export function createLookupOperation(
db: Firestore,
diff --git a/firestore/operations/release.ts b/firestore/operations/release.ts
index 3c9697e..cb729ce 100644
--- a/firestore/operations/release.ts
+++ b/firestore/operations/release.ts
@@ -30,10 +30,8 @@ import type { FirestoreConfig, LockDocument } from "../types.js";
* Omits `.limit(1)` to detect duplicate lockIds (ADR-014). Expired duplicates cleaned,
* live duplicates fail safely.
*
- * @see specs/interface.md#release-operation-requirements - Normative TOCTOU and ownership requirements
- * @see specs/firestore-backend.md#release-operation-requirements - Firestore transaction pattern
- * @see specs/adrs.md ADR-003 - Explicit ownership verification rationale
- * @see specs/adrs.md ADR-014 - Defensive duplicate detection rationale
+ * @see docs/specs/interface.md#release-operation-requirements - Normative TOCTOU and ownership requirements
+ * @see docs/specs/firestore-backend.md#release-operation-requirements - Firestore transaction pattern
*/
export function createReleaseOperation(
db: Firestore,
diff --git a/index.ts b/index.ts
index 483d895..e3674c1 100644
--- a/index.ts
+++ b/index.ts
@@ -5,8 +5,8 @@
* SyncGuard - Distributed Lock Library
*
* Core exports for custom backend implementations and lock helper API.
- * See: specs/interface.md for complete API contracts and usage patterns.
- * See: specs/adrs.md for architectural decisions (telemetry, retry logic).
+ * See: docs/specs/interface.md for complete API contracts and usage patterns.
+ * See: docs/adr/ for architectural decisions (telemetry, retry logic).
*/
// Core Types
diff --git a/postgres/README.md b/postgres/README.md
index 3cdfb52..40461a3 100644
--- a/postgres/README.md
+++ b/postgres/README.md
@@ -209,6 +209,6 @@ WHERE expires_at_ms < EXTRACT(EPOCH FROM NOW()) * 1000;
## Implementation References
-- **Specification**: See `specs/postgres-backend.md` for complete implementation requirements
-- **Common Interface**: See `specs/interface.md` for shared LockBackend contract
-- **ADRs**: See `specs/adrs.md` for architectural decisions
+- **Specification**: See `docs/specs/postgres-backend.md` for complete implementation requirements
+- **Common Interface**: See `docs/specs/interface.md` for shared LockBackend contract
+- **ADRs**: See `docs/adr/` for architectural decisions
diff --git a/postgres/backend.ts b/postgres/backend.ts
index d1318c7..9927884 100644
--- a/postgres/backend.ts
+++ b/postgres/backend.ts
@@ -29,7 +29,7 @@ import type { PostgresBackendOptions, PostgresCapabilities } from "./types.js";
* @param sql - postgres.js SQL instance
* @param options - Backend configuration (tables, cleanup options)
* @returns LockBackend with server-side time authority
- * @see specs/postgres-backend.md
+ * @see docs/specs/postgres-backend.md
*
* @example
* ```typescript
diff --git a/postgres/operations/extend.ts b/postgres/operations/extend.ts
index 7fd0065..a68c369 100644
--- a/postgres/operations/extend.ts
+++ b/postgres/operations/extend.ts
@@ -37,9 +37,7 @@ import type { LockRow, PostgresConfig } from "../types.js";
* @param config - PostgreSQL backend configuration
* @returns Extend operation function
*
- * @see specs/interface.md#extend-operation-requirements - Normative TOCTOU requirements
- * @see specs/adrs.md ADR-003 - Explicit ownership verification rationale
- * @see specs/adrs.md ADR-010 - Authoritative expiresAtMs from mutations rationale
+ * @see docs/specs/interface.md#extend-operation-requirements - Normative TOCTOU requirements
*/
export function createExtendOperation(sql: Sql, config: PostgresConfig) {
return async (opts: LockOp & { ttlMs: number }): Promise => {
diff --git a/postgres/operations/is-locked.ts b/postgres/operations/is-locked.ts
index 5d0be5d..f3fb7f0 100644
--- a/postgres/operations/is-locked.ts
+++ b/postgres/operations/is-locked.ts
@@ -34,7 +34,7 @@ import type { LockRow, PostgresConfig } from "../types.js";
* @param config - PostgreSQL backend configuration
* @returns IsLocked operation function
*
- * @see specs/interface.md#islocked-operation-requirements - Normative requirements
+ * @see docs/specs/interface.md#islocked-operation-requirements - Normative requirements
*/
export function createIsLockedOperation(sql: Sql, config: PostgresConfig) {
return async (opts: KeyOp): Promise => {
diff --git a/postgres/operations/lookup.ts b/postgres/operations/lookup.ts
index f716a7c..45bd9d0 100644
--- a/postgres/operations/lookup.ts
+++ b/postgres/operations/lookup.ts
@@ -48,8 +48,7 @@ import type {
* @param config - PostgreSQL backend configuration
* @returns Lookup operation function
*
- * @see specs/interface.md#lookup-operation-requirements - Normative requirements
- * @see specs/adrs.md ADR-011 - Non-atomic lookup rationale
+ * @see docs/specs/interface.md#lookup-operation-requirements - Normative requirements
*/
export function createLookupOperation(sql: Sql, config: PostgresConfig) {
return async (
diff --git a/postgres/operations/release.ts b/postgres/operations/release.ts
index d8cbc64..88c8acf 100644
--- a/postgres/operations/release.ts
+++ b/postgres/operations/release.ts
@@ -34,8 +34,7 @@ import type { LockRow, PostgresConfig } from "../types.js";
* @param config - PostgreSQL backend configuration
* @returns Release operation function
*
- * @see specs/interface.md#release-operation-requirements - Normative TOCTOU requirements
- * @see specs/adrs.md ADR-003 - Explicit ownership verification rationale
+ * @see docs/specs/interface.md#release-operation-requirements - Normative TOCTOU requirements
*/
export function createReleaseOperation(sql: Sql, config: PostgresConfig) {
return async (opts: LockOp): Promise => {
diff --git a/redis/README.md b/redis/README.md
index e6f1e9f..529fdd3 100644
--- a/redis/README.md
+++ b/redis/README.md
@@ -115,6 +115,6 @@ redis-cli --scan --pattern "syncguard:fence:*" | wc -l
## Implementation References
-- **Specification**: See `specs/redis-backend.md` for complete implementation requirements
-- **Common Interface**: See `specs/interface.md` for shared LockBackend contract
-- **ADRs**: See `specs/adrs.md` for architectural decisions
+- **Specification**: See `docs/specs/redis-backend.md` for complete implementation requirements
+- **Common Interface**: See `docs/specs/interface.md` for shared LockBackend contract
+- **ADRs**: See `docs/adr/` for architectural decisions
diff --git a/redis/backend.ts b/redis/backend.ts
index e04aba0..dd47da5 100644
--- a/redis/backend.ts
+++ b/redis/backend.ts
@@ -69,7 +69,7 @@ interface RedisWithCommands extends Redis {
* @param redis - ioredis client instance
* @param options - Backend configuration (keyPrefix, ttl, tolerance)
* @returns LockBackend with server-side time authority
- * @see specs/redis-backend.md
+ * @see docs/specs/redis-backend.md
*/
export function createRedisBackend(
redis: Redis,
diff --git a/redis/config.ts b/redis/config.ts
index f06bc43..8baee9b 100644
--- a/redis/config.ts
+++ b/redis/config.ts
@@ -6,7 +6,7 @@ import type { RedisBackendOptions, RedisConfig } from "./types.js";
/**
* Default configuration for Redis backend.
- * @see specs/redis-backend.md
+ * @see docs/specs/redis-backend.md
*/
export const REDIS_DEFAULTS = {
/** Key prefix for Redis lock entries */
@@ -27,7 +27,7 @@ export function createRedisConfig(
const keyPrefix = options.keyPrefix ?? REDIS_DEFAULTS.keyPrefix;
// CRITICAL: Validate keyPrefix doesn't create namespace overlap with fence counters
- // Per specs/redis-backend.md: Cleanup MUST ONLY delete lock data keys, never fence counter keys
+ // Per docs/specs/redis-backend.md: Cleanup MUST ONLY delete lock data keys, never fence counter keys
// Fence keys use pattern: ${keyPrefix}:fence:*
// Lock data uses pattern: ${keyPrefix}:* (main) and ${keyPrefix}:id:* (index)
// This validation ensures fence keys are distinct from lock data keys
diff --git a/redis/errors.ts b/redis/errors.ts
index 7768314..64b026e 100644
--- a/redis/errors.ts
+++ b/redis/errors.ts
@@ -21,7 +21,7 @@ export function checkAborted(signal?: AbortSignal): void {
*
* @param error - Redis client error or string
* @returns LockError with appropriate code and context
- * @see specs/interface.md
+ * @see docs/specs/interface.md
*/
export function mapRedisError(error: any): LockError {
const errorMessage = error instanceof Error ? error.message : String(error);
diff --git a/redis/operations/extend.ts b/redis/operations/extend.ts
index 6a75a57..4b2bf9c 100644
--- a/redis/operations/extend.ts
+++ b/redis/operations/extend.ts
@@ -34,7 +34,7 @@ interface RedisWithCommands {
/**
* Creates extend operation that atomically renews lock TTL (replaces entirely, not additive).
- * @see specs/redis-backend.md
+ * @see docs/specs/redis-backend.md
*/
export function createExtendOperation(
redis: RedisWithCommands,
diff --git a/redis/scripts.ts b/redis/scripts.ts
index 606a80f..4ddf6f5 100644
--- a/redis/scripts.ts
+++ b/redis/scripts.ts
@@ -6,7 +6,7 @@
* Flow: check expiration → generate fence token → set both keys with identical TTL
*
* @returns {1, fence, expiresAtMs} on success, 0 on contention
- * @see specs/redis-backend.md
+ * @see docs/specs/redis-backend.md
*
* KEYS: [lockKey, lockIdKey, fenceKey]
* ARGV: [lockId, ttlMs, toleranceMs, storageKey, userKey]
@@ -51,7 +51,8 @@ return {1, fence, expiresAtMs}
* Flow: reverse lookup → verify ownership → atomic delete
*
* @returns 1=success, 0=ownership mismatch, -1=not found, -2=expired
- * @see specs/adrs.md (ADR-003: ownership verification, ADR-013: index retrieval)
+ * @see docs/adr/003-explicit-ownership-verification.md - Ownership verification
+ * @see docs/adr/013-full-storage-key-in-index.md - Index retrieval
*
* KEYS: [lockIdKey]
* ARGV: [lockId, toleranceMs]
@@ -92,7 +93,8 @@ return 1
* Flow: reverse lookup → verify ownership → replace TTL entirely (not additive)
*
* @returns {1, newExpiresAtMs} on success, 0 on ownership mismatch/not found/expired
- * @see specs/adrs.md (ADR-003: ownership verification, ADR-013: index retrieval)
+ * @see docs/adr/003-explicit-ownership-verification.md - Ownership verification
+ * @see docs/adr/013-full-storage-key-in-index.md - Index retrieval
*
* KEYS: [lockIdKey]
* ARGV: [lockId, toleranceMs, ttlMs]
@@ -137,7 +139,7 @@ return {1, newExpiresAtMs}
* Cleanup uses 2s safety buffer to prevent extend() race conditions.
*
* @returns 1 if locked and live, 0 otherwise
- * @see specs/redis-backend.md
+ * @see docs/specs/redis-backend.md
*
* KEYS: [lockKey]
* ARGV: [keyPrefix, toleranceMs, enableCleanup ("true"|"false")]
@@ -184,7 +186,7 @@ return 1
* Lookup lock by key, returns info only if live.
*
* @returns lock info (JSON) if live, nil otherwise
- * @see specs/interface.md
+ * @see docs/specs/interface.md
*
* KEYS: [lockKey]
* ARGV: [toleranceMs]
@@ -218,8 +220,7 @@ return cjson.encode(data)
* ONLY—correctness relies on atomic release/extend operations, NOT lookup results.
*
* @returns lock info (JSON) if live and owned, nil otherwise
- * @see specs/interface.md
- * @see specs/adrs.md (ADR-011: atomicity, ADR-013: index retrieval)
+ * @see docs/specs/interface.md
*
* KEYS: [lockIdKey]
* ARGV: [lockId, toleranceMs]
diff --git a/specs/README.md b/specs/README.md
deleted file mode 100644
index 5b829c0..0000000
--- a/specs/README.md
+++ /dev/null
@@ -1,120 +0,0 @@
-# SyncGuard Specifications
-
-Technical specifications for SyncGuard's distributed lock system.
-
-## Reading Order
-
-For best understanding, read specifications in this order:
-
-1. **[interface.md](./interface.md)** — Core LockBackend contract, types, and common requirements
-2. Backend-specific deltas (extend core contract with implementation details):
- - **[redis-backend.md](./redis-backend.md)** — Redis implementation with Lua scripts
- - **[postgres-backend.md](./postgres-backend.md)** — PostgreSQL implementation with transactions
- - **[firestore-backend.md](./firestore-backend.md)** — Firestore implementation with transactions
-3. **[adrs.md](./adrs.md)** — Architecture decisions (optional, historical context)
-
-## Quick Reference
-
-| Need | File |
-| ----------------------- | ------------------------------------------------ |
-| API contract | `interface.md` |
-| Required diagnostic API | `interface.md` → Lookup Operation |
-| Error handling | `interface.md` → Error Handling Standards |
-| Redis patterns | `redis-backend.md` → Lua scripts, key schema |
-| PostgreSQL patterns | `postgres-backend.md` → Transactions, tables |
-| Firestore patterns | `firestore-backend.md` → Transactions, documents |
-| Architectural decisions | `adrs.md` |
-
-## Implementation Checklist
-
-When building a new backend:
-
-1. Implement all core operations including required `lookup()`
-2. Use atomic operations for all mutations
-3. Follow error classification from `interface.md`
-4. Support key-based and lockId-based `lookup()` queries
-5. Document storage limits and TTL semantics
-6. Use unified `isLive()` predicate from `common/time-predicates.ts`
-7. Implement TOCTOU protection for release/extend operations
-
-## Specification Structure
-
-All specifications use a **normative vs rationale** pattern to reduce cognitive load and improve machine readability:
-
-### Document Pattern
-
-Each major section follows this structure:
-
-```markdown
-## Topic
-
-### Requirements
-
-[All MUST/SHOULD/MAY/NEVER statements - pure contract, parseable by agents]
-
-### Rationale & Notes
-
-[Background, design decisions, tradeoffs, operational guidance]
-```
-
-### Benefits for AI Agents
-
-- **Reduced cognitive load**: Agents can extract normative contract bits quickly without parsing explanatory text
-- **Clear boundaries**: No mixing of requirements and rationale
-- **Machine-parseable**: MUST/SHOULD/MAY/NEVER keywords appear only in Requirements sections
-- **Better maintainability**: Updates go to the correct section
-
-### Reading Strategies
-
-**For coding agents** (Claude Code, Gemini CLI, Codex CLI):
-
-- Focus on **Requirements** sections for implementation contracts
-- Use **Rationale & Notes** sections only when understanding "why" helps solve ambiguity
-
-**For humans**:
-
-- Read both sections together for complete understanding
-- Requirements provide the "what", Rationale provides the "why"
-
-## Specification Keywords
-
-- **MUST** — Required for correctness or contract compliance
-- **SHOULD** — Strongly recommended unless valid reason to deviate
-- **MAY** — Optional, implementation-specific choice
-- **NEVER** — Explicitly forbidden (safety or correctness violation)
-
-**Important**: These keywords appear ONLY in Requirements sections, never in Rationale & Notes sections.
-
-## Architecture Decision Records (ADRs)
-
-ADRs document **why** decisions were made, not **how** to implement them. They focus on design rationale, tradeoffs, and alternatives—not implementation details.
-
-### Content Separation
-
-| Content Type | Belongs In | ADRs contain |
-| ------------------------------ | --------------------------- | -------------------------------- |
-| Requirements (MUST/SHOULD/MAY) | interface.md, backend specs | Rationale only |
-| Implementation details | interface.md, backend specs | Cross-references |
-| **Design decisions** | **adrs.md** | Context, rationale, consequences |
-
-### Key Principle
-
-**ADRs explain WHY. Specifications define WHAT.**
-
-See [adrs.md](./adrs.md) for the complete ADR template, writing guidelines, and examples.
-
-## Backend Delta Pattern
-
-Backend specs (`redis-backend.md`, `postgres-backend.md`, `firestore-backend.md`) extend the core interface specification (`interface.md`) with implementation-specific details. To enhance machine-parseability and prevent agent drift (per ADR-012), backend specs:
-
-**MUST restate** key inherited requirements as explicit MUST/SHOULD bullets in their operation requirement sections, with cross-references to the rationale in `interface.md` and ADRs (e.g., "see ADR-010 for rationale").
-
-**MUST document** backend-specific implementation details:
-
-- Storage schema and key design
-- Atomic operation implementation (Lua scripts, transactions)
-- Backend-specific error mapping
-- Performance characteristics and limits
-- TTL/expiration semantics
-
-This pattern ensures agents can verify compliance from backend-specific operation tables alone, without requiring complex cross-referencing during implementation validation.
diff --git a/specs/adrs.md b/specs/adrs.md
deleted file mode 100644
index c7c956a..0000000
--- a/specs/adrs.md
+++ /dev/null
@@ -1,719 +0,0 @@
-# Architecture Decision Records
-
-This document contains architectural decisions made during the development of SyncGuard. These records document key design choices, their rationale, and consequences.
-
-**Date format:** All dates use `YYYY-MM` format, reflecting when the decision was accepted.
-
----
-
-## Writing ADRs
-
-### Structure Template
-
-```markdown
-## ADR-NNN: Decision Title
-
-**Date:** YYYY-MM
-**Status:** Accepted
-
-**Context**: [Problem being solved, constraints, prior approach, why change was needed]
-
-**Decision**: [What was decided - high-level requirement or design choice]
-
-**Rationale**:
-
-[Structured explanation of WHY this decision was made:]
-
-- **Why [aspect]**: [Design reasoning, tradeoffs, impact]
-- **Alternatives considered and rejected**: [What was evaluated but not chosen, and why]
-
-**Consequences**:
-
-- **Breaking changes**: [If any, with justification]
-- **Impact areas**: [Where normative requirements are documented]
-- **Cross-references**: [Links to interface.md, backend specs with section anchors]
-```
-
-### What Belongs in ADRs vs Specifications
-
-| Content Type | Belongs In | Example |
-| ------------------------- | -------------------------------- | ------------------------------------ |
-| MUST/SHOULD requirements | interface.md, backend specs | "MUST use 15-digit format" |
-| Implementation algorithms | interface.md (with anchor links) | `makeStorageKey()` specification |
-| Script signatures/formats | Backend specs (redis-backend.md) | Lua script KEYS/ARGV details |
-| Type definitions | interface.md, backend specs | `type Fence = string` |
-| **Decision rationale** | **ADRs** | Why 15 digits vs 19 digits |
-| **Design tradeoffs** | **ADRs** | Precision safety vs capacity |
-| **Alternatives rejected** | **ADRs** | Why not BigInt format |
-| **Problem context** | **ADRs** | What bug/limitation triggered change |
-
-### Writing Guidelines
-
-**DO:**
-
-- Explain **why** the decision was made and **what problem** it solves
-- Include **alternatives considered** with reasons for rejection
-- Reference normative specifications for implementation details
-- Use structured rationale with clear subsections (e.g., "Why X matters:", "Why Y was insufficient:")
-- Document tradeoffs explicitly
-- Keep consequences focused on impact areas and cross-references
-
-**DON'T:**
-
-- Repeat implementation details already in interface.md or backend specs
-- Include code snippets unless illustrating a concept (not normative)
-- Mix requirements (MUST/SHOULD) with rationale prose
-- Provide step-by-step implementation instructions
-- Duplicate algorithm specifications
-
-### Example: Good vs Bad ADR Content
-
-**❌ Bad (Too implementation-heavy):**
-
-```markdown
-**Decision**: Use 15-digit fence format.
-
-**Implementation**:
-
-- Redis: `string.format("%015d", redis.call('INCR', fenceKey))`
-- TypeScript: `String(n).padStart(15, '0')`
-- Overflow: throw when fence > 999999999999999
-```
-
-**✅ Good (Rationale-focused):**
-
-```markdown
-**Decision**: Use 15-digit fence format for guaranteed precision safety.
-
-**Rationale**:
-
-**Why 15 digits specifically:**
-
-- Stays within Lua's 53-bit IEEE 754 precision (2^53-1 ≈ 9.007e15)
-- Provides 10^15 capacity = ~31.7 years at 1M locks/sec
-- Zero rounding risk across all platforms
-
-**Alternatives considered:**
-
-- 19-digit format: Exceeds Lua precision, would break monotonicity
-- BigInt format: Not JSON-safe, poor cross-language support
-
-**Consequences**:
-
-- See interface.md §Fence Token Format for normative specification
-- See redis-backend.md for Lua implementation details
-```
-
----
-
-## ADR-003: Explicit Ownership Re-Verification in Mutations
-
-**Date:** 2025-09
-**Status:** Accepted
-**Context**: Backend implementations perform release/extend operations by reverse mapping `lockId → key` then querying/mutating the lock. While the atomic transaction/script pattern already provides TOCTOU protection, explicit ownership verification adds defense-in-depth.
-
-**Decision**: ALL backends MUST perform explicit ownership verification after reverse mapping lookup:
-
-```typescript
-// After fetching document via reverse mapping
-if (data?.lockId !== lockId) {
- return { ok: false, reason: "not-found" };
-}
-```
-
-**Rationale**:
-
-- **Defense-in-depth**: Additional safety layer with negligible performance cost
-- **Cross-backend consistency**: Ensures Redis and Firestore implement identical ownership checking
-- **TOCTOU protection**: Guards against any edge cases in the atomic resolve→validate→mutate flow
-- **Code clarity**: Makes ownership verification explicit rather than implicit in the transaction logic
-
-**Consequences**:
-
-- Backends must implement explicit verification (not just index trust)
-- Cross-backend consistency ensures Redis and Firestore handle edge cases identically
-- Protection against rare but catastrophic wrong-lock mutations
-- Documentation of security-critical decision for future maintainers
-
-## ADR-004: Lexicographic Fence Comparison
-
-**Date:** 2025-09
-**Status:** Accepted
-
-**Context**: The original fence design claimed tokens were "opaque" while simultaneously mandating specific formatting and shipping comparison helper functions. This contradiction increased API surface area and created potential for misuse. More critically, the initial 19-digit format created a **precision safety issue** in Redis Lua implementations: Lua numbers use IEEE 754 doubles with ~53 bits of mantissa precision (≈15-16 exact decimal digits). Fence values exceeding 2^53-1 (~9e15) would suffer precision loss, breaking monotonicity guarantees—the core correctness property of fencing tokens.
-
-**Decision**: Fence tokens are **fixed-width decimal strings with lexicographic ordering**, using a **15-digit format** for guaranteed precision safety.
-
-**Rationale**:
-
-**Why strings over numbers:**
-
-- **Simplest possible API**: Direct string comparison (`fenceA > fenceB`) eliminates need for helper functions
-- **Intuitive developer experience**: String comparison matches expectations for ordered values
-- **JSON-safe**: Strings serialize naturally without BigInt precision issues
-- **Cross-language compatible**: All languages support lexicographic string comparison
-- **Eliminates contradictions**: No "opaque" claims while mandating specific formats
-
-**Why 15 digits specifically:**
-
-- **Precision safety**: Stays well within Lua's 53-bit IEEE 754 precision limit (2^53-1 ≈ 9.007e15)
-- **Practical capacity**: 10^15 operations = ~31.7 years at 1M locks/sec (ample for production use)
-- **Zero rounding risk**: Guarantees exact integer representation in IEEE 754 doubles across all platforms
-- **Correctness over optimization**: Aligns with project principle "prioritize correctness and safety over micro-optimizations"
-
-**Why fixed-width zero-padding:**
-
-- **Reliable ordering**: "000000000000002" > "000000000000001" without parsing
-- **Cross-backend consistency**: All backends produce identical formats for same fence value
-- **Deterministic behavior**: String comparison = chronological comparison, always
-
-**Alternatives considered and rejected:**
-
-- **BigInt format**: Not JSON-safe, poor cross-language support
-- **19-digit format**: Exceeds Lua precision limits, would break monotonicity
-- **Variable-width strings**: Lexicographic comparison fails ("9" > "10")
-- **Helper functions for comparison**: Unnecessary complexity when strings work natively
-
-**Consequences**:
-
-- **Breaking change**: Existing fence values incompatible (acceptable pre-1.0)
-- **Simpler public API**: Remove `compareFence()` and `isNewerFence()` helpers
-- **Simplified documentation**: One comparison rule replaces complex usage patterns
-- **Backend contract**: All backends must return identical 15-digit zero-padded format (see interface.md Fence Token Format for normative specification)
-- **Overflow handling**: Backends enforce `FENCE_THRESHOLDS.MAX` internally with warnings at `FENCE_THRESHOLDS.WARN` (see common/constants.ts)
-- **Cleanup safety**: Fence counters must never be deleted during cleanup operations (only lock data)
-- **Cross-references**: See interface.md for normative fence format requirements
-
-## ADR-005: Unified Time Tolerance
-
-**Date:** 2025-09
-**Status:** Accepted
-**Context**: The original `timeMode` design created inconsistent semantics across backends: `timeMode: "strict"` meant 0ms tolerance on Redis (server-time) but 1000ms tolerance on Firestore (client-time minimum safe). This violated the principle of predictable cross-backend behavior and created operational risks when switching backends.
-
-**Decision**: Remove `timeMode` configuration entirely and use unified tolerance across all backends:
-
-- **Single tolerance**: See `TIME_TOLERANCE_MS` in interface.md (normative definition)
-- **Consistent behavior**: Same configuration produces identical liveness semantics
-- **No modes**: Remove `timeMode` from capabilities and configuration
-- **Cross-backend portability**: Backend switching preserves lock behavior
-
-**Rationale**:
-
-- **Eliminates confusion**: "Strict" mode was misleading - didn't mean the same thing across backends
-- **Predictable behavior**: Users can reason about lock liveness without backend-specific knowledge
-- **Operational safety**: Backend migration doesn't change lock semantics subtly
-- **Testing simplicity**: Cross-backend tests work without tolerance adjustments
-- **Realistic precision**: See `TIME_TOLERANCE_MS` rationale in interface.md
-
-**Consequences**:
-
-- Remove `timeMode` from `BackendCapabilities` interface
-- Establish `TIME_TOLERANCE_MS` in interface.md as single normative source
-- Update Redis and Firestore specs to reference interface.md constant
-- Simplify backend implementation - no conditional tolerance mapping
-- Cross-backend consistency tests become straightforward
-- Documentation focuses on time authority differences, not tolerance modes
-
-## ADR-006: Mandatory Uniform Key Truncation
-
-**Date:** 2025-09
-**Status:** Accepted
-**Context**: Original specs allowed backends to either truncate or throw when prefixed storage keys exceeded backend limits, creating inconsistent cross-backend behavior. This made the library unpredictable and difficult to test, as the same user key could produce different outcomes on different backends.
-
-**Decision**: Make truncation **mandatory** when `prefix:userKey` exceeds backend storage limits:
-
-- **Mandatory truncation**: All backends MUST apply standardized hash-truncation when prefixed keys exceed limits
-- **Throw only as last resort**: `InvalidArgument` only when even truncated form exceeds absolute backend limits
-- **Canonical algorithm**: See [Standardized Storage Key Generation](interface.md#storage-key-generation) in interface.md for the normative `makeStorageKey()` specification
-- **Universal application**: Applies to main lock keys, reverse index keys, and fence counter keys
-- **Fence key consistency**: See [Two-Step Fence Key Derivation Pattern](interface.md#fence-key-derivation) in interface.md for the normative fence key generation specification
-
-**Rationale**:
-
-- **Predictable behavior**: Same user key produces same outcome across all backends
-- **Testable**: Cross-backend tests work without special-casing backend limits
-- **Composable**: Applications can rely on uniform truncation behavior
-- **Safe**: Maintains DoS protection while ensuring consistency
-- **Simple**: Eliminates "either/or" complexity in backend implementations
-- **1:1 fence mapping**: Guarantees each distinct user key maps to a unique fence counter
-- **Single normative source**: interface.md defines the canonical algorithm; ADRs provide rationale only
-
-**Consequences**:
-
-- Remove "either throw or truncate" language from all backend specs
-- Create common `makeStorageKey()` helper implementing the interface.md specification
-- Update backend specs to reference interface.md rather than repeating algorithm details
-- Simplify cross-backend testing (no need to handle different outcomes)
-- Ensure all key types (main, index, fence) use identical truncation logic with proper composition
-- Backend specs maintain only backend-specific byte limits (e.g., 1500 for Firestore, 1000 for Redis)
-
-## ADR-007: Opt-In Telemetry
-
-**Date:** 2025-09
-**Status:** Accepted
-**Context**: Original specification mandated telemetry for all operations, requiring backends to compute hashes and emit events even when no consumer existed. This created unnecessary overhead, complicated the core API with redaction policies, and made testing more difficult due to side effects in every operation.
-
-**Decision**: Make telemetry **opt-in** via a decorator pattern:
-
-- **Telemetry OFF by default**: Backends track cheap internal details but don't compute hashes or emit events
-- **Decorator pattern**: `withTelemetry(backend, options)` wraps backends to add observability
-- **Simplified lookup**: `lookup()` always returns sanitized data; `getByKeyRaw()`/`getByIdRaw()` provide raw access
-- **Unified redaction**: Single `includeRaw` option in decorator, no per-call overrides
-- **Async isolation**: Event callbacks never block operations or propagate errors
-
-**Rationale**:
-
-- **Zero-cost abstraction**: No performance impact when telemetry disabled
-- **Cleaner separation**: Core backends focus on correctness; telemetry is a composable layer
-- **Simpler API**: Removes `includeRaw` from core config and per-call parameters
-- **Better testing**: Pure functions without side effects by default
-- **Tree-shakable**: Applications without telemetry can exclude the decorator entirely
-
-**Consequences**:
-
-- **Breaking change**: `onEvent` in `LockConfig` deprecated; use decorator instead
-- **Migration required**: Applications using telemetry must wrap backends
-- **New helpers**: Add `withTelemetry()` decorator and `getByKeyRaw()`/`getByIdRaw()` functions
-- **Simplified backends**: Remove hash computation and event emission from core operations
-- **Documentation update**: Clearly mark telemetry as optional feature
-
-## ADR-008: Compile-Time Fencing Contract
-
-**Date:** 2025-10
-**Status:** Accepted
-**Context**: The specification claimed that with Redis/Firestore "TypeScript knows fence exists," yet the type system required optional fence fields, forcing runtime assertions (`expectFence`/`hasFence`) even when backends guaranteed fencing support. This created unnecessary boilerplate and contradicted the promised ergonomics.
-
-**Decision**: Parameterize result types by capabilities so fence is **required** when `supportsFencing: true`:
-
-- **Type-level guarantee**: `AcquireResult` includes required `fence` when `C['supportsFencing'] extends true`
-- **No runtime assertions**: Direct access to `result.fence` for fencing backends
-- **Simplified helpers**: Keep only `hasFence()` for generic code accepting unknown backends
-- **v1 scope**: All bundled backends (Redis, Firestore) provide fencing; non-fencing backends out of scope
-
-**Implementation**:
-
-```typescript
-type AcquireOk = {
- ok: true;
- lockId: string;
- expiresAtMs: number;
-} & (C["supportsFencing"] extends true ? { fence: Fence } : {});
-
-type AcquireResult =
- | AcquireOk
- | { ok: false; reason: "locked"; retryAfterMs?: number };
-
-interface LockBackend {
- acquire(opts: KeyOp & { ttlMs: number }): Promise>;
- // ...
-}
-```
-
-**Rationale**:
-
-- **Zero boilerplate**: Fencing backends provide fence at compile-time, no assertions needed
-- **Type safety**: TypeScript prevents accessing fence on non-fencing backends
-- **Cleaner API**: Removes `expectFence()` and `supportsFencing()` helpers from public API
-- **Delivers promise**: Matches ergonomic examples in backend documentation
-- **Forward compatible**: Capabilities field remains for potential future non-fencing adapters
-
-**Consequences**:
-
-- **Breaking change**: `AcquireResult` becomes generic, parameterized by capabilities
-- **Improved DX**: Direct `result.fence` access for Redis/Firestore backends
-- **Simplified surface**: Remove two helper functions from public API
-- **Documentation update**: Show direct fence access in all examples
-- **Migration**: Applications using `expectFence()` can remove it; `hasFence()` remains for generic code
-
-## ADR-009: Retries Live in Helpers, Core Backends are Single-Attempt
-
-**Date:** 2025-10
-**Status:** Accepted
-**Context**: Users expect transparent retry on contention, but we want to keep backends minimal and composable. The initial spec included retry configuration in core constants, creating confusion about where retry logic lives.
-
-**Decision**:
-
-- **`lock()` helper handles all retry logic** and is the primary export
-- **Backends perform single-attempt operations only** - no retry logic in backends
-- **Split constants**: `BACKEND_DEFAULTS` (ttlMs only) from `LOCK_DEFAULTS` (retry config)
-- **Default retry strategy**: Exponential backoff with equal jitter (50% randomization)
-- **Removed `retryAfterMs`** field - no current backends can provide meaningful hints
-
-**Rationale**:
-
-- **Clear layering**: Backends stay minimal, helpers add smart behavior
-- **Predictable API**: Single-attempt semantics at backend level
-- **Composable**: Users can build custom retry strategies if needed
-- **No dead fields**: Removing `retryAfterMs` simplifies the interface
-- **Discoverable**: Making `lock()` primary export guides users to the happy path
-
-**Consequences**:
-
-- **Breaking change**: Remove `retryAfterMs` from `AcquireResult`
-- **Smaller core API**: Backends have simpler contract
-- **Easier onboarding**: Users discover `lock()` first
-- **Clearer responsibilities**: Retry logic centralized in helper
-- **Simpler test matrix**: Backend tests don't need retry scenarios
-
-## ADR-010: Authoritative ExpiresAtMs from Mutations
-
-**Date:** 2025-10
-**Status:** Accepted
-
-**Context**: Originally, Redis acquire/extend Lua scripts returned only success indicators and fence tokens, forcing the TypeScript wrapper to approximate `expiresAtMs` using client-side calculations (`Date.now() + ttlMs`). This created two critical problems:
-
-1. **Time authority inconsistency**: Redis uses server time for all lock operations and liveness checks, but expiresAtMs was computed from client time, creating subtle drift between authoritative state and reported expiry
-2. **Heartbeat scheduling inaccuracy**: Callers scheduling extend operations based on approximate expiry could miss the window (extending too late) or waste resources (extending unnecessarily early), especially with clock skew
-
-This violated the principle that timestamps should originate from the backend's designated time authority.
-
-**Decision**: All backend mutation operations (acquire, extend) MUST return authoritative `expiresAtMs` computed from the backend's designated time source—no client-side approximation permitted.
-
-**Rationale**:
-
-**Why time authority consistency matters:**
-
-- **Single source of truth**: All timestamps (stored expiry, returned expiry, liveness checks) originate from the same authoritative clock
-- **Eliminates skew-induced bugs**: Client clock drift doesn't create divergence between "what the backend thinks" and "what the client reports"
-- **Predictable semantics**: `expiresAtMs` always reflects the backend's view of expiration, matching liveness predicate behavior
-
-**Why approximation is insufficient:**
-
-- **Accumulating drift**: Repeated client-side calculations compound errors over time
-- **Clock skew sensitivity**: Client/server clock differences make approximations unreliable
-- **Debugging complexity**: Discrepancies between reported and actual expiry complicate troubleshooting
-
-**Why heartbeat scheduling needs precision:**
-
-- **Auto-extend patterns**: Reliable heartbeating requires knowing exact server-time expiry
-- **Avoid premature extension**: Extending too early wastes backend round-trips
-- **Avoid missed windows**: Extending too late risks lock expiration and loss of ownership
-
-**Why minimal overhead:**
-
-- **Trivial cost**: Adding one number to return payload (8 bytes) has negligible impact
-- **Already available**: Backends computing expiry for storage can return it at no extra cost
-- **Composability win**: Enables higher-level patterns (auto-extend, adaptive heartbeats) without compromise
-
-**Alternatives considered and rejected:**
-
-- **Client-side approximation with tolerance buffer**: Still suffers from drift; band-aids the problem
-- **Separate getExpiry() operation**: Extra round-trip overhead; doesn't solve scheduling race
-- **Backend-neutral timestamps**: Impossible—time authority differs by backend design
-
-**Consequences**:
-
-- **Time authority requirement**: Documented in interface.md Time Authority
-- **Backend compliance**: All backends must return authoritative expiresAtMs from mutations (see redis-backend.md and firestore-backend.md operation specs)
-- **TypeScript wrappers updated**: Parse and validate returned expiresAtMs with robustness checks
-- **Test coverage**: Unit tests verify no client-side approximation; integration tests verify heartbeat accuracy
-- **Cross-references**: See interface.md for normative authoritative expiresAtMs requirement
-
-## ADR-011: Relaxed Atomicity for Diagnostic Lookup
-
-**Date:** 2025-10
-**Status:** Accepted
-**Context**: The original interface.md specification stated that ownership-mode lookup (`lookup({ lockId })`) MUST be atomic to prevent TOCTOU races. However, this requirement was inconsistent across backends:
-
-- **interface.md**: Required atomicity ("MUST use atomic script/transaction")
-- **redis-backend.md**: Correctly implemented atomic Lua script for multi-key reads (index + main lock)
-- **firestore-backend.md**: Described non-atomic indexed query as "inherently safe" without transaction
-
-This inconsistency created confusion and incompatible guarantees, despite the spec explicitly stating that lookup is diagnostic-only and NOT a correctness guard for mutations.
-
-**Decision**: Relax the atomicity requirement to match the diagnostic nature of lookup:
-
-- **SHOULD be atomic** for stores requiring multi-key reads (e.g., Redis via Lua script)
-- **MAY be non-atomic** for indexed stores with post-read ownership verification (e.g., Firestore single indexed query)
-- **Reinforce diagnostic nature**: Add strong warning that lookup is for diagnostics/UI/monitoring ONLY—NEVER use to gate release/extend operations
-
-**Rationale**:
-
-- **Aligns with pragmatic safety**: Strict atomicity unneeded for read-only diagnostic operations
-- **Reflects actual correctness model**: Release/extend provide atomic TOCTOU protection; lookup is for observation only
-- **Improves consistency**: Removes spec contradiction between common interface and backend implementations
-- **Simplifies Firestore**: Single indexed query is natural and efficient; transaction overhead unnecessary
-- **Preserves Redis strength**: Atomic Lua script remains correct and efficient for multi-key pattern
-- **Clear guidance**: "SHOULD/MAY" based on backend characteristics provides implementation flexibility
-
-**Consequences**:
-
-- **Specification updates**:
- - interface.md: Change "MUST be atomic" → "SHOULD be atomic for multi-key stores, MAY use single indexed query for indexed stores"
- - interface.md: Enhance diagnostic warning with explicit cross-reference to atomic release/extend operations
- - firestore-backend.md: Reference relaxed rule and clarify non-atomic is acceptable for diagnostic use
-- **No implementation changes**: Current Redis (atomic) and Firestore (non-atomic) implementations already correct
-- **Testing enhancement**: Add cross-backend test for "lookup expired lock by lockId returns null consistently" (ensures portability without over-testing races)
-- **Documentation clarity**: Makes explicit that lookup atomicity is implementation detail, not correctness requirement
-
-## ADR-012: Explicit Restatement of Authoritative expiresAtMs in Backend Specs
-
-**Date:** 2025-10
-**Status:** Accepted
-**Context**: ADR-010 and interface.md established that acquire/extend operations MUST return authoritative `expiresAtMs` from the backend's time authority (no client-side approximation). However, the top-level operation requirement tables in redis-backend.md and firestore-backend.md didn't explicitly restate this as a bold **MUST** bullet, making it easy for agents to miss during compliance checks.
-
-**Decision**: Backend specifications MUST restate key inherited requirements in operation sections:
-
-- **Add explicit MUST bullets** to Acquire and Extend operation requirements in both backend specs
-- **Reference ADR-010** for rationale to avoid redundant prose
-- **Update interface.md** with Backend Delta Pattern guidance: backend specs MUST restate inherited requirements for agent parseability
-
-**Rationale**:
-
-- **Machine-parseability**: Agents can verify compliance from backend-specific operation tables without cross-referencing interface.md
-- **Prevents drift**: Explicit restatements reduce risk of agents missing critical requirements
-- **Minimal redundancy**: Cross-references to ADR-010/interface.md provide "why" without repeating rationale
-- **Consistency**: Follows normative/rationale pattern (tables = normative, ADRs = rationale)
-
-**Consequences**:
-
-- **redis-backend.md updated**: Added "**MUST return authoritative expiresAtMs**" bullets to Acquire and Extend sections
-- **firestore-backend.md updated**: Added "**MUST return authoritative expiresAtMs**" bullets to Acquire and Extend sections
-- **interface.md updated**: Added Backend Delta Pattern section explaining restatement requirement
-- **Future backend implementations**: Must follow this pattern for all inherited requirements
-- **Testing enhancement**: Can add cross-backend tests to verify "Acquire/Extend returns expiresAtMs from authority (no approximation)" using mocked time sources
-
-## ADR-013: Store Full Storage Key in Reverse Index
-
-**Date:** 2025-10
-**Status:** Accepted
-
-**Context**: The Redis backend's reverse mapping logic contained a **correctness bug** when key truncation occurred. Per ADR-006, `makeStorageKey()` hashes and truncates long prefixed keys to a 22-character base64url string when they exceed the backend's storage limit. However, the acquire script stored the **original user key** in the reverse index, while release/extend scripts **reconstructed** the main lock key by concatenating `{prefix}:{originalKey}`.
-
-This created a critical mismatch when truncation occurred:
-
-- **During acquire**: Main lock key is truncated form (e.g., `syncguard:<22-char-hash>`)
-- **During release/extend**: Reconstruction uses original key (e.g., `syncguard:`), which doesn't match
-
-**Result**: Release/extend operations would fail to find the lock (returning "not found") or, in worst cases, target an unrelated key. This violated TOCTOU protection and ownership verification (ADR-003), breaking the core correctness guarantee for mutations.
-
-Truncation triggers when `len(prefix + ':' + userKey) + 26 > 1000` bytes (roughly when `len(prefix) > 461` bytes with a 512-byte user key). While uncommon with the default prefix ("syncguard"), it's possible with custom namespaces, making this a latent safety issue.
-
-**Decision**: The reverse index MUST store the full computed storage key (post-truncation), not the original user key. Eliminate key reconstruction entirely.
-
-**Rationale**:
-
-**Why reconstruction was fundamentally broken:**
-
-- **Mismatch under truncation**: Original key reconstruction produces different result than truncated key
-- **Silent failure**: Bug only manifests with long prefixes/keys, making it hard to catch in typical testing
-- **Violates TOCTOU protection**: Operations target wrong key, bypassing atomic verification guarantees
-- **Composability failure**: Valid configurations (long prefix + max key) produced incorrect behavior
-
-**Why storing full storage key fixes it:**
-
-- **Eliminates reconstruction**: No string concatenation, no mismatch possible
-- **Consistency guarantee**: Index always returns exactly the key used during acquire
-- **Works under all conditions**: Truncated or not, index lookup returns correct target
-- **Defense-in-depth**: Even if truncation logic changes, reverse index remains correct
-
-**Why minimal overhead:**
-
-- **Storage cost**: Negligible—Redis values handle 1000-byte strings efficiently
-- **Performance cost**: None—GET operation works identically regardless of value length
-- **Complexity reduction**: Removing reconstruction logic simplifies scripts
-
-**Why testability matters:**
-
-- **Previously untestable**: Hard to simulate truncation without long prefixes in tests
-- **Now verifiable**: Unit tests can use long prefix + max key to exercise truncation path
-- **Regression prevention**: Tests ensure future changes don't reintroduce bug
-
-**Alternatives considered and rejected:**
-
-- **Fix reconstruction logic**: Still fragile; any future truncation changes risk re-breaking
-- **Disable truncation for reverse index**: Doesn't solve mismatch; creates inconsistent key handling
-- **Separate truncation for index**: Complexity explosion; hard to reason about correctness
-
-**Consequences**:
-
-- **Breaking change**: Reverse index format changed (acceptable pre-1.0)
-- **Script simplification**: Remove all prefix reconstruction logic and `keyPrefix` parameter
-- **Redis implementation updated**: Acquire stores full lockKey; release/extend/lookup retrieve it directly (see redis-backend.md for script specifications)
-- **Test coverage**: Added `test/unit/redis-truncation-correctness.test.ts` to verify truncation handling
-- **No data migration**: Existing locks in production expire naturally or need manual cleanup
-- **Cross-references**: See interface.md Standardized Storage Key Generation for truncation algorithm
-
-## ADR-014: Defensive Detection of Duplicate LockId Documents (Firestore)
-
-**Date:** 2025-10
-**Status:** Accepted
-**Context**: Firestore lacks database-level unique indexes on fields. The library queries locks by lockId using `where("lockId", "==", lockId).limit(1)`, relying on correct implementation to prevent duplicate documents with the same lockId. However, in real-world operations, bugs, race conditions during migrations, or manual interventions could create duplicates. If this occurs:
-
-- **Query ambiguity**: `.limit(1)` returns an arbitrary document when duplicates exist
-- **Ownership verification helps but isn't sufficient**: ADR-003's explicit verification prevents wrong-lock mutations, but doesn't address the underlying data inconsistency
-- **Observability blind spot**: Duplicates remain invisible without defensive checks, complicating debugging and cleanup
-- **State drift accumulation**: Without detection, duplicate documents could accumulate over time
-
-While this shouldn't happen in normal operation, defensive programming principles require handling operational foot-guns.
-
-**Decision**: Add defensive SHOULD requirement for Firestore operations that query by lockId:
-
-- **Query adjustment**: Remove `.limit(1)` from lockId queries to enable duplicate detection
-- **Detection**: When transaction reads return `querySnapshot.docs.length > 1`, treat as internal inconsistency
-- **Telemetry**: Log warning with key and lockId context (not error, since this is defensive)
-- **Safe cleanup**: MAY delete expired duplicate documents within the same transaction (NEVER delete live locks)
-- **Fail-safe mutation**: When duplicates detected and any are live, operations SHOULD return `{ ok: false }` to avoid mutating ambiguous state
-- **Scope**: Applies to release, extend, and lookup operations (acquire uses direct document access by key)
-- **Performance note**: Removing `.limit(1)` has negligible impact since Firestore uses indexed queries and duplicates shouldn't exist in normal operation
-
-**Rationale**:
-
-- **Defense-in-depth**: Catches data inconsistencies that shouldn't exist but might occur in production
-- **Operational visibility**: Telemetry provides early warning for investigation/cleanup
-- **Safety first**: Failing mutations on ambiguous state prevents cascading errors
-- **No false positives**: Detection only triggers on genuine duplicates (legitimate case: zero or one document)
-- **Minimal performance impact**: Removing `.limit(1)` adds negligible overhead since indexed queries are fast and duplicates are rare
-- **Composable cleanup**: Optional expired-document deletion reduces state drift without risking live locks
-- **Correct detection semantics**: `.limit(1)` would prevent detection by capping results at 1 document
-
-**Consequences**:
-
-- **Specification updates**: Add section in `firestore-backend.md` with SHOULD requirements
-- **Implementation**: See JSDoc comments in `firestore/operations/*.ts` for detection patterns
-- **Testing**: Integration tests SHOULD verify duplicate handling
-- **Backward compatibility**: SHOULD requirement allows gradual adoption
-
-## ADR-015: Async RAII for Locks
-
-**Date:** 2025-10
-**Status:** Accepted
-
-**Context**: Lock management requires careful cleanup on all code paths—including early returns, exceptions, and normal completion. Manual cleanup patterns (`try/finally`) are error-prone and verbose. JavaScript's `await using` syntax (AsyncDisposable, Node.js ≥20) provides RAII (Resource Acquisition Is Initialization) for automatic cleanup, but integrating it with the existing lock API required design decisions around error handling, signal propagation, and state management.
-
-**Decision**: Integrate AsyncDisposable support into all backend `acquire()` results, providing automatic lock release on scope exit:
-
-- **Automatic disposal**: All `AcquireResult` objects implement `Symbol.asyncDispose` for `await using` compatibility
-- **Two configuration patterns**: Backend-level callbacks for low-level API (`await using`), lock-level callbacks for high-level helper (`lock()`)
-- **Stateless handle design**: No local state tracking—delegate idempotency and ownership verification to backend
-- **Full signal support**: Handle methods (`release`, `extend`) accept optional `AbortSignal` for per-operation cancellation
-- **Error callback integration**: `onReleaseError` callback for disposal failures (never throws from disposal per spec)
-- **Type narrowing**: TypeScript's discriminated unions provide automatic narrowing after `if (lock.ok)` check
-
-**Rationale**:
-
-**Why AsyncDisposable integration:**
-
-- **Correctness guarantee**: Ensures cleanup on all code paths without manual try/finally
-- **Ergonomic API**: `await using` is concise and familiar to developers using modern JavaScript
-- **Error resilience**: Cleanup happens even when scope exits with exceptions
-- **Composable**: Works with both backend.acquire() (low-level) and lock() helper (high-level)
-
-**Why two configuration patterns:**
-
-- **Pattern A (backend-level)**: Configure `onReleaseError` once for all acquisitions—ideal for low-level `await using` API
-- **Pattern B (lock-level)**: Configure `onReleaseError` per-call—ideal for high-level `lock()` helper with fine-grained control
-- **Independence**: These serve different APIs, not meant to be mixed (choose based on usage pattern)
-- **No duplication**: Each pattern targets a specific use case
-
-**Why stateless handle design:**
-
-- **Race-free**: Eliminates potential race conditions from mutable `released` boolean flag
-- **Simpler code**: No local state to synchronize or reason about
-- **Trust backend**: Backend already provides atomic idempotency—don't duplicate checks
-- **Correctness over optimization**: Aligns with project principle; duplicate backend calls are rare and cheap
-
-**Why full signal support:**
-
-- **API consistency**: Handle methods mirror backend method signatures (all accept optional `signal`)
-- **Per-operation control**: Independent cancellation of different operations (release vs extend)
-- **Composability**: Enables advanced patterns like timeout-guarded releases
-- **Backward compatible**: Optional parameters don't break existing code
-
-**Why error callbacks never throw:**
-
-- **Disposal safety**: `Symbol.asyncDispose` must never throw per JavaScript spec
-- **Observable failures**: Callbacks provide visibility without disrupting cleanup
-- **Silent fallback**: Without callback, disposal errors are silently ignored (best-effort cleanup)
-
-**Why manual operations throw but disposal swallows:**
-
-- **API consistency**: `handle.release()` and `handle.extend()` behave identically to `backend.release()` and `backend.extend()` (both throw on system errors)
-- **RAII semantics**: Manual operations report errors for actionable handling; automatic disposal is best-effort cleanup
-- **Predictable behavior**: Users can rely on consistent error propagation across manual operations
-- **Safety**: System errors (network failures, auth errors) are visible and distinguishable from domain failures (lock not found)
-
-**Alternatives considered and rejected:**
-
-- **Implicit signal capture (Option B for Issue 1)**: Hidden state reduces flexibility and testability
-- **Mutable released flag with promise serialization (Option B for Issue 2)**: Over-engineering; adds complexity for marginal benefit
-- **Separate disposable wrapper type**: Extra API surface; violates "smallest possible API" principle
-- **Throwing from disposal**: Violates AsyncDisposable contract; masks original errors
-
-**Consequences**:
-
-- **Breaking changes**: None—AsyncDisposable is additive to existing API
-- **New exports**: `decorateAcquireResult()` and `acquireHandle()` in `common/disposable.ts`
-- **Backend integration**: All backends (Redis, Postgres, Firestore) call `decorateAcquireResult()` in acquire operations
-- **Type changes**: `AcquireResult` includes `Symbol.asyncDispose` on both success and failure results
-- **Error handling contract**: Manual `release()` and `extend()` throw on system errors (consistent with backend API); only `Symbol.asyncDispose` swallows errors and routes to `onReleaseError` callback
-- **Documentation**: See interface.md Resource Management section for normative specification
-- **Test coverage**: 24 disposal unit tests, 18 integration tests across all backends
-- **Cross-references**: See `common/disposable.ts` for implementation, `specs/interface.md` for usage examples
-
-## ADR-016: Opt-In Disposal Timeout
-
-**Date:** 2025-10
-**Status:** Accepted
-
-**Context**: The `Symbol.asyncDispose` method in disposable lock handles calls `release()` without any timeout or AbortSignal. If a backend's release operation hangs (e.g., network latency in Firestore/Redis, slow PostgreSQL query under load), disposal could block indefinitely. While backend clients should have their own timeouts (Redis socket timeout, PostgreSQL statement_timeout, Firestore client timeout), there was no mechanism to enforce disposal-specific timeout behavior independent of general backend timeouts.
-
-This creates a potential inconsistency: manual `release()` supports `AbortSignal` for cancellation, but automatic disposal (via `await using`) doesn't, leading to different cancellation behavior between explicit and automatic cleanup.
-
-**Decision**: Add **opt-in** `disposeTimeoutMs` configuration with no default:
-
-- **Opt-in configuration**: New `disposeTimeoutMs` field in `BackendConfig` interface (optional, no default value)
-- **Timeout mechanism**: When configured, disposal creates internal `AbortController` with `setTimeout`, passes signal to `release()`
-- **Error handling**: Timeout errors flow through existing `onReleaseError` callback with normalized error context
-- **Backend-agnostic**: Applies uniformly to Redis, PostgreSQL, and Firestore backends
-- **Manual operations unaffected**: Timeout only applies to automatic disposal; manual `release()` uses caller-provided signal
-
-**Rationale**:
-
-**Why opt-in (no default):**
-
-- **Pragmatic**: Keeps default behavior simple (status quo), adds safety only when users need it
-- **Minimal API growth**: Single optional field in existing config interface
-- **Avoids false timeouts**: No risk of premature timeout in slow but valid operations
-- **User choice**: High-reliability environments can enable it; others rely on backend-level timeouts
-
-**Why timeout disposal specifically:**
-
-- **Responsiveness guarantee**: Prevents indefinite hangs on scope exit in RAII pattern
-- **Consistent with signal support**: Uses existing `AbortSignal` infrastructure from ADR-015
-- **Observable failures**: Timeout errors reported via `onReleaseError` callback for visibility
-- **Defense-in-depth**: Additional safety layer when backend client timeouts insufficient
-
-**Why not global signal approach:**
-
-- **Too complex**: Requires users to manage global signals, easy to forget
-- **No per-lock granularity**: Cannot configure different timeouts for different lock types
-- **Error handling burden**: If signal aborts, error handling is user-dependent, potentially unlogged
-
-**Why not status quo:**
-
-- **Safety concern**: Legitimate risk of hangs in distributed systems with unreliable networks
-- **Inconsistent behavior**: Manual `release()` has cancellation support, automatic disposal doesn't
-- **Operational risk**: Silent hangs reduce observability and reliability in production
-
-**Why minimal complexity:**
-
-- **Reuses existing infrastructure**: `AbortController`, `onReleaseError`, backend signal support
-- **No new abstractions**: Timeout is implementation detail of disposal, not exposed API
-- **Clear semantics**: Timeout = abort disposal after N milliseconds, report via callback
-
-**Alternatives considered and rejected:**
-
-- **Default timeout (e.g., 5s)**: Forces timeout behavior on all users; might cause false timeouts
-- **Global signal configuration**: Too complex for users to manage; no per-lock control
-- **Do nothing**: Ignores legitimate safety concerns in high-reliability systems
-
-**Consequences**:
-
-- **Breaking changes**: None—`disposeTimeoutMs` is optional with no default
-- **API addition**: Single field in `BackendConfig`, `RedisBackendOptions`, `PostgresBackendOptions`, `FirestoreBackendOptions`
-- **Implementation**: Updated `common/disposable.ts` to support timeout parameter, passed through backend `decorateAcquireResult()` calls
-- **Backend updates**: Redis, PostgreSQL, Firestore backends pass `config.disposeTimeoutMs` to `decorateAcquireResult()`
-- **Test coverage**: 5 new unit tests in `test/unit/disposable.test.ts` covering timeout behavior, signal handling, and error reporting
-- **Documentation**: JSDoc in `common/types.ts` explains opt-in nature, use cases, and recommends backend-level timeouts as primary approach
-- **Cross-references**: See `common/disposable.ts` for implementation, `common/types.ts` for configuration
diff --git a/test/unit/common/time-helpers.test.ts b/test/unit/common/time-helpers.test.ts
index fc4454d..43e3a9f 100644
--- a/test/unit/common/time-helpers.test.ts
+++ b/test/unit/common/time-helpers.test.ts
@@ -9,7 +9,7 @@
* - isLive predicate for liveness checking
* - calculateRedisServerTimeMs for Redis TIME command
*
- * Per specs/interface.md:
+ * Per docs/specs/interface.md:
* - TIME_TOLERANCE_MS in common/time-predicates.ts is the NORMATIVE SOURCE
* - All backends MUST import and use this constant
* - Backends MUST NOT hard-code alternative tolerance values
@@ -315,7 +315,7 @@ describe("TIME_TOLERANCE_MS Enforcement Across Backends", () => {
describe("Documentation Consistency", () => {
it("interface spec must declare TIME_TOLERANCE_MS as normative source", () => {
- const spec = readFileSync("specs/interface.md", "utf-8");
+ const spec = readFileSync("docs/specs/interface.md", "utf-8");
// Verify spec marks it as normative
expect(spec).toContain("TIME_TOLERANCE_MS");
@@ -324,8 +324,11 @@ describe("TIME_TOLERANCE_MS Enforcement Across Backends", () => {
});
it("backend specs must reference interface.md, not duplicate the value", () => {
- const firestoreSpec = readFileSync("specs/firestore-backend.md", "utf-8");
- const redisSpec = readFileSync("specs/redis-backend.md", "utf-8");
+ const firestoreSpec = readFileSync(
+ "docs/specs/firestore-backend.md",
+ "utf-8",
+ );
+ const redisSpec = readFileSync("docs/specs/redis-backend.md", "utf-8");
// Both specs should reference interface.md
expect(firestoreSpec).toContain("TIME_TOLERANCE_MS");
@@ -334,16 +337,15 @@ describe("TIME_TOLERANCE_MS Enforcement Across Backends", () => {
expect(redisSpec).toContain("interface.md");
});
- it("ADR-005 must reference interface.md as normative source", () => {
- const adr = readFileSync("specs/adrs.md", "utf-8");
+ it("ADR-005 must reference interface.md as source", () => {
+ const adr = readFileSync(
+ "docs/adr/005-unified-time-tolerance.md",
+ "utf-8",
+ );
- // ADR-005 section should reference interface.md
- const adr005Section =
- adr.split("## ADR-005")[1]?.split("## ADR-")[0] ?? "";
-
- expect(adr005Section).toContain("TIME_TOLERANCE_MS");
- expect(adr005Section).toContain("interface.md");
- expect(adr005Section).toContain("normative");
+ // ADR should reference interface.md and TIME_TOLERANCE_MS
+ expect(adr).toContain("TIME_TOLERANCE_MS");
+ expect(adr).toContain("docs/specs/interface.md");
});
});
});