feat(storage): implement content-addressable PathResolver (S06 Step 5)

What

Implements the concrete PathResolver — a pure, I/O-free function family that computes ADR-008's content-addressable storage path layout from a namespace UUID and a content hash. This is Step 5 of the Storage Layer (S06) plan, a Stage 2 / Track A prerequisite for the real PostgreSQL BlobStore/Session.

  • NewPathResolver(rootDirectory string) PathResolver — public factory returning the PathResolver interface introduced in Step 1.

  • DefaultRootDirectory ("artifact_registry") — the documented default path prefix.

  • ObjectPath, UploadPath, NamespacePath produce:

    {root}/{ns_hash[0:2]}/{ns_hash[2:4]}/{ns_hash}/objects/{ch[0:2]}/{ch[2:4]}/{ch}
    {root}/{ns_hash[0:2]}/{ns_hash[2:4]}/{ns_hash}/uploads/{upload_id}

    where ns_hash = hex(SHA256(namespace_id UUID bytes)) and ch = hex(SHA256(content)). The {root}/.../{ns_hash} prefix is shared by objects/ and uploads/, so the commit-time move is always intra-partition.

Why this shape

  • Content-addressable keying per ADR-008: the content hash is the object key directly.
  • Two-level sharding (65,536 buckets per level) on both ns_hash and content_hash to distribute writes.
  • Reversible parseability per ADR-011: given a path, ns_hash and content_hash are recoverable by component parsing with no database lookup, enabling reconciliation.

References

  • Plan: docs/plans/2026-05-15-storage-layer.md — Step 5 (PathResolver implementation and tests). Plan MR merged on main.
  • Spec: docs/specs/S06-storage-layer.md — PathResolver section + acceptance criteria.
  • ADRs: ADR-008 (content-addressable storage), ADR-011 (reversible path structure).

Tests

internal/storage/paths_test.go (unit, runs in go_unittests):

  • Golden-string assertions of the literal ADR-008 layout for all three methods (known UUID/content fixtures, incl. zero-UUID and empty-content).
  • rapid property tests: reversibility (object + upload), determinism, namespace-path-is-prefix, and namespace isolation (distinct namespaces never share a prefix).
  • Sharding correctness and the root_directory override.

Acceptance

  • go build ./..., go vet, golangci-lint (0 issues), goimports, go test -race -short ./internal/storage/ all pass locally.

🤖 Generated with Claude Code

Merge request reports

Loading
Loading