feat(npm): dist-tags merge, pass-1 validation (S31 Step 13, part 1/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 1 of 3 of docs/plans/2026-08-21-s31-npm-virtual.md — Step 13: metadata merge and serve surface

This step's plan text is split into three parts; this stack ships Part 1 only ("the streaming merger") — a pure library merger with no DB, no HTTP, and no context deadline. Parts 2 (collection/fan-out) and 3 (re-point, ETag, HTTP serve) are separate, later steps; Part 2 is currently blocked.

This part's scope:

  • MergeDistTags, the standalone dist-tags document merge (buffered map, lowest position wins each tag, no latest synthesized).
  • The bounded pass-1 validator shared by both merge entry points: well-formedness, size-cap enforcement, object-shape checks for versions/dist-tags/time, dist-tag value string checks, and route-awareness between the dist-tags and packument routes.
  • Fatal-vs-per-position-omission error classification: an Open() or read failure, or an invalid jsonsafe.Limits, fails the whole call; a malformed or oversized individual input is omitted, letting the read still serve the survivors.
  • The three merge_duration_seconds/merge_inputs/integrity_divergence_total Prometheus vectors end to end (registration, docs/dev/observability.md, the cardinality budget in internal/metrics/cardinality.go).

Notes:

  • This is the first production adopter of internal/jsonsafe (a bounded jsontext wrapper enforcing depth/key-count limits), which existed unadopted by any format package before this change.
  • docs/specs/S31-npm-virtual.md's metrics table rides in this MR: the operation label is renamed to npm_virtual_operation (it collided with the OCI proxy's already-pinned operation label in internal/metrics/cardinality.go), and a merge-rule row is clarified to state explicitly that descriptive top-level fields are taken as a whole set, not per-field. This is an intentional, deliberate exception to the usual "specs aren't edited by step MRs" convention, made because the collision was only discovered during implementation.
  • No e2e catalog or docs/user/ change: this is a pure merger with no route wired to it yet (Part 3's job). Conformance tests: n/a, no protocol surface is reachable yet.

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