Loading
docs(plans): add S10 Maven local implementation plan
🎯 Context
Decomposes S10 Maven local into 12 MR-sized steps. Work item #21 (closed).
Per the /planner skill: research and approach signed off via the plan-MR merge gate; no step MR may open before this lands on main.
This MR has been revised across multiple review cycles (S06 spec + S27 Phase 1 plan alignment, compression pass against the spec-mirror anti-pattern, pr-review-toolkit code-reviewer pass, plan-exit-review, blob-storage foundation alignment). The shape and key decisions below reflect the current plan, not the initial draft.
Resolved blockers:
- "How to FK to
blob_storage_attachments" (review thread) — the foundation MR for the table (plan merged 2026-05-21; Step 1 implementation MR pending) lands the partitioned table + sharedCreateBlobStorageAttachment(ctx, q Querier, nsID, sha256)free function +ErrParentBlobMissingsentinel.maven_filesactually carries two blob FKs (composite intoblob_storage_attachments, direct intoblob_storage_blobs(namespace_id, sha256)per ADR-007:735-736), so Step 2 is gated on the foundation MR and S06 Step 6 (which landsblob_storage_blobs+ the attachments → blobs FK, expanded per the foundation plan). Step 8b and Step 9 call the shared helper from inside the format tx, mapErrParentBlobMissingto 500, and inherit the foundation plan's MR-order gate: no INSERT-callsite MR may merge before S06 Step 6.
🧭 Plan shape
- 12 steps, sized 200–400 LOC each target (~500 ceiling for genuinely indivisible concerns; vendored / generated / binary excluded), per
/plannerskill rules. - Step 8 pre-split into 8a + 8b based on pr-review-toolkit feedback — 8a ships the pre-stream slice (
SetReadDeadline+MaxBytesReader+ find-or-create + version-cap with 501-stub on the stream branch); 8b ships stream + commit + duplicate handling + concurrent test harness + test-data fixture generator. Eliminates late-MR-open size-decision pressure. - Mermaid DAG + per-step
Depends on:field (per !200 (merged)/plannerconventions).- Parallel-runnable cohorts: roots (Steps 1 + 3 — both
Depends on: —; Step 2 is foundation-gated and opens once theblob_storage_attachmentsfoundation MR + S06 Step 6 merge); format internals (Steps 6/8a/9 sibling-parallel sharing predecessors 4 + 5); external-gated tail (Steps 10 + 11 each gate on a separate external dep). - Sequential coupling: 8b after 8a; 7 after 6 (shared
digest.go+DigestStore).
- Parallel-runnable cohorts: roots (Steps 1 + 3 — both
- Exhaustive AC 1–33 → step matrix as authoritative coverage contract.
- Direct S06 import. Maven imports
internal/storagetypes directly —BlobStore,Session,BlobContent(withIsRedirect()/RedirectURL()/Reader()/Size()),BlobOpenOption(withWithDeliveryMode+WithForceStream),BlobCommitOption(withWithBlobMetadata),BlobUserMetadata{SHA1 *SHA1Sum}, sentinels. No Maven-side facade. Maven callsStoreBlob/OpenBlob/DeleteBlob/NewSession+ theSessionlifecycle (Write/Close/Commit/Cancel/UploadID); S06'sBlobInfo/ResumeSession/GetSessionStatusexist on the interface but Maven never calls them (single-shot PUTs, no resume, no SHA-256 lookups). Integration tests + dev/CI binary consumeinternal/storage/stub(S06 Steps 2–4); when S06 Steps 7–9 + 16/17 merge, the composition root swaps to the realBlobStoretransparently through the interface — no Maven step required. - S08 stubbed at the middleware boundary. If the request reaches the Maven handler, auth + authz have been resolved by middleware. S08 today does not load namespace / repository into the request context; the Maven handler loads both itself in Step 5 pre-flight (shared
NamespaceStore.FindBySlug+ Step 4'sFindRepository, which wraps sharedRepositoryStore.FindByName). Step 6'sOpenBlobcall-site readsdelivery_mode_overridefrom theNamespacerow Step 5 loaded — no context-value source, noIdentity/NamespaceMetaplaceholders. - Step 10 consumes S27's
jobsriver.ClientPhase 1 (Maven is a named Phase 1 consumer per S27 spec).EnqueueTx[T]is atomic with the caller's*sql.Txper S27 spec AC #5. Per-Maven-package dedup contract satisfied viaWithUnique(UniqueByArgs())+ the data-model invariant that(NamespaceID, RepositoryID)is functionally determined byPackageIDat the data layer. - Step 11 runs the
registry-conformanceMaven runner. - Invariant guardrails (Steps 4 + 5) in a single
invariants_test.gowith two AST-walk diagnostics:TODO(s18-buffered-counters)markers on direct-UPDATEsites (Step 4) +http.Error-ban in the package outside test files (Step 5, AC 30). - Explicit interface satisfaction on
*store(var _ DownloadStore = (*store)(nil)× 4) so consumer-interface signature drift fails at the implementation site, not at handler wire-up.
📦 Key design decisions
- Greenfield rewrite from spec. Monolith (
app/services/packages/maven/*) referenced for behavior edges only. - Path traversal & router invariant (AC 19, T-02 threat model). Primary
..defense isnet/http.ServeMuxcleaning; Step 5's segment rejection is defense-in-depth. 9-wire-form variant matrix on both GET and PUT lives in Step 5's handler tests; router-swap MRs MUST keep it green. - Archive-class
downloads_countallowlist{jar, war, ear, aar, zip}— matches monolith framing;.pom/.module/ metadata don't bump. - SHA-1 hand-off via
WithBlobMetadata. Step 8b passesWithBlobMetadata(BlobUserMetadata{SHA1: ...})toSession.Commit; S06 writes the value asx-amz-meta-checksum-sha1/x-goog-meta-checksum-sha1atomically with the move. Redirect-mode responses carry the SHA-1 header from the storage backend on the final 200. - MD5 redirect-mode asymmetry (per merged S10 spec amendment): MD5 never persisted as backend object metadata; proxy mode emits from
maven_files.md5column;.md5sidecar GET/HEAD short-circuits with422 md5_disallowed_under_fipsbefore any DB lookup under FIPS-on. - Handler-owned redirect emit:
BlobContent.IsRedirect() == true→ write302 Found+Location: <RedirectURL()>per S10 spec API Contracts. Plan adopts S06's value-object API; the S10 spec's older "S06 owns serve path" wording is flagged as a stale-relative-to-S06-surface ambiguity for post-implementation spec amendment. - Handler-owned Range/206/416 dispatch on the stream path (parse Range, validate against
BlobContent.Size(), write 200 / 206 + Content-Range / 416 +bytes */<size>). Per S06 specBlobStoredocstring: format handlers own the redirect-vs-stream branch. - HEAD bypasses
OpenBlobstructurally (row + headers + empty body; no signed URL ever generated). Honors spec's "HEAD is never redirected" by construction, not by ignoringIsRedirect()after a wasted call. - Counter emissions deferred to S18 / S20 / S22. Every direct-
UPDATEcall-site (BumpAccessTimestamps,BumpRepoDownloadCount,BumpRepoCounters,MarkRepoLastUpdated) ships as a TODO-marked no-op stub in Step 4. The mechanism (buffered pipeline, batched flush, etc.) is owned by S18 (download tracking) + S20 (lifecycle) + S22 (storage accounting) per the two-stage development model. Marker convention enforced byinvariants_test.godiagnostic #1. - CPU-bound publish observation. Maven primary PUTs run four hash operations per byte split across the Maven handler (SHA-1 + SHA-512 + MD5 unless FIPS) and S06's
Session(incremental SHA-256). Treated as a CPU-hot path during deployment sizing; exact CPU budget at the ADR 004 5 GB ceiling is hardware-dependent and should be measured against production CPU profile. Publish concurrency protection is platform-territory (deployment-level HTTP-server max-conns / upstream proxy) and out of scope for this plan. - Reconciler enqueue contract (T-06). Per-Maven-package enqueue dedup + idempotent execution under at-least-once delivery. Phase 1 binds dedup to
WithUnique(UniqueByArgs())— operationally equivalent to per-Maven-package because(NamespaceID, RepositoryID)is functionally determined byPackageIDat the data layer. - Reconciler
WithForceStream()mandatory — server-side caller cannot follow a redirect. Two complementary tests: call-site mock assertion + behavioral guard (stub returnsIsRedirect() == true→ reconciler returns clean typed error instead of corrupting/panicking/hanging). - No EarlyDuplicate fast-path — authoritative SQL conflict check at commit time is sufficient; spec fast-path is "optional optimization".
- Storage-quota 429 mapping — deferred.
internal/storagedoes not declare a quota sentinel today. The intended mapping per spec Step 4c → ADR 004 Storage Quota (429 Too Many Requests+code: "quota_exceeded"+Retry-After) is deferred to a plan-amendment once the sentinel is defined. ErrInvalidMetadatasentinel (Step 9 reconciler) for DOCTYPE rejection (billion-laughs defense, asserted viaerrors.Is).<release>conditional emit (Step 9): unconditional<latest>+<lastUpdated>; emit<release>only when a live non--SNAPSHOTversion exists.
🧪 Test plan
- Vale + markdownlint + lychee + gitlint pass (verified locally; all hooks green at commit).
- Reviewer walks the AC matrix and confirms every per-step Acceptance bullet matches.
- Reviewer verifies the DAG (Mermaid +
Depends on:) is consistent with the Status table (12 rows: 1, 2, 3, 4, 5, 6, 7, 8a, 8b, 9, 10, 11). - Reviewer confirms the three "Spec ambiguities resolved in plan" are acceptable resolutions (package-level primary GET counter, archive-class
downloads_countnarrowing, S10 spec Step 3b stale relative to S06 surface). - Reviewer confirms direct
internal/storageimport (no Maven facade) — integration tests + dev binary consumeinternal/storage/stub(S06 Steps 2–4); real-impl swap is transparent through the interface. - Reviewer confirms Step 2's dependency on the
blob_storage_attachmentsfoundation MR + S06 Step 6 is acceptable (FK targets + sharedCreateBlobStorageAttachmenthelper + MR-order gate for Step 8b / 9 INSERT-callsites). - Reviewer confirms Step 10's
EnqueueTx[ReconcileArgs]binds correctly to S27 Phase 1's*sql.TxAPI and theWithUnique(UniqueByArgs())dedup mechanism.
Edited by Moaz Khalifa