Skip to content

feat(storyboard): add field_greater_than / field_at_most / field_at_least matchers (#1839)#1841

Merged
bokelley merged 2 commits into
mainfrom
bokelley/field-at-most-matcher
May 18, 2026
Merged

feat(storyboard): add field_greater_than / field_at_most / field_at_least matchers (#1839)#1841
bokelley merged 2 commits into
mainfrom
bokelley/field-at-most-matcher

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Closes #1839.

Adds two new storyboard validation check kinds:

  • field_at_most — assert numeric field ≤ comparand (non-strict)
  • field_at_least — assert numeric field ≥ comparand (non-strict)

They share the same comparand-resolution model as the existing strict field_less_than: either a literal value or a runtime-captured context_key, with absent context keys passing the check with a context_key_absent observation (the prior step may have been legitimately skipped on a branch-set path).

Why

Storyboard scenarios that assert "value X is at most / at least N" — cap and floor enforcement — have no clean primitive today. The strongest available is field_less_than (strict <), which forces a literal-plus-epsilon workaround:

- check: field_less_than
  path: media_buy_deliveries[0].totals.frequency
  value: 3.01  # cap is 3; epsilon to allow equality

That's semantically wrong (assumes a domain rounding convention) and brittle when sellers report the cap value exactly (3.00 < 3.01 passes; 3.00 < 3.00 fails).

field_at_most: 3 is the right primitive.

Implementation note

Refactored validateFieldLessThan into a shared validateNumericComparison(op) helper parameterised by check name, operator symbol, and comparator function. All three numeric checks dispatch through it — no copy-paste branches. Forward-compat default in the runner already grades unknown check kinds as not_applicable, so older runners won't brick.

Concrete consumer

media_buy_seller/frequency_cap_enforcement (adcontextprotocol/adcp#4727) ships with the epsilon workaround. Once this lands, a small follow-up will swap field_less_than: 3.01field_at_most: 3 in that scenario yaml.

Test plan

  • 12 new unit tests covering both matchers (test/storyboard-cross-step-validators.test.js):
    • strictly-less / strictly-greater paths
    • boundary-equality paths (the cases that distinguish / from </>)
    • literal-value comparand
    • context_key comparand
    • context_key_absent observation
    • type-error: missing field
    • type-error: non-numeric field
    • type-error: non-numeric comparand
    • missing path
  • All 22 existing field_less_than / field_equals_context tests still pass (refactor preserves behaviour).
  • npm run format:check clean.
  • tsc --noEmit clean.

Python parity

Per the issue: once TS ships, mirror in adcp-client-python. Not addressed here.

🤖 Generated with Claude Code

bokelley and others added 2 commits May 18, 2026 06:05
Cap- and floor-style scenarios (frequency ≤ 3, delivered_reach ≥ promised)
have no clean primitive today. The strongest available is `field_less_than`
(strict <), which forces a literal-plus-epsilon workaround
(`field_less_than: 3.01`) that's semantically wrong and brittle when
sellers report the cap value exactly.

`field_at_most` (≤) and `field_at_least` (≥) are non-strict comparators
that share `field_less_than`'s comparand-resolution semantics: either a
literal `value` or a runtime-captured `context_key`, with absent context
keys passing the check with a `context_key_absent` observation.

Refactors the previous one-off `validateFieldLessThan` into a shared
`validateNumericComparison(op)` helper parameterised by check name,
operator symbol, and comparator function — all three checks dispatch
through it. Forward-compat default in the runner already grades unknown
check kinds as `not_applicable`, so older runners won't brick.

Tests: 12 new covering both checks (literal + context_key paths, the
critical equality-at-boundary cases that distinguish them from
`field_less_than`, type-error paths, and missing-path).

Concrete consumer: `media_buy_seller/frequency_cap_enforcement`
(adcontextprotocol/adcp#4727) ships with the epsilon workaround; a small
follow-up will swap `field_less_than: 3.01` → `field_at_most: 3`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…adrant

Per protocol review on this PR: shipping only `field_at_most`/`field_at_least`
without `field_greater_than` leaves 3 of 4 numeric comparators implemented.
Storyboard authors reaching for strict-`>` would hit the runner's
forward-compat `not_applicable` default and silently grade as passing —
semantically wrong for assertions that should fail at the boundary.

`field_greater_than` slots into the same `NUMERIC_OPS` table; the
`validateNumericComparison` helper handles it without code duplication.
Four-quadrant vocabulary now complete: `<`, `>`, `<=`, `>=`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bokelley bokelley changed the title feat(storyboard): add field_at_most / field_at_least matchers (#1839) feat(storyboard): add field_greater_than / field_at_most / field_at_least matchers (#1839) May 18, 2026
@bokelley

Copy link
Copy Markdown
Contributor Author

Review pass — done

Parallel reviews from code-reviewer + ad-tech-protocol-expert.

code-reviewer: ship. Refactor preserves all original behavior (error messages, expected/actual fields, observation pass-through, type-error paths) exactly; boundary semantics correct; NUMERIC_OPS typing sound (as const satisfies …).

ad-tech-protocol-expert: sound-with-one-caveat. Naming matches the prose-style vocabulary (field_present, array_length, contains) rather than operator-symbol style (field_lte) — right call. Forward-compat verified. Flagged: shipping / without > leaves the comparator quadrant 3-of-4 filled. Storyboard authors reaching for strict-> would hit the runner's forward-compat not_applicable default and silently grade as passing — semantically wrong.

Addressed in ba4faed4: added field_greater_than to close the quadrant. One more NUMERIC_OPS entry; the shared validateNumericComparison helper handles it without duplication. Tests added covering the strict-> equality-boundary case (the unique distinguishing behavior vs field_at_least). 38/38 tests pass.

Title updated to reflect all three matchers.

@bokelley bokelley merged commit ad849f7 into main May 18, 2026
11 checks passed
@bokelley bokelley deleted the bokelley/field-at-most-matcher branch May 18, 2026 11:03
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.

storyboard runner: add field_at_most / field_at_least matchers for numeric threshold assertions

1 participant