Format-Adapter persist↔download path lint
TL;DR. Every Format-Adapter under
api/internal/formats/<fmt>/uploads artifacts withformatutil.Upsert(... PERSIST_PATH ...)and serves them back withformatutil.LookupDigest(... DOWNLOAD_PATH ...). The two paths must agree, or the artifact lands in the blob store but the download handler returns 404.make lint-format-pathswalks each adapter's AST and refuses to compile a PR whose persist/download leading-literal prefixes diverge.
Why this lint exists
The 2026-05-08 testcontainer run (item 103 of the product roadmap) surfaced three adapters — Pub, Puppet, and Swift — whose persist site wrote to a path like tmp/<uuid> while the download site read from packages/<name>/<version>.tar.gz. The integration suite eventually caught it, but only after a four-minute testcontainer bootstrap per format.
A static AST walk catches the same bug class in under a second, so the lint sits as a gate in front of the integration job. A red lint pass fails the workflow before docker even spins up.
What the lint checks
For each adapter directory (api/internal/formats/<fmt>/, excluding formatutil/):
- Parse every non-test
.gofile withgo/parser. - Find every call to
formatutil.Upsert(...)(the persist site) and every call toformatutil.LookupDigest(...)(the download site). - Extract the path argument — index 4 for
Upsert, index 3 forLookupDigest. - Compute a leading literal prefix for each path expression:
"foo/" + x→"foo/"fmt.Sprintf("/foo/%s", x)→"/foo/"helper(args)→ recurse into helper's firstreturn EXPRlocalVar→ recurse into its:=binding- opaque (function parameter, cross-package call) → empty string
- For each persist path
P:- If
leadingLiteral(P)is empty: skip (no signal to compare). - If any lookup in the same adapter shares the prefix: OK.
- If any lookup is itself indeterminate (opaque expression): skip — the lint cannot prove a mismatch when the lookup side could route through any path at runtime.
- Otherwise: report mismatch.
- If
The matching rule is intentionally conservative. A clean exit on the current corpus is the deployment gate — false positives block the merge pipeline far more than false negatives. Phase B can tighten the rule later if a real-world bug slips through.
What you'll see when it fires
api/internal/formats/pub/pub.go:234:18: format=pub persist="fmt.Sprintf(\"tmp/%s\", uuid)" (leading-literal "tmp/") has no matching lookup (lookup leading-literals: [packages/])
lint-format-paths: 1 persist/lookup mismatch(es)The line number points at the formatutil.Upsert call site. The report includes:
- the rendered expression so you can grep for it,
- the leading literal the linter could prove statically,
- the set of lookup-side prefixes in the same adapter, so you can tell whether the lookup needs to be widened or the persist needs to be narrowed.
Running it
make lint-format-paths # gates `make test-integration` in CI
make lint-all # runs every lint-* target locally
go test ./tools/lint/format-paths/... # unit-tests the linter itselfThe linter binary lives at tools/lint/format-paths/ as its own Go module so it builds against the stdlib only — no pgx, S3, OIDC or tracing deps to drag in.
Handling a false positive
If the lint fires on a legitimately-different persist/download pair (very unlikely; the prefix-match rule has been audited against all 30+ adapters in the tree), the right fix is almost always to align the two sites on a shared helper function rather than silence the linter. The whole point of the bug class is that divergent literals creep in when the persist and download implementations are written months apart by different people.
If you're certain the divergence is correct, file an issue rather than adding an inline allowlist — the rule needs to grow with the corpus, not get patched per-call-site.
Implementation pointers
- Linter source:
tools/lint/format-paths/main.go - Wrapper:
scripts/lint-format-paths.sh - CI wiring:
.github/workflows/integration.yml - Roadmap item: PRODUCT-ROADMAP.md item 108
- Related: item 103 (the bug fix that motivated the lint), item 99 Phase D (slug-naming sibling lint).