Skip to content
SQA Cockpit

ADR-0037 — Snappy owns the contracts package; SQA imports it as a versioned dep

ADRsUpdated 10 min readEdit on GitHub ↗
5 sections··

ADR-0037 — Snappy owns the contracts package; SQA imports it as a versioned dep #

  • Status: Accepted
  • Date: 2026-05-13
  • Deciders: Natan
  • Source: PRD-17 §17.1.1

(docs/prds/17-shared-snappy-contracts-package.md), CONTRACTS-DESIGN.md §2 / §8.1 (docs/research/2026-05-11-domain-activation-sweep/CONTRACTS-DESIGN.md), empirical motivation in ISSUES.md §II.1 / §II.4 / §II.5 (docs/research/2026-05-11-domain-activation-sweep/ISSUES.md).

Context

Context #

The 2026-05-11 100-domain F500 sweep against snappy-api 0.15.2 produced 56 fails across 100 domains. After attribution, five of seven non-resolved issues — a clean majority — were not bugs at all. They were drift between snappy's source of truth and SQA's restatement of it:

  • P5 — KNOWN_CDNS stale. SQA hardcoded a CDN-provider set in

src/systems/snappy/domain-activation.ts:471–480 that was copied at some point from snappy-crawl's packages/snappy-crawl/src/protection/patterns.ts:30–38. Snappy later renamed cloudfront → aws-cloudfront and added vercel, netlify, framer. Seven valid active domains failed C1 not because snappy was wrong but because SQA's idea of "what cdn values are legal" had not been updated.

  • P3 — state-aware verifiers. Verifiers C1, C2, C5, C6, C7

assumed the active happy-path. When snappy correctly classified a domain as blocked or unresolved, each verifier independently fired a misleading gap. The fix landed INVARIANTS_ACTIVE / INVARIANTS_BLOCKED / INVARIANTS_UNRESOLVED maps at domain-activation.ts:167–205 — but those maps are SQA-internal data restating snappy's per- terminal-state guarantees. Anyone changing snappy's state-machine has no signal that SQA's data needs to move in step.

  • P10 — canonical-merge invisibility. Snappy's pipeline merges

probe domains into canonical records on redirect; the probe domainId vanishes from Mongo, ClickHouse, and Loki. The fix added a canonicalMerged scenario flag at domain-activation.ts:355–367. Again — this is contract knowledge that lives only in SQA's verifier head; snappy's source has no machine-readable indication that "after canonical-merge, the probe-domainId is no longer a valid lookup key."

Each fix is correct for today. None of them prevent the next drift. The structural cause is the same for all three: the contract between snappy and the world is not a first-class artifact. It exists as snappy source (the truth), prose in docs/contracts/snappy/domain-activation.md (a human restatement), and code in SQA's verifiers (an executable restatement). Three sources, two restating the first. Drift is not a risk — it is a certainty. We have demonstrated it three times in a single sweep cycle.

ADR-0032 established that SQA contract docs are observer-owned, not Newman-style CDCs. That ADR pins the prose convention. This ADR pins the artifact convention: when the contract becomes typed code, where does the typed code live, and who owns it.

ADR-0007 carved SQA's scope to operational verification and explicitly banned audience- impression work. The preamble of docs/contracts/snappy/domain-activation.md:6–13 goes further: "SQA is an external observer. We do not import snappy's code." Importing a contracts package from snappy appears to violate that purity. This ADR carves out the exception and explains why it preserves both properties the rule was written to protect.

Decision

Decision #

The @metaintro/snappy-contracts package is owned by snappy, lives in the snappy monorepo at packages/snappy-contracts/, is versioned alongside snappy-api releases, and is imported by SQA as a versioned dependency. Three rules follow:

  1. Ownership. Snappy authors and reviews changes to the

package. SQA is a consumer, not an owner. A snappy PR that changes the contract bumps the package; SQA bumps the dep in a separate PR after the snappy release lands in production. This cadence applies to ongoing contract evolution. The bootstrap commit — the cross-repo PR that introduces the @metaintro/snappy-contracts package and the initial SQA dep on it — is exempt and lands as one coordinated change.

  1. Carve-out scope. SQA is allowed to import from

@metaintro/snappy-contracts and only from there. The allowed symbols are: the typed Claim<Observed, Evidence> interface and validateClaim() API; per-claim definitions (C1_…, C2_…, etc.); shared enums referenced by claims (DomainStatus, TerminalState, CDN_PROVIDERS, strategy names, structured event names); the canonical evidence shapes and GapRow type. SQA is forbidden from importing snappy's implementation packages directly — @metaintro/snappy-api, @metaintro/snappy-crawl, @metaintro/snappy-db, internal mongoose schemas, repository functions, business logic. The contracts package is the only seam through which SQA touches snappy code.

  1. Dependency direction. @metaintro/snappy-contracts sits

at the leaf level of the snappy monorepo's package graph, as a peer of @metaintro/snappy-schemas. Its only workspace dependency is @metaintro/snappy-schemas (for DomainDoc, DomainStatusHistoryDoc, and other wire types). @metaintro/snappy-crawl and @metaintro/snappy-api import from snappy-contracts, never the reverse. Symbols currently authored in mid-level packages (notably CDN_PROVIDERS at packages/snappy-crawl/src/protection/patterns.ts:30) move into snappy-contracts during migration — they are deleted from their former home and re-imported from snappy-contracts by their previous callers. Re-export-only stubs are explicitly disallowed; the new package must be the single point of authorship, not a thin passthrough.

Consequences

Consequences #

Architectural #

  • Drift becomes a compile error. When snappy adds a CDN

provider to CDN_PROVIDERS, the value lands in the contracts package. When snappy adds a new TerminalState, the Record<TerminalState, ClaimBranch<…>> shape in every claim errors until each per-state branch is authored. The forcing function lives in the type checker, not in reviewer attention.

  • One source of truth per fact. The enum, the per-terminal

invariants, the structured-event vocabulary, the mirror SLO budgets — each lives in exactly one place, the package. The hand-maintained markdown contract becomes a generated artifact (per PRD-17 §17.6.1).

  • SQA's dep graph stays small. Because the package sits next

to snappy-schemas at the leaf level, the transitive closure of SQA's node_modules does not pull crawlee, playwright, undici, or linkedom. SQA remains a thin observer; the contracts seam doesn't smuggle in the SUT's runtime.

  • Cross-repo coupling is explicit. SQA pins a specific

contracts version. A snappy contract change cannot affect SQA until SQA bumps the dep — which is itself a PR with code review and a CI run. There is no implicit, silent, "code I didn't know I depended on" coupling.

Behavioural #

  • A contract change in snappy is a *deliberate, version-tagged

decision*, not a comment edit. Adding a CDN provider is a patch bump; adding a terminal state is a minor bump; renaming a field is a major bump (CONTRACTS-DESIGN.md §2, §8.2). The cadence forces snappy authors to think about the consumer in the same PR that introduces the change.

  • The cross-repo CI gate proposed in PRD-17 §17.4 runs

validateClaim against shared __fixtures__ in both repos. If snappy's CI breaks against a fixture, snappy fixes the fixture or the implementation in the same PR. If SQA's CI breaks against the same fixture after a dep bump, SQA's PR is the visible place where the contract evolution lands. The pattern is OpenAPI/Protobuf inter-service testing applied to side-effect contracts.

  • The "external observer" property is preserved, not weakened.

Newman CDCs are about producer-side gates on consumer expectations; ADR-0032 already established SQA contracts are not CDCs. This ADR preserves Newman's direction — the producer publishes the spec, the observer verifies against it — while explicitly not adopting the Pact-Broker shape. SQA imports the spec because the producer published it, exactly as an inter-service test imports a producer's OpenAPI schema.

Falsifiability #

  • If a future PR adds @metaintro/snappy-api,

@metaintro/snappy-crawl, or @metaintro/snappy-db as a direct dependency of SQA, this ADR has failed — the carve-out was for snappy-contracts only.

  • If @metaintro/snappy-contracts/package.json gains a workspace

dep on @metaintro/snappy-crawl (or anything else above snappy-schemas in the dep graph), this ADR has failed — the leaf-level placement is load-bearing.

  • If SQA's node_modules after install contains playwright,

crawlee, undici, or linkedom, this ADR has failed — those are the smoke-test signals that the dep direction was reversed.

  • If the contract code drifts back into ad-hoc `const KNOWN_CDNS

= new Set([…])` blocks inside SQA verifiers, this ADR has failed — the package is supposed to be the only place those enums live for SQA's purposes.

  • If snappy ships a contract-changing release (new terminal

state, renamed enum value, tightened invariant) without bumping the contracts package version, this ADR has failed — the producer-side discipline is what makes the consumer-side pin meaningful.

  • If @metaintro/snappy-contracts contains only re-exports of

symbols whose authorship still lives in snappy-crawl, snappy-api, or snappy-db, this ADR has failed — the package must be the authoring home for the symbols it exposes, not a passthrough.

Alternatives considered

Alternatives considered #

  1. SQA owns the contracts package. Rejected. The contract is

what snappy promises about its outputs; snappy is the producer and the authority on what it promises. SQA's role is to verify the promise, not to author it. If SQA owned the package, every contract change would be SQA reading snappy source and inferring intent — the same restatement problem, one workspace closer. Snappy's runtime self-assertion path (CONTRACTS-DESIGN.md §6.2, PRD-17 §17.5) also becomes impossible: snappy cannot reasonably depend on a package owned by its own external observer for production self-checks.

  1. A third repo (e.g. metaintro/contracts). Rejected.

Adds a release surface (separate CI, separate version cadence, separate review group) without solving any of the problems above. The snappy monorepo already has the tooling to publish workspace packages and gate releases on lint/test; a separate repo loses cache cohesion in Turbo and forces every snappy contract change into a cross-repo PR even when it only affects snappy's own tests. The dossier (§9.1) explicitly notes the dependency graph for contracts is local to snappy.

  1. Inline duplication (the status quo). Rejected on

evidence. This is the configuration that produced P3, P5, and P10 in a single 100-domain sweep. ISSUES.md attribution showed five of seven non-resolved issues were drift; cost of detection was a full sweep cycle per drift; cost of fix was minutes-to-hours per fix. The detection cost dominates and compounds.

  1. Share contract knowledge via a Markdown contract doc only.

Rejected. docs/contracts/snappy/domain-activation.md is the current shape of this alternative and is what produced the drift — a comment that nothing reads (P7 archival semantics lived in activate-domain.workflow.ts:194-199; SQA had to read it, re-encode it, and the next change to it produces no signal). Prose has no forcing function. The markdown stays valuable as human-readable narrative and becomes a generated artifact (PRD-17 §17.6.1), but it cannot be load-bearing on its own.

  1. **@metaintro/snappy-contracts imports from

@metaintro/snappy-crawl.** Rejected. snappy-crawl carries heavy runtime dependencies (crawlee, playwright, undici, linkedom, pino) that SQA has no business transitively depending on. If snappy-contracts imports from snappy-crawl, every SQA bun install pulls the entire crawl stack into node_modules and SQA stops being a thin observer. The correct direction is the inverse: snappy-crawl imports from snappy-contracts, with snappy-contracts at the leaf level of the dep graph next to snappy-schemas. Symbols currently authored in snappy-crawl and used contractually (CDN_PROVIDERS, ProtectionProviderType) move down into snappy-contracts during the migration and snappy-crawl re-imports them from there. This rule generalizes: anything that becomes part of the public snappy↔world contract lives in the leaf-level package, regardless of which mid-level package authored it first.

See also

See also #

PRD this ADR ships under, including the four-phase migration plan and sub-item structure.

1,340-line design dossier; §2 (package overview), §3 (Claim interface), §4 (validator API), §8.1 (the "SQA purity" trade- off this ADR resolves), §8.4 (SUT-namespacing recommendation).

empirical motivation; §II.1 (P5), §II.4 (P10), §II.5 (P3).

boundary; the contracts surface stays inside operational invariants and does not drift into adversarial or audience- impression territory.

— the prose-level companion: SQA contract documents are observer-owned, not Newman CDCs. This ADR is the artifact-level consequence of that prose convention.

— sibling ADR for PRD-15's negative-claim surface; same ownership and import discipline applies if negative claims later land in a peer package.

  • monorepo/packages/snappy-schemas/package.json — the leaf-level

reference package whose shape snappy-contracts mirrors.

  • monorepo/packages/chat-contracts/package.json — existing

precedent for an SUT-namespaced contracts package in the same monorepo.

Was this page helpful?