feat(npm): packument-merge core (S31 Step 13, part 2/3)

Stacked MRs (review/merge bottom-up)

Split into 3 stacked MRs to keep each within the review size limit (≈800 ideal). Each part targets the previous (part 1 → main), reviewed and merged bottom-up.

Part 2 of 3 of docs/plans/2026-08-21-s31-npm-virtual.md — Step 13: metadata merge and serve surface

Continues Part 1 of this stack — see part 1 for the full step scope and the parts this step's own plan text is split into.

This part's scope:

  • MergePackuments, the packument-merge core: the streaming dispatcher that merges versions (whole-object, lowest-position-wins), dist-tags (union), time (per-key union, modified max / created min, RFC3339Nano tie-break), and the descriptive top-level fields (whole set, lowest contributing position) into one document, streamed to an io.Writer rather than buffered.
  • The per-version integrity/shasum digest-pair tracking that feeds the integrity_divergence_total counter added in part 1.
  • One happy-path test per merge sub-feature, plus the streaming-not-buffering and byte-identical-rerun determinism guarantees.

Added while rebasing onto part 1's review fixes: part 1's review renamed readStringOrSkip→readStringValue, making a non-string pass-2 dist-tags value fatal instead of coercing it to "" — a generalization of the atomic-per-position-omission pattern the reviewer flagged as open, not spec-mandated (see part 1's discussion). This part's time.modified/time.created handling used the same helper under its old name; rather than reintroduce a separate coercing helper, it's now unified onto readStringValue too, so a non-string time.modified/time.created in pass 2 is fatal the same way. Pass 1 already rejects a non-string time member (validateTimeMembers, merged in part 1), so this only matters if a contributor's Open breaks its same-bytes-every-call contract — pinned by TestMergePackuments_Pass2NonStringTimeValueIsFatal, mirroring part 1's dist-tags test. The sentinel error is renamed errMergeTagValueNotString → errMergeValueNotString to match.

Part 3 adds the remaining edge-case, integrity-divergence, and property tests over this part's code.

Spec: https://gitlab.com/gitlab-org/ops/artifact-registry/-/blob/main/docs/specs/S31-npm-virtual.md, "Cross-upstream packument merge" and "Metrics". Plan: https://gitlab.com/gitlab-org/ops/artifact-registry/-/blob/main/docs/plans/2026-08-21-s31-npm-virtual.md, Step 13.

Related to #894 (closed)

Edited by David Fernandez

Merge request reports

Loading
Loading