feat(drift)!: schema-anchored, diff-based drift detection (ADR 0015)#331
Merged
Conversation
Replace the snapshot drift mechanism with schema-anchored drift. Validation is the hard contract (throws on a missing-required/incompatible value and returns the validated value — coerced, defaulted, unknown keys stripped); soft drift is the diff of the raw body against that validated value, classified undeclared (info) / coerced (warn) / defaulted (verbose) and never fatal. This eliminates the variance false positives a single snapshot baseline produced (#327): an optional field absent, a nullable null, an empty/heterogeneous array all validate clean and report nothing. - core: new zero-dep diff.ts + classifyDiff; engine returns the validated value and diffs raw-vs-validated into drift events; DriftOptions = { ignore, severity } - remove the snapshot subsystem: snapshotFile / readonly / onMissing / learn, the `stitch drift generate` CLI, and the critical / watch leveling - vendor-neutral: works for any validator returning a parsed value (no Zod codes) - docs: ADR 0013 (supersedes the drift clauses of 0005), READMEs, the drift guide / two recipes / error page, OVERVIEW / DESIGN / FEATURE-LENSES, and the schema-drift blog post BREAKING CHANGE: drift() no longer accepts critical / watch / onNew / snapshotFile / readonly / onMissing; the `stitch drift generate` command is removed; a call now resolves to the validated value (coerced/defaulted/stripped) rather than the raw body; a missing required field surfaces as `invalid` (make a field required to fail, optional to tolerate). Closes #327. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR #328 (open) already claims ADR 0013 (gen) and 0014 (publishing). Renumber the schema-anchored drift ADR to 0015 and update all references; the planned expose-raw follow-up becomes 0016. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve the one conflict in catch-a-breaking-api-change.mdx: keep this branch's schema-anchored diff-model prose (the snapshot model #329 had only editorially polished is gone) and fold in #329's "watch a field erode" cross-link. Re-sync the docs IA manifest's drift entries (guide title/description, the two recipe descriptions, the STITCH_DRIFT error description) to the new model. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The merge left the rewritten drift table's separator row unaligned; `pnpm check:format` (prettier --check) flagged it. Alignment only. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #327.
What & why
Drift detection was snapshot-based: each response's shape was compared against a committed
<name>.contract.jsonbaseline. A single baseline can't tell real drift from natural response variance, so optional-absent / nullable-null / empty-or-heterogeneous-array all surfaced as false positives (#327).This replaces it with schema-anchored, diff-based drift (ADR 0015): the declared
outputschema is the contract.change: 'invalid',error); the call now returns the validated value — coerced, defaulted, unknown keys stripped — so the result matches the declared type (a soundness fix; it previously returned the raw body).diff.ts):undeclared(info),coerced(warn — a wire-type shift validation hides),defaulted(verbose). All non-fatal,[]-collapsed and deduped, vendor-neutral (any validator returning a parsed value)..optional()/.nullable()/array schema validates clean → no finding.DriftOptions = { ignore, severity }.ignoresilences acknowledged-but-unconsumed paths (kept out of the typed schema);severityfilters (a level / list) or re-levels (a per-kind map).Breaking changes (pre-1.0)
drift()no longer acceptscritical/watch/onNew/snapshotFile/readonly/onMissing.stitch drift generateCLI command is removed.invalid(make a field required to fail,.optional()to tolerate).Migration: drop the snapshot options; make the fields you depend on required; use
ignore/severityfor the soft signals.Out of scope (follow-ups)
Verification
tsc(src + test), ESLint,tsd, attw exports — all greendiff.ts: 34 unit tests🤖 Generated with Claude Code