feat(maven): the wrong-format data-error mark (S30 plan: 10b/25)
What
Step 10b of the S30 plan: the Maven-side wrong-format gate — internal/format/maven/virtual_wrong_format.go — the mark that builds the data-error position over virtual.NewDataErrorUpstream under virtual.ReasonWrongFormat for a row whose repositories.format is outside the Maven family, mirroring the orphan-shape marks npm shipped.
It consumes the four foundation declarations S32's Step 10a owns — the constructor in internal/virtual/upstream.go, the Reason classification in internal/virtual/rules.go, the loadAndFilter arms in internal/virtual/resolve.go, and the audit const in internal/remote/audit.go — and redeclares none of them (a second declaration of the const is a duplicate, not a merge). This branch modifies no internal/virtual or internal/remote file.
The gate declines a row whose kind is outside the hosted/remote set, leaving it to the caller's kind arm, and an in-range observed kind rides through untranslated. It builds no assembly: the only caller is Step 10c, which lands next, so no route can reach this code yet and the unexpected-kind arm keeps its existing behavior.
Governing ADRs
None directly. ADR-023 (Code Structure and Enforcement) relates: the gate lives in internal/format/maven/ and imports only shared internal/ packages, which is the layout that ADR fixes.
Depends on
S32's Step 10a (!2831 (merged)) — required merge order, satisfied. Its declarations are in main and main is merged into this branch, so this MR targets main directly. Per the plan's Dependencies: S32's Step 10a lands before this plan's Step 10b; both passes of the cross-reference check against internal/virtual/orphan_upstream_shapes.md's updated reasons list stay in this step's tests.
Tests
The mark's suite (mirroring npm's orphan-shape suites): builds the data-error message from the spec position, excludes the wrong-format position under the data-error reason rather than the policy-denial one, withholds the 404 its miss would establish, is ineligible when alone, and keeps its own reason among the orphans. GitLab-hosted, remote, and both-row variants covered; in-range kinds asserted per arm, and the virtual and out-of-range kinds beside a non-Maven format asserted to be declined.
No docs/testing/ scenario is added or affected (guardrail): no route composes this gate until Step 10c, and the Maven conformance suite exercises a hosted repository, which cannot reach this path. No docs/user/ surface changes: nothing a customer can call, see, or configure changes in this step.
Diff: 385 lines (34 production, 351 test), under the 500-line reviewable ceiling.
Spec coverage
Spec: docs/specs/S30-maven-virtual.md
Acceptance criteria
| # | Criterion | Tests |
|---|---|---|
| AC (wrong format) | An upstream whose repositories.format is not maven is ineligible, logged as a data error, and not audited as a denial |
TestVirtualWrongFormatPosition_ExcludesThePositionAsItsOwnDataError |
| AC (not-found withheld) | The 404 its missing Maven-table Lookup would establish is not returned |
TestVirtualWrongFormatPosition_WithholdsTheNotFound |
| AC (unexpected kind) | An upstream of an unexpected kind fails closed; Step 10c's kind arm owns the fail-closed response, and this step pins the gate declining the row and the mark reporting an in-range kind | TestVirtualWrongFormatPosition_BuildsTheDataErrorMessage |
| AC (own reason) | Each data-error shape keeps its own reason, the case a shared mark would collapse | TestVirtualWrongFormatPosition_KeepsItsOwnReasonAmongTheOrphans |
Error cases
| # | Condition | Tests |
|---|---|---|
| E (data error, nothing eligible) | No upstream eligible with a data-error position maps AllUpstreamsIneligible rather than a not-found |
TestVirtualWrongFormatPosition_IsIneligibleWhenAlone |
Security considerations
| # | Concern | Tests |
|---|---|---|
| S (recursion guard, resolution half) | Resolution rejects an upstream whose kind is neither hosted nor remote; this gate declines such a row and leaves it to the caller's kind arm | TestVirtualWrongFormatPosition_BuildsTheDataErrorMessage (the virtual and out-of-range kind cases) |
Related to #289 (closed)