feat(maven): streaming metadata merger (S30 plan: 12/25)

Summary

Step 12 of the S30 Maven virtual plan (12 of 25): the pure cross-upstream merge of package-level maven-metadata.xml documents — a streaming token pass over each input, the merge rules, and the deterministic serialized output. No database, no HTTP, no context, which is what makes the merge-rule acceptance criteria table-tested.

The step declares Depends on: — and opens at plan merge, parallel to the whole resolution track; Step 13's collection and Step 14's serve surface consume it through the DAG.

Spec coverage

The step's scope is S30, Package-level metadata merge:

  • Streaming parse, byte-bounded, directive-rejecting (virtual_metadata_merge.go): the pass streams tokens through the shared cappedReader shape from metadata_parse.go, bounded by the caller's maven.max_metadata_size value, and rejects a DOCTYPE directive with ErrInvalidMetadata (billion-laughs defense).
  • Merge rules (the spec's rule table): the union of versions in position-major order with first-occurrence duplicate handling; each hint (latest, release) taken from the newest parseable <lastUpdated>, tie to the lower position; <lastUpdated> the maximum, omitted when none parses; coordinates taken from the request; every merged document declares modelVersion="1.1.0".
  • Group-level <plugins> detection for the single-winner serve: the shape is detected and excluded from the merge fields; the verbatim serve is Step 14's.
  • Deterministic output: xml.MarshalIndent over a stable input order, pinned by a run-twice bytes test.
  • Escaping: every upstream-sourced string serializes through the XML escaper — the escaping table covers markup characters, ]]>, CDATA openers, processing instructions, and an injected <plugins> element, asserting the merged document still parses as one Maven metadata document with no injected element.
  • Per-input data errors: an over-cap, malformed, or directive-carrying input is that input's data error (errMetadataTooLarge / errMetadataMalformed / ErrInvalidMetadata), not the read's failure — the three classes are pinned apart.
  • Merge-algorithm version byte: metadataMergeAlgorithmVersion is declared here for the validator's canonical encoding, which Step 14's validator consumes.

Governing ADRs

ADR-4 (data and application limits) is the one ADR this diff works under: the size bound counts bytes as they are read through cappedReader — the implementation notes' prescribed shape — rather than trusting a declared length. The other ADRs the plan and spec cite (1, 5, 7, 8, 9, 12) govern tenancy, delivery, schema, CAS, API shape, and usage events; the diff touches none of those surfaces, and the pure merge emits no event (Step 17 books the emissions). Checked against all seven cited ADRs.

Reviewable LOC

345 reviewable source lines plus 768 test lines and 1 CI line at head 2dcc4d364 (1114 added, none removed; git diff --numstat origin/main...HEAD). Under the 500-reviewable-LOC ceiling in docs/dev/development-model.md, so no split or justification is owed.

Testing

virtual_metadata_merge_test.go is table-driven, one table per rule, per the plan's Tests entry: the parse-extraction and input-error-classification tables, the version-union, position-major-ordering, hint-selection, lastUpdated-maximum, request-coordinates, and escaping tables, and the determinism run-twice test. virtual_metadata_merge_property_test.go carries the position-order property test in npm's rapid shape, generating the position count and the input order and checking the emitted order plus a byte-identical second run.

virtual_metadata_merge_fuzz_test.go adds FuzzParseMetadataMergeInput for the untrusted-input parser per docs/dev/go-testing.md (## Fuzzing for parsers and untrusted input), with the directive, cap, and error-taxonomy oracles over a fixed 4 KiB cap, registered in the fuzz:maven CI job so the coverage-guided loop runs past the seeds.

go test ./internal/format/maven/ passes locally; a 20s live fuzz run executed ~320k inputs clean. CI is authoritative.

e2e catalog: no scenario added or affected — the step is a pure library surface with no route, dispatch, or client-visible behavior change. The plan books the e2e.maven.virtual.* catalog group at Step 20.

Related to #1357

Merge request reports

Loading
Loading