From 7c539cb86a903f56eec51201837f838207c05574 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Sun, 24 May 2026 16:32:01 -0400 Subject: [PATCH] docs(227): ADR on input-validation checking semantic shape, not just type (#228) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs(227): create ADR for input-validation-shape-not-just-type Captures the architectural standard that defensive normalization at trust boundaries must validate both type and semantic shape, with silent coercion on failure. Concrete cases: parentTraceId UUID v4 fix in PR #225 and release-version validation in ADR 218. Closes #227 * docs(227): cross-reference new ADR from ADR 218 Appends a "See also" section at the end of ADR 218 pointing forward to ADR 227, which generalises the type+semantic-shape validation principle documented in ADR 218's narrower release-workflow context. * docs(227): add CONTRIBUTING pointer to new ADR Adds a "Code Review Lessons → Input validation" section after the Reviewer Standards block, linking to ADR 227 as the citable reference for the type+semantic-shape validation standard. --- CONTRIBUTING.md | 6 + docs/adr/218-release-version-validation.md | 4 + ...27-input-validation-shape-not-just-type.md | 110 ++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 docs/adr/227-input-validation-shape-not-just-type.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e7f3efffd..883d1a7c2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -758,6 +758,12 @@ Reviewers do not rely solely on CI to verify correctness. Before approving a PR, **"Tests pass in CI" is not sufficient for merge.** The implementation must correctly solve the problem described in the linked issue. +## Code Review Lessons + +### Input validation: check shape, not just type + +Defensive normalization at trust boundaries must validate both the value's type and its semantic shape. A `typeof === 'string'` check is necessary but insufficient when the field's contract requires a specific format (UUID v4, semver, file path, etc.). See [ADR 227](docs/adr/227-input-validation-shape-not-just-type.md) for the architectural standard and concrete cases. + ## Code Style - **CommonJS** (`.cjs`) — the project uses `require()`, not ESM `import` diff --git a/docs/adr/218-release-version-validation.md b/docs/adr/218-release-version-validation.md index 2add6a9c3..28cf322d2 100644 --- a/docs/adr/218-release-version-validation.md +++ b/docs/adr/218-release-version-validation.md @@ -87,3 +87,7 @@ git push origin --delete release/1.03.0 - **Leading-zero inputs fail in under 5 seconds** at the `validate-version` job before any branch, install, or publish operation runs. - **Duplicate-version inputs fail in under 5 seconds** at `validate-version` rather than after a full install-and-test cycle. - **The late dry-run check in `finalize` is unchanged.** It remains a belt-and-suspenders guard; the new precheck does not remove it. + +## See also + +- ADR 227 (`docs/adr/227-input-validation-shape-not-just-type.md`) generalises the principle this ADR documents in the narrower release-validation context: input validation at trust boundaries must check both type and semantic shape, with silent coercion on failure. diff --git a/docs/adr/227-input-validation-shape-not-just-type.md b/docs/adr/227-input-validation-shape-not-just-type.md new file mode 100644 index 000000000..2082e1b27 --- /dev/null +++ b/docs/adr/227-input-validation-shape-not-just-type.md @@ -0,0 +1,110 @@ +# ADR 227: Input validation must check semantic shape, not just type + +- **Status:** Accepted (2026-05-24) +- **Date:** 2026-05-24 + +## Context + +Defensive normalization at trust boundaries typically starts with a type check: + +```js +if (typeof value !== 'string') return undefined; +``` + +This stops non-string values but accepts any string — including the empty string, garbage payloads, and values that are structurally correct (a string) but contractually invalid (not a UUID v4, not a semver, not a file path). The remaining attack surface is the gap between "is a string" and "satisfies the field's contract." + +### The PR #225 trigger + +PR #225 (`refactor/178-trace-id-propagation`, P1.4 of ADR-0174 SDK retirement) introduced `parentTraceId` on `DispatchEvent`. The initial implementation normalized the field with a type-only guard: + +```js +parentTraceId: typeof raw.parentTraceId === 'string' ? raw.parentTraceId : undefined, +``` + +Codex adversarial-review (commit range `fb94ba8d`–`338d0951`) flagged that this propagated: + +- empty strings (`""`) — a correlation key that matches nothing +- garbage strings (`"not-a-uuid"`, `";"`, `"