feat(maven): add the mavenroute leaf grammar and its depguard fence (S09 Enforcement plan: 6/20)
🎯 What
Adds internal/format/maven/mavenroute, the dependency-free Maven mount grammar: ParseRoute parses the decoded URL path /{slug}/maven/{repository_name}/{path...} into the slug, the repository name, and the raw artifact-path remainder, reporting no match for a malformed shape or an empty artifact path. Its leaf-ness is fenced by a new mavenroute-stays-a-leaf depguard rule and a negative-control probe under the existing depguard_nc_ociroute build tag, wired into the depguard:negative-control CI job.
💭 Why
Step 6 of the S09 enforcement plan. internal/authz cannot import internal/format/maven without closing a cycle through internal/server, so the mount grammar the Maven RouteParser needs lives in a fenced leaf package — the shape internal/format/oci/ociroute proved for the container surface.
Nothing imports the package in this MR. The RouteParser that consumes it and the wiring that mounts it are steps 7 and 9 of the plan.
🔍 Design notes
- The remainder is opaque and kept verbatim. No trailing-slash trim (
ociroutetrims one for OCI's canonicalblobs/uploads/form; Maven has no such convention), no Maven-coordinate parsing (that staysmaven.ParsePath's job), no slug charset validation (resolution settles validity, per theocirouteprecedent). Only an empty remainder fails to match; the tests pin the raw/and leading-slash-remainder acceptances explicitly. - The probe shares the
depguard_nc_ociroutebuild tag rather than adding a new one: likeociroute, this leaf sits upstream ofinternal/authzonce step 7 lands, so its probe cannot compile alongside the shared-tag npm/npmrules probes without closing a real cycle throughinternal/serverandinternal/authz. internal/format/oci/ociroute's probe header is compressed because this MR falsified it: the tag is no longer ociroute's "own", and the invocation is no longer "scoped to this probe alone". The tag and import rationale now live with the rules in.golangci.yaml, and the touched Go comment blocks sit within thelint:comment-capscaps.
⚠️ Plan drift
Three additions beyond Step 6's Files: list:
FuzzParseRouteand itsfuzz:mavenline. The fuzz job's package arguments do not recurse, so the new leaf package needed its own entry; without it the target would only ever replay its seeds.docs/dev/go-testing.mdexpects each fuzz target to run in CI, andfuzz:ocicarries the exact analogue forociroute.- The
ocirouteprobe-header compression described above, owed by the comment-accuracy guardrail once this MR made the old wording false. - A second negative-control probe,
depguard_negative_control_marker.go, and itsdepguard:negative-controlgate.exclusions.generateddrops every finding for a file golangci-lint reads as generated, depguard included, so the fence is disarmed on such a file. The probe carries the marker and the twin's violation, and the job asserts no finding is reported at its location, which makes the exemption a checked property. The rule'sdescno longer claims to be the only thing between that edge and a cycle.
🧪 Testing
| Plan acceptance criterion | Covered by |
|---|---|
| The grammar accepts the mount shape | TestParseRoute_SuccessTable — 12 rows, including the maven-as-slug, maven-as-repository, and maven-heading-the-remainder collision rows that pin the literal check as positional, and the verbatim-remainder pins |
| Rejects a short path, an empty repository name, and an empty artifact path | TestParseRoute_FailureTable — 13 rows, each row's comment naming the guard that rejects it, each asserting the zero-value return |
depguard:negative-control reports mavenroute-stays-a-leaf as fired |
The job's rule loop gains the rule; reproduced locally, the depguard_nc_ociroute invocation fires mavenroute-stays-a-leaf, ociroute-stays-a-leaf, authz-format-boundary, and no-remotetest-in-prod, with no typecheck or load failure |
FuzzParseRoute (25 seeds) asserts non-empty fields and a lossless round-trip on every accepted input; multi-minute local runs found nothing. Statement coverage on the package is 100%. go test ./... green, golangci-lint run 0 issues, and the comment-caps check passes at the tip.
e2e scenario catalogs: unchanged, deliberately. The package is reachable from no request path in this MR; the Access control rows in docs/testing/e2e/maven.md move off blocked in step 9, the step that makes Maven enforce.
📏 Diff size
+558/−53 across 7 files, past the 500-line justification threshold. 342 of the added lines are the test suite, 77 the depguard rules' configuration and their comments, and 83 the negative-control CI job and its gates, leaving 61 lines of new production Go in one file plus 25 across the two probes. Splitting the leaf grammar from the table that pins it, or either probe from the rule it exercises, would raise the count a reviewer has to hold in mind rather than lower it.
Related to #848 (closed)