Skip to content

split instrument availability into constraints and options axes#626

Open
igrigorik wants to merge 1 commit into
proto/funding-source-and-credential-constraintsfrom
feat/constraints-partitioned
Open

split instrument availability into constraints and options axes#626
igrigorik wants to merge 1 commit into
proto/funding-source-and-credential-constraintsfrom
feat/constraints-partitioned

Conversation

@igrigorik

@igrigorik igrigorik commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Current #424 shape put everything an instrument declares — presence, per-field
constraints, accepted card brands, and credential families — into one
constraints bag. Prototyping a resolver against that shape showed the cost:
to enforce field requirements a consumer must first classify every key by
resolving its concrete schema (is this a field constraint? a value menu? a
typed family?). That classification is the whole problem — it needs the target
schema, defeats "compile from data alone," and produced real bugs in the
prototype (per-key allOf composition, and a required name collision).

Partition the bag into two sibling axes:

  • constraints — field requirements over the instrument's OWN fields (an
    Object Constraint: required plus one direct key per constrained field).
    Compiles to a JSON Schema overlay (required + properties) a standard
    validator runs.
  • options — accepted value menus (brands) and typed families
    (credentials), resolved by data lookup and never part of the overlay.

With the menus and families moved to options, constraints holds nothing but
required and field constraints — and object_constraint now enforces exactly
that: every non-required key is a nested Object Constraint or a Value
Constraint (recursing via $ref "#"). Compiling the overlay is therefore a
pure data walk — no schema resolution, no classification. That is the win: the
resolver is clean by construction, not by careful bookkeeping. Proven in the
ucp-schema resolver prototype (~20-line compile; the compiled overlay enforces
presence and allowed values against real instances).

Trade-offs:

  • constraints is deliberately not a literal JSON Schema: fields are direct
    keys, so a consumer compiles it (take required, lift field keys under
    properties). A properties-keyed sub-map would make it literal but collide
    the JSON Schema properties keyword with the data key when a schema narrows a
    nested field. The compile is trivial, and a resolver runs regardless.
  • required is reserved inside a constraint object, so a field literally named
    required can be listed as present but cannot carry a nested constraint.
    Rare, and documented.
  • The shape still does not enforce name resolution (does the named field exist
    on the target?). Unchanged from before; a lint pass can add it later — the
    partition makes it tractable, since every non-required key is unambiguously
    a field name.

   The prior shape put everything an instrument declares — presence, per-field
   constraints, accepted card brands, and credential families — into one
   `constraints` bag. Prototyping a resolver against that shape showed the cost:
   to enforce field requirements a consumer must first classify every key by
   resolving its concrete schema (is this a field constraint? a value menu? a
   typed family?). That classification is the whole problem — it needs the target
   schema, defeats "compile from data alone," and produced real bugs in the
   prototype (per-key allOf composition, and a `required` name collision).

   Partition the bag into two sibling axes:

   - `constraints` — field requirements over the instrument's OWN fields (an
     Object Constraint: `required` plus one direct key per constrained field).
     Compiles to a JSON Schema overlay (`required` + `properties`) a standard
     validator runs.
   - `options` — accepted value menus (`brands`) and typed families
     (`credentials`), resolved by data lookup and never part of the overlay.

   With the menus and families moved to `options`, `constraints` holds nothing but
   `required` and field constraints — and `object_constraint` now enforces exactly
   that: every non-`required` key is a nested Object Constraint or a Value
   Constraint (recursing via `$ref "#"`). Compiling the overlay is therefore a
   pure data walk — no schema resolution, no classification. That is the win: the
   resolver is clean by construction, not by careful bookkeeping. Proven in the
   ucp-schema resolver prototype (~20-line compile; the compiled overlay enforces
   presence and allowed values against real instances).

   Trade-offs:

   - `constraints` is deliberately not a literal JSON Schema: fields are direct
     keys, so a consumer compiles it (take `required`, lift field keys under
     `properties`). A `properties`-keyed sub-map would make it literal but collide
     the JSON Schema `properties` keyword with the data key when a schema narrows a
     nested field. The compile is trivial, and a resolver runs regardless.
   - `required` is reserved inside a constraint object, so a field literally named
     `required` can be listed as present but cannot carry a nested constraint.
     Rare, and documented.
   - The shape still does not enforce name resolution (does the named field exist
     on the target?). Unchanged from before; a lint pass can add it later — the
     partition makes it tractable, since every non-`required` key is unambiguously
     a field name.

   Migration: the stricter `object_constraint` (every non-`required` key must be a
   field constraint) surfaced five stale example docs still using
   `constraints: {brands}`; migrated them to `options: {brands}`. schema-authoring
   and payment-handler-guide are rewritten to the two-axis model, and the
   availability wire example is now validated by the example harness instead of
   skipped.
@igrigorik
igrigorik marked this pull request as ready for review July 22, 2026 04:14
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Jul 22, 2026
}
"properties": { "type": { "const": "token" } }
},
"then": { "$ref": "token_credential.json#/$defs/constraint" }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not introduced here ... but unless I'm missing something, I don't see a token_credential.json#/$defs/constraint?

**from the data alone** — `enum`/`const` is a Value Constraint, otherwise it's an
Object Constraint — so a consumer compiles the overlay without resolving the
concrete schema. Field constraints are **open by default**: they live in the wire
data and validate against the open Object Constraint, so most handlers only narrow

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: inclusion in schema-authoring.md implies use across UCP broadly so "handlers" could be slightly confusing here.

A concrete Object Constraint **MUST** extend `object_constraint.json` and define
every supported key. The key's schema determines its meaning:
- **`constraints`** — field requirements over the target's OWN fields, as an
Object Constraint: `required` (presence) plus one key per constrained field.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we clarify whether the required should include the keys from options? They are presumably required (with more than just presence)

@carolinerg1 carolinerg1 added status:under-review gov:needs-tc-review and removed status:needs-triage Signal that the PR is ready for human triage labels Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants