Skip to content

feat: enforce the document's declared bounds at plan time - #94

Merged
ShocOne merged 1 commit into
mainfrom
feat/constraint-validators
Aug 14, 2026
Merged

feat: enforce the document's declared bounds at plan time#94
ShocOne merged 1 commit into
mainfrom
feat/constraint-validators

Conversation

@ShocOne

@ShocOne ShocOne commented Aug 14, 2026

Copy link
Copy Markdown
Member

mapping.md row 9. An API that accepts an out-of-range value and silently stores
it clamped or truncated leaves state and config disagreeing with no error to
explain it. The bounds the document already declares — carried into the IR by
#91 — now become validators, so the configuration fails at plan time instead of
diverging at apply time.

Before this, Validators: appeared 1,766 times across the pilots and was
entirely stringvalidator.OneOf.

What is emitted

Kind Source Validator
string minLength / maxLength stringvalidator.UTF8LengthBetween / AtLeast / AtMost
string pattern stringvalidator.RegexMatches
int64 minimum / maximum int64validator.Between / AtLeast / AtMost
float64 minimum / maximum float64validator.…
list minItems / maxItems listvalidator.SizeBetween /
map minItems / maxItems mapvalidator.SizeBetween /

Each is a code.CustomValidator carrying its own import, so it cannot reach a
file whose import block forgot it — TestUnit_ConstraintValidators_DeclareTheirOwnImports
holds that.

Three decisions worth reviewing

Length is counted in characters, not bytes. JSON Schema defines maxLength
in characters. The framework offers both: LengthBetween uses len() and
UTF8LengthBetween uses utf8.RuneCountInString. Using the byte-counting one
would refuse a valid value the moment it stopped being ASCII, so this emits the
UTF8Length* family.

A pattern is compiled at generation time and dropped when Go cannot take it.
OpenAPI patterns are ECMA-262; Go's regexp is RE2, which has no lookahead and
no backreferences. An expression RE2 rejects would panic the generated provider
inside regexp.MustCompile at package initialisation — before any plan or
test runs, and somewhere postcheck's go build cannot see it. So the pattern
is compiled here first and yields no validator if it fails, while the length
bound beside it still stands.

On these three documents nothing was actually dropped: all 25 pattern-carrying
attributes compiled. The guard is defensive, and
TestUnit_ConstraintValidators_SkipAPatternRE2CannotCompile proves it with
^(?=.*[A-Z]).{8,}$.

A fractional bound on an integer attribute is dropped, not truncated.
minimum: 1.5 on an integer describes a value the attribute cannot hold;
truncating to 1 would silently move the boundary. The integral bound beside it
survives.

A length or range declared on an object is ignored — it describes something
the attribute does not hold. A size bound on a nested list still applies,
because the list is the thing being sized.

Measured effect

Refusals unchanged on all three pilots — 817 / 290 / 343, provider verify
reports no drift, postcheck (go mod tidy, go build, go vet) passes. This
adds validators to attributes that already generated.

448 constraint validators where there were none:

UTF8Length RegexMatches int64 float64 listSize
github 40 9 3 13 14
jamfpro 125 11 52 0 8
thousandeyes 0 0 142 30 1
total 165 20 197 43 23

handoff.md:98 records LengthBetween 0, RegexMatches 0, int64validator.Between 0; all three are now non-zero. mapvalidator stays at 0 because no pilot
declares minItems on a map.

Gates

  • gofmt -l internal cmd — empty
  • scripts/repo_hygiene_gate.sh — pass
  • golangci-lint run — 0 issues
  • go build ./... && go vet ./... — pass
  • go test ./... — pass, coverage 91.4% total
  • provider generate + provider verify on all three — no drift

New file internal/emit/render_constraints.go with 5 tests in
render_constraints_test.go.

Unrelated, and noted rather than fixed

The github corpus pin is stale and will keep drifting: its upstreamUrl
points at github/rest-api-description /main/, a moving branch ref
(pinned 80850db2…, served 81c0ff2b…). No test reads that document today —
specmodel and intermediate_representation both fetch only thousandeyes
so CI is green, but the first test that reads it fails immediately.
corpus.Describer is also still never installed, so the next RewriteLock run
writes "unparsed", 0, 0. Both want their own PR.

🤖 Generated with Claude Code

An API that silently clamps or truncates an out-of-range value leaves
state and config disagreeing with no error to explain it. The bounds
the document declares now become validators, so the configuration fails
at plan time instead.

Length is measured in characters: JSON Schema counts maxLength in
characters and the framework's LengthBetween counts bytes, which would
refuse a valid value the moment it stopped being ASCII.

A pattern is compiled at generation time and dropped when Go cannot
take it. OpenAPI patterns are ECMA-262 and Go's regexp is RE2, which
has no lookahead; an expression RE2 rejects would panic the generated
provider inside MustCompile at package initialisation, where go build
cannot see it.

A fractional bound on an integer attribute is dropped rather than
truncated, which would silently move the boundary.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ShocOne
ShocOne merged commit 89ca993 into main Aug 14, 2026
2 checks passed
@ShocOne
ShocOne deleted the feat/constraint-validators branch August 14, 2026 16:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant