docs(specs): add S08 remote repository contracts
Adds docs/specs/S08-remote-contracts.md, the contract the per-format
remote children are implemented and reviewed against, and the S04
amendments it commits to.
Closes #48 (closed). Parent: #47 (closed).
Why
Every read row in the current catalog seeds its own fixture with a
write: OCI pushes into conformance/{runID}/{slug}, Maven deploys
com.gitlab.conformance:conformance-{runID}-{slug}, npm publishes
@conformance/conformance-{runID}-{slug}. On a remote repository every
one of those writes answers 405, so narrowing the catalog with
--filter does not produce a runnable remote suite: the read rows are
the ones that write first.
What S08 pins
- Repository kinds.
hostedandremote;virtualis out of scope and keeps the catalog's## Virtualsection as its inventory. - Fixture seeding. Push to
--upstream-url, read the same coordinate back through--registry-url. Run-ID coordinates make the first read a guaranteed cache miss, make the expected bytes known, and make a mismatch attributable to the repository under test. - Catalog split. A
## Remotesection and<format>.remote.*slugs. All 20 rows in today's## Virtualsections are classified: pull-through and cache rows move, rows that presume several upstreams stay. A second kind-independent section,## Protocol baseline, takesoci.version.check,npm.meta.ping, andnpm.meta.whoamiout of## Localso a remote run still negotiatesGET /v2/and npm's service routes.## Negative authis kind-independent as before. - Per-format surface, all three formats. Read availability, the
write-refusal routes with their
Allowvalues, and the three behaviors only a remote run exercises: Maven sidecars are the proxy's own arithmetic, npm rewritesdist.tarball, OCI listings may be proxied live. - Row inventory. 9 Maven, 12 npm, 15 OCI slugs. Each carries its
assertion and its assertion class, except the five re-filed cache
rows, which land
optionaland outside the class taxonomy. This is the children's scope. - Assertion sources. Protocol-mandated assertions separated from target-specific ones, so onboarding a second target is a scoped edit rather than a catalog re-derivation.
S04 amendments
| Location | Edit |
|---|---|
run flag table, flag inventory, validation rules |
--repository-kind, --upstream-url, --upstream-free-only added |
list flag table |
--repository-kind accepted; both upstream flags stay run-only |
Where validated values go, Config struct, Config.Validate |
The three fields and their rules |
| Error types | repository-kind, upstream-url, upstream-free-only added to ConfigError.Field |
Module interface purity clause |
RepositoryKind is the one Config field a descriptor set may depend on |
TestDescriptor |
NeedsUpstream bool, so the runner owns the upstream skip instead of each Fn |
TestCase |
SetupFailure error, the channel a row uses to end the run; the row still reports StatusSkip |
| Entry points | SelectDescriptors(m, cfg) exported, plus the ErrNothingEstablished sentinel |
| Execution order, steps 8 and 9 | Loop short-circuits on SetupFailure; a loop that completes is checked for the all-skip verdict |
Report struct |
Interrupted widened off "always ctx.Err()" |
--timeout registration, validation, and where it lands |
Registered with no default, so a remote run derives one from --settle-timeout and the settling-row count |
| Run-ID resolution, AC #25 (closed) | Widened to 16 hex characters |
| Security Considerations | Seeding and read-back response bodies are content-scanned at construction |
| AC #5 (closed), AC #11 (closed), AC #13 (closed) | List-equals-run promise holds per kind; list accepts the kind |
pkg/conformance gains exported API here, not only fields:
SelectDescriptors, ErrNothingEstablished, and two struct fields.
AC #12 (closed) is unaffected: selecting a descriptor set needs no I/O, so
list still contacts nothing.
Decisions worth a reviewer's attention
- All three formats in one spec. A remote read's request and response semantics are identical to the hosted ones, so a per-format spec would mostly restate S05 / S06 / S07, while what does differ is shared across formats.
- The preflight is a test row, not a tool error. Reporting an
unlinked pair of URLs at env setup would need network I/O before
S04's execution-order step 7 and would turn a transient upstream
outage into exit
2. - A remote run must name a fixture source or declare it has none.
It supplies exactly one of
--upstream-urlor--upstream-free-only; omitting both is a configuration error.--upstream-free-onlyruns the absence and write-refusal rows and skips every row needing a fixture, because those two classes need no upstream and are the whole of what is assertable against a remote repository whose upstream the operator cannot write to. Its exit0is not proxying evidence, which the README now says as well. This resolves the contradiction in #47 (closed), which asked for both a required flag and a skip gate on its absence: the declaration is required, and the skip gate is what the operator opts into. - A run that establishes nothing does not exit
0. A seeding failure is a skip, never a fail, because a fixture that never reached the upstream says nothing about the repository under test. But two cases end the run with exit2rather than reporting green: a cause that belongs to the run rather than the row (a revoked credential, or a transient shape that fails its one re-attempt), and a completed run in which every seeding row skipped. The second is the reused---run-idcase, where every write answers409, each skip is correctly row-attributable, and the run would otherwise relay nothing and still pass a CI gate. - Target-specific assertions are required rows, not optional ones. npm and Maven have no standard error vocabulary, so there is no neutral spelling; the GitLab monolith reaches read-only by not routing writes at all. The Notes column names the spec requiring each one.
Prerequisites this spec assumes, both on the Artifact Registry side
- A
kind=2repository whose upstream is a hosted repository on the same instance.scripts/conformance/provision.shseedskind=0today. - The instance's own host in
outbound_allowlist. S13's upstream dialer denies loopback, RFC1918, link-local, and the host's own interface addresses, and the allowlist is the documented bypass.
Ownership of both is open on #47 (closed).
Next
#49 (closed) turns this into docs/plans/2026-08-20-remote-conformance.md and
opens the three format children.