Skip to content

chore: Refactor schema references to common types#436

Merged
amithanda merged 13 commits into
mainfrom
refactor/move-common-types
May 15, 2026
Merged

chore: Refactor schema references to common types#436
amithanda merged 13 commits into
mainfrom
refactor/move-common-types

Conversation

@amithanda

@amithanda amithanda commented May 12, 2026

Copy link
Copy Markdown
Contributor

refactor: move generic types from shopping/ to common/types/

Rationale

All types currently live under shopping/types/, but many of them carry no shopping-specific semantics. As UCP is envisioned to expand to other verticals beyond shopping, those verticals will need the same primitives — money amounts, addresses, pagination, messaging, rich text — without taking a dependency on the shopping namespace. This refactor establishes common/types/ as the canonical home for types that belong to the protocol, not to a vertical.

This also fixes a pre-existing namespace violation: common/identity_linking.json was referencing ../shopping/types/description.json, which is the exact problem this refactor corrects structurally.


Phases to do the refactor

Phase 1 — Pure primitives

No vertical semantics. Any future capability will need these.

amount, signed_amount, price, description, media, postal_address, pagination, reverse_domain_name, link, buyer

Phase 2 — Protocol messaging infrastructure

UCP's error/warning/info messaging pattern. Every vertical reuses this plumbing verbatim.

message, message_error, message_warning, message_info, error_response, error_code, warning_code, info_code

Phase 3 — Cross-vertical candidates (future PR)

Strong candidates with clear cross-vertical semantics, deferred to validate design stability first.

attribution, signals, context, rating

Phase 4 — Transactional primitives (future PR)

Financial breakdown types whose schema may need to evolve per vertical before being promoted.

total, totals, price_range, price_filter

Phase 5 - Payment types (future PR)

payment_instrument, payment_credential, payment_identity, card_credential, card_payment_instrument, token_credential, available_payment_instrument — these are cross-vertical but domain-specific. Should they be in their own payment/types/ namespace?


This PR (Phase 1 + Phase 2)

  • Creates source/schemas/common/types/ with 18 new files, each with updated $id (https://ucp.dev/schemas/common/types/...)
  • Deletes the 18 originals from source/schemas/shopping/types/
  • Updates all $ref paths in:
    • shopping/types/*.json (remaining shopping types that reference moved types)
    • shopping/*.json (capability schemas: cart, checkout, order, fulfillment, etc.)
    • source/schemas/ucp.json
    • source/schemas/common/identity_linking.json (fixes pre-existing violation)
    • source/services/shopping/ (OpenRPC and OpenAPI service definitions)
  • Adds source/schemas/common to SCHEMAS_DIRS in main.py so the docs macro system resolves moved types correctly

No schema semantics were changed — this is a pure relocation with path updates.

- Added a new common schemas directory and updated references in various shopping schemas to point to common types.
- Removed shopping types such as amount.json, buyer.json, and others, consolidating their definitions into the common directory.
- Adjusted paths in multiple JSON schema files to ensure they correctly reference the new common types, enhancing maintainability and consistency across the project.
amithanda added 3 commits May 11, 2026 19:28
…string, ensuring correct processing of fragment-stripped paths for JSON schema references.
Comment thread source/schemas/common/types/buyer.json Outdated
@amithanda amithanda requested a review from igrigorik May 12, 2026 21:31
@amithanda amithanda added the TC review Ready for TC review label May 12, 2026

@jamesandersen jamesandersen left a comment

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.

thanks @amithanda for setting these types up for more broad use

Comment thread source/schemas/common/identity_linking.json Outdated
Comment thread source/schemas/common/types/error_code.json Outdated
Comment thread main.py Outdated
Comment thread source/schemas/common/identity_linking.json Outdated
@igrigorik

Copy link
Copy Markdown
Contributor

TY for restoring error codes, that was my main flag on firt pass.

Pedantic nice: technically this change may be breaking because we're changing $id pointers and URLs from published schemas. If any code or validators cached that state, they'll get 404's. My intuition is that we should be OK, but this is something to watch out for... let's see if and what breaks.

Question: how do we want to distinguish, or not, common vs domain-specific schemas? Right now they're indistinguishable in how we render then in our tables. Should we separate and note that some are common vs not? This also connects to my flag on main.py behavior where we can end up in shadow / duplicate schemas unless we put some strict guards around it.

amithanda and others added 3 commits May 13, 2026 17:10
buyer carries shopping-domain semantics — it implies a purchase
transaction and may not generalize cleanly to non-shopping verticals. Promoting it to common/ would lock in
shopping-centric naming before cross-vertical requirements are known.
A neutral common type (e.g. party or contact) should be introduced when
the first non-shopping vertical is designed, with field names validated
against actual cross-vertical needs rather than inferred from shopping.
… build time

Adds _validate_common_vertical_uniqueness() to main.py, which runs at
module load time and fails fast if any filename in common/types/ collides
with a filename in a vertical's types/ directory (currently shopping/types/).

A common/vertical collision is always a design mistake: the docs macro
system resolves schemas by basename across SCHEMAS_DIRS, so a collision
silently picks the wrong schema — producing incorrect anchors and broken
reference links with no build error. Two verticals sharing a filename
(e.g. shopping/checkout.json and newvertical/checkout.json) is intentional
and is deliberately not flagged.

New verticals are registered by adding one line to VERTICAL_TYPES_DIRS.
@amithanda

Copy link
Copy Markdown
Contributor Author

Question: how do we want to distinguish, or not, common vs domain-specific schemas? Right now they're indistinguishable in how we render then in our tables. Should we separate and note that some are common vs not? This also connects to my flag on main.py behavior where we can end up in shadow / duplicate schemas unless we put some strict guards around it.

You're right that the reference page currently renders all types indistinguishably:
Type Schemas
Amount ← common/types/ (no label)
Buyer ← shopping/types/ (no label)
Message ← common/types/ (no label)
Product ← shopping/types/ (no label)
This matters for implementors building non-shopping verticals who need to know at a glance which types
are safe to reuse. Fixing it requires changes to reference.md and auto_generate_schema_reference
(section split, or a "Common" badge, or both). Intentionally leaving this for a follow-up so the
presentation choice gets proper discussion rather than being decided quietly inside a refactor PR.

Shadow/duplicate schema guard

Implemented — but we scoped the guard more narrowly than your suggestion after thinking through the
multi-vertical future. PTAL at my other comment.

@amithanda

Copy link
Copy Markdown
Contributor Author

Following up on @yanheChen and @jingyli's comments — agreed that buyer carries shopping-domain
semantics and shouldn't be declared a cross-vertical primitive prematurely.

Reverted buyer back to shopping/types/ in this PR. The reasoning:

  • The name encodes a purchase transaction — it may not generalize cleanly to other verticals
  • Promoting it to common/ would lock in shopping-centric field names (first_name, last_name,
    email, phone_number) as the universal person model before UCP has any non-shopping verticals
    to validate that shape against
  • The right pattern is to introduce a neutral type (e.g. party or contact) when the first
    non-shopping vertical is designed and its person-identity requirements are actually known — not
    to rename a shopping type preemptively and carry the debt forward

common/types/ now contains 18 types, all of which have zero vertical-specific semantics. buyer
remains in shopping/types/ unchanged.

The neutral cross-vertical person type is tracked as a follow-up for when the first non-shopping
vertical is scoped.

amithanda and others added 2 commits May 14, 2026 20:28
Replaces all inline Path(...) literals in SCHEMAS_DIRS and related
configuration with named constants, making the directory hierarchy
explicit and eliminating duplication:

  SCHEMAS_DIR             = Path("source/schemas")
  HANDLERS_GOOGLE_PAY_DIR = Path("source/handlers/google_pay")
  SHOPPING_SCHEMAS_DIR    = SCHEMAS_DIR / "shopping"
  SHOPPING_TYPES_DIR      = SHOPPING_SCHEMAS_DIR / "types"
  COMMON_SCHEMAS_DIR      = SCHEMAS_DIR / "common"
  COMMON_TYPES_DIR        = COMMON_SCHEMAS_DIR / "types"

SCHEMAS_DIRS and VERTICAL_TYPES_DIRS now reference only named constants.
UCP_SCHEMA_PATH derives from SCHEMAS_DIR rather than repeating the
literal path. No behavioral changes.
The redirect logic in create_link still resolved schema paths through raw
string literals after the named-constants refactor extracted SCHEMAS_DIR /
COMMON_TYPES_DIR / SHOPPING_TYPES_DIR / SHOPPING_SCHEMAS_DIR. Consolidate
on the constants.
igrigorik added 2 commits May 15, 2026 05:55
The previous guard required VERTICAL_TYPES_DIRS to be manually maintained
("Add new vertical types directories here as verticals are introduced")
and only enforced uniqueness between common/types/ and the listed vertical
type dirs.

Auto-discover non-common immediate subdirs of source/schemas/ as verticals
and extend the guard to cover every pair of type namespaces (common ×
vertical and vertical × vertical). Cost: verticals cannot share type
filenames; the escape hatch ("rename or refactor the resolver to be
path-aware") is surfaced in the error message when someone hits it.

Auto-discovery also surfaced source/schemas/transports/ as a sibling
namespace not previously tracked. It has no types/ subdir today so the
guard is a no-op for it, but it will participate automatically if one
appears.

@igrigorik igrigorik left a comment

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.

@amithanda pushed a small improvement to the validation logic, please sanity check.

LGTM.

@amithanda

Copy link
Copy Markdown
Contributor Author

@amithanda pushed a small improvement to the validation logic, please sanity check.

LGTM.

@igrigorik - Thanks for the cleanup and auto-discovery in 72eca50!

However, zooming out to the multi-vertical design, there is a fundamental gap in enforcing global filename uniqueness across all vertical namespaces:

  1. Top-Level Capabilities Remain Unprotected: The current check only validates filenames under the types/ subdirectories. It does not protect capability schemas (e.g., checkout.json, order.json, fulfillment.json). If a second vertical introduces new-vertical/checkout.json alongside shopping/checkout.json, main.py will still suffer from the exact same silent resolution bug (loading whichever vertical appears first in SCHEMAS_DIRS for all pages) without triggering this guard.
  2. Restrictions on Vertical Autonomy: Enforcing global name uniqueness forces different verticals to use different names for parallel concepts (e.g., we'd have to use new-vertical_status.json vs. shopping_status.json instead of clean, vertical-scoped types/status.json files).

Proposal: A Path-Aware Resolver

Instead of using strict filename constraints to workaround the resolver's flat-search design, we should make the resolver path-aware. We can inspect the MkDocs compilation context (env.page.file.src_path) to identify which vertical's documentation page is being compiled, and prioritize that vertical's folders in the lookup path.

Here is a quick sketch of how we can achieve this in main.py:

def get_active_vertical() -> str:
  try:
    # src_path is like "specification/new-vertical/checkout.md" 
    # or "specification/checkout.md" (defaults to shopping)
    src_path = Path(env.page.file.src_path)
    parts = src_path.parts
    if len(parts) > 2 and parts[0] == "specification":
      vertical = parts[1]
      # Verify against discovered verticals
      if vertical in [d.name for d in VERTICAL_DIRS]:
        return vertical
  except AttributeError:
    pass
  return "shopping"  # Default vertical fallback

Then, in _load_json_file(entity_name):

def _load_json_file(entity_name):
  """Try loading a JSON file prioritizing the active vertical."""
  active_vertical = get_active_vertical()
  
  # Construct prioritized search paths for the active vertical
  active_schemas_dir = SCHEMAS_DIR / active_vertical
  active_types_dir = active_schemas_dir / "types"
  
  search_dirs = []
  if active_types_dir.exists():
    search_dirs.append(active_types_dir)
  if active_schemas_dir.exists():
    search_dirs.append(active_schemas_dir)
    
  # Append remaining directories in standard order
  for d in schemas_dirs:
    if d not in search_dirs:
      search_dirs.append(d)
      
  # Resolve file
  for schemas_dir in search_dirs:
    full_path = Path(schemas_dir) / (entity_name + ".json")
    try:
      with full_path.open(encoding="utf-8") as f:
        return json.load(f)
    except FileNotFoundError:
      continue
  return None

Next Steps

If this looks good, we can:

  1. Revert 72eca50 to remove the vertical-to-vertical type name constraints.
  2. Keep the common vs. vertical uniqueness check (since a common type should never be shadowed by a vertical).
  3. Implement this path-aware lookup, which guarantees that pages for any vertical will always resolve their own schemas correctly, without restricting filenames.

OR

We can come back and look at build time enforcement once we have some idea on the patterns that are emerging.

@igrigorik

Copy link
Copy Markdown
Contributor

Restrictions on Vertical Autonomy: Enforcing global name uniqueness forces different verticals to use different names for parallel concepts (e.g., we'd have to use new-vertical_status.json vs. shopping_status.json instead of clean, vertical-scoped types/status.json files).

Right, I see this as a feature, not a bug. If we promote something to common, then let's not create a shadow override in sub-verticals -- that violates the contract of common.

@amithanda

Copy link
Copy Markdown
Contributor Author

I think there are 3 issues we should discuss:

1. How should we generate documentation clearly for each vertical? (Path-Aware Resolver)

  • The Problem:
    The MkDocs macro system (main.py) resolves JSON schemas by searching for their basenames (e.g., 'checkout' or 'types/status') across a flat list of directories configured in SCHEMAS_DIRS. The first folder match wins. When multiple verticals exist, this flat lookup causes silent cross-vertical rendering failures.

  • Example (Silent Doc Corruption):
    Suppose we have a new vertical new-vertical alongside shopping, and both define a capability schema checkout.json:

  1. source/schemas/shopping/checkout.json (defines shopping-specific lines, totals, cart links)
  2. source/schemas/new-vertical/checkout.json (defines new-vertical-specific reservation details, identifiers)

Since new-vertical comes first alphabetically (or is ordered before shopping), source/schemas/new-vertical appears before source/schemas/shopping in SCHEMAS_DIRS.
When compiling the shopping checkout page, calling {{ schema_fields('checkout', 'checkout') }} will scan SCHEMAS_DIRS, find new-vertical/checkout.json first, and silently render the new-vertical schema on the shopping documentation page without any build warnings or errors.

  • Recommended Solution:
    Implement a Path-Aware Resolver. We inspect the MkDocs compiler context (env.page.file.src_path) to identify which vertical's page is being built and prioritize that vertical's schema and types directories at the top of the search path.
  • For page specification/new-vertical/checkout.md, search path starts with: [new-vertical/types, new-vertical]
  • For page specification/checkout.md (shopping default), search path starts with: [shopping/types, shopping]

2. Should we enforce that vertical and common namespaces cannot have overlapping names?

  • The Question:
    Should we allow a vertical to define a type name that already exists in the core protocol namespace (e.g., common/types/)?

  • Example:
    If common/types/amount.json exists as the protocol's canonical definition of money, should a vertical be allowed to define source/schemas/shopping/types/amount.json?

  • Recommendation:
    Enforce strict uniqueness (no overlaps).
    The common namespace defines universal protocol primitives. Shadowing a core common type with a vertical-specific version of the same name introduces severe developer confusion and breaks specification predictability. If a vertical needs a different shape, it should define a distinctly named type (e.g., types/billing_amount.json) or propose an update to the common type. We should maintain a build-time guard that fails fast on any common-to-vertical name collision.


3. Should we enforce that filenames be unique between different verticals?

  • The Question:
    Should we force sibling verticals to have globally unique filenames across the entire codebase?

  • Example:
    If the shopping vertical defines a type shopping/types/guest.json, should the new-vertical vertical be forbidden from defining a type new-vertical/types/guest.json?

  • Recommendation:
    Allow overlapping names between verticals (do NOT enforce uniqueness).
    Each vertical represents a separate, modular application domain. Forcing different verticals to coordinate filenames globally goes against vertical autonomy and adds unnecessary prefix boilerplate (e.g., renaming files to shopping_guest.json and new-vertical_guest.json).

Once the Path-Aware Resolver (Point 1) is implemented, the documentation build will resolve vertical-specific files correctly based on the compilation context. Sibling verticals should have the freedom to design their namespaces independently using clean, intuitive terms.

The previous validator (72eca50) overreached in two directions:
- only checked types/ subdirs, missing top-level capability schema
  collisions (e.g. newthing/checkout.json shadowing
  shopping/checkout.json would not have triggered);
- enforced vertical-vs-vertical uniqueness, restricting vertical
  autonomy without justification.

The actual invariant — confirmed in TC on 2026-05-15 — is narrower:
once a basename is claimed by common/ (top-level or types/), no
vertical may use that filename. Verticals are autonomous siblings
and may share filenames with each other; the docs resolver's path
ambiguity for vertical-vs-vertical is a separate follow-up tracked
as the path-aware resolver work.
@igrigorik

Copy link
Copy Markdown
Contributor

@amithanda updated per our TC discussion, PTAL -- right shape?

@amithanda amithanda merged commit 72f5da6 into main May 15, 2026
11 checks passed
@amithanda amithanda deleted the refactor/move-common-types branch May 15, 2026 20:00
igrigorik added a commit that referenced this pull request May 19, 2026
   Upstream main moved 15 widely-shared types from source/schemas/shopping/types/
   to source/schemas/common/types/ (#436) to enable cross-vertical reuse — most
   recently the loyalty extension's common/loyalty.json depends on the relocated
   amount.json.

   Update every `schema=shopping/types/error_response` annotation on our branch
   to `schema=common/types/error_response` so the validator can resolve the
   schema at its new location. 31 annotations across 14 spec docs; pure path
   swap, no semantic change.
igrigorik added a commit that referenced this pull request May 21, 2026
* feat: schema-validated documentation examples

  Introduce a validation harness that ensures every JSON example in the spec
  docs is either validated against UCP schemas or explicitly skipped. This
  catches documentation drift — when schemas change, stale examples break CI
  instead of silently misleading readers.

  ## Contract

  Every ```json block requires an annotation on the preceding line:

      <!-- ucp:example schema=shopping/checkout op=read -->
      <!-- ucp:example schema=shopping/checkout path=$.totals op=read -->
      <!-- ucp:example schema=shopping/cart op=create direction=request -->
      <!-- ucp:example skip reason="JSON-RPC transport binding" -->

  Unannotated blocks are hard failures.

  Principle: "if you open it, you own it." When an example opens an object's
  braces, every required field (per the resolved schema for the declared
  op + direction) must be shown or acknowledged. This applies recursively.

  Three-tier value contract for annotated (non-skip) examples:
  - Full value: validated against schema (types, constraints, enums)
  - Required fields ({ ... }, [ ... ], "..."): field must exists but
    values are optional; scaffold fills it for validation.
  - Absent fields only permitted for optional schema fields

  ## Pipeline

  1. Extract — find annotated ```json blocks (handles tab indentation)
  2. Unwrap — strip HTTP headers from request/response examples
  3. Expand — substitute {{ ucp_version }} with valid date
  4. Preprocess — convert bare ... to valid JSON ({ ... } → {"...":"..."})
  5. Coverage — walk resolved schema, verify required fields acknowledged
  6. Strip — remove ellipsis markers
  7. Merge — deep-merge into scaffold (example wins, scaffold fills gaps)
  8. Validate — ucp-schema validate on merged payload
  9. Report — map errors to source file:line

  ## Bugs caught and fixed

  - checkout.md: item missing required `price` field in disclosure example
  - checkout.md: totals array missing required `subtotal` entry
  - checkout-rest.md: business outcome used non-schema `available_quantity`
    field on line_item; replaced with message-based pattern
  - discount.md (4 instances): `quantity` wrongly nested inside `item`
    object instead of on `line_item` — schema requires quantity at line_item
    level
  - discount.md: `price` and `title` shown in create requests where schema
    omits them (request-only fields: only `item.id` is valid)

  ## Current state

  268 blocks across 39 files:
  - 52 validated (REST request/response pairs, core spec examples, error
    responses, extension examples)
  - 216 skipped (transport bindings, schema definitions, configs — future
    work: payload_path= for JSON-RPC unwrap)

  Run: python3 scripts/validate_examples.py --schema-base source/schemas/

* fix: validate empty request bodies instead of skipping

Address review feedback (drewolson-google): empty `{}` request bodies
for GET and cancel operations are valid requests, not skip-worthy.

The harness now recognizes `{}` as trivially valid — no coverage or
schema validation needed for empty bodies. Changed annotations from
`skip reason="empty request body"` to proper schema declarations.

* feat: validate catalog examples, add def= and ellipsis suppression

Add schema $defs targeting via def= annotation parameter for schemas
that define request/response types in $defs (catalog_search,
catalog_lookup). The harness resolves the schema, extracts the named
$def, and validates against it.

Fix ellipsis handling: [ ... ] and { ... } now preserve field presence
(as [] and {}) instead of stripping the key entirely. Validation errors
at ellipsis-acknowledged paths are suppressed — the coverage check
already verified the field was acknowledged.

Catalog bugs caught and fixed:
- catalog/rest.md: variant missing required description field
- catalog/index.md: product missing description and price_range
- catalog/rest.md: error response wrongly annotated as get_product

14 new validated blocks (catalog REST + index).

* chore: improve overview.md skip annotations with precise reasons

Replace blanket "conceptual/profile fragment" with specific reasons:
service endpoint fragment, schema definition, profile structure,
JSON-RPC transport/error format, payment handler advertisement, etc.

Validate the one block that maps cleanly to a schema (version
unsupported error response → error_response).

1 validated, 30 skipped with descriptive reasons.

* chore: overview.md — validate 2 blocks, precise skip reasons

Validate: complete checkout request (line 1282), version error response
(line 1910). The rest have specific skip reasons:

- 8x JSON-RPC transport (needs payload_path= feature)
- 8x profile documents (ucp metadata nested under "ucp" key, no
  wrapper schema — needs payload_path=$.ucp to extract inner object)
- 3x schema definitions (meta-schemas, not data payloads)
- 2x invalid JSON (contains // comments)
- 6x fragments and declarations

* chore: reconcile annotations after main rebase

Main has moved 28 commits since branch point. Reconcile in two parts:

1. Annotate 11 new doc blocks introduced upstream:
   - core-concepts.md (2): capability declaration + profile fragment
   - schema-authoring.md (3): cancellation_reason / status / totals
     anti-pattern from the new "extensibility" section
   - identity-linking.md (5): scope metadata, OAuth token response,
     profile documents, incomplete checkout fragment (#354 OAuth rewrite)
   - overview.md (1): attribution fragment (#391)

   All map cleanly to existing skip-reason taxonomy.

2. Fix checkout-rest.md:1298 — upstream rewrote the example in #371
   but dropped required top-level fields. Added "currency" and "links"
   to satisfy the schema. The validator caught the bug; this is exactly
   the drift-prevention this work exists for.

After this commit:
  69 passed, 1 failed, 0 errors, 209 skipped

The single remaining failure (discount.md:336 trailing comma) is fixed
by the next commit (// comment + trailing comma stripping).

* feat: strip // comments and trailing commas, validate 8 more blocks

Documentation examples occasionally use JSONC conventions — line
comments to annotate fields, trailing commas for diff hygiene. The
content is still meaningful UCP payload; only the syntax is non-strict.

Add a preprocessing step that strips // comments (outside strings) and
trailing commas before json.loads. Pattern-matching, not a full JSONC
parser — the doc corpus is well-behaved enough that the simpler approach
holds. If we ever hit a false positive (e.g. a string containing `//`
or `,]`), swap in a real JSONC tokenizer.

Convert 8 previously-skipped blocks to validated:
  - ap2-mandates.md (2): merchant_authorization checkout response,
    AP2 complete payment request
  - buyer-consent.md (1): consent-bound checkout response
  - checkout-rest.md (4): three update requests + one read response
  - overview.md (2): two complete checkout requests with // annotations

After this commit:
  79 passed, 0 failed, 0 errors, 200 skipped

* fix(docs): remove trailing commas in JSON examples

Four examples used trailing commas before closing braces, which is
invalid RFC 8259 JSON. The validator's previous lenient stripper
masked these — they parse correctly with strict JSON.

  - ap2-mandates.md: card display.description
  - checkout-rest.md: line_items[0].quantity in update example
  - discount.md: item.id in cart create (introduced via #371)
  - overview.md: payment.instruments token, ap2.checkout_mandate

These fixes stand on their own as bug fixes against strict JSON.
A follow-up commit removes the validator's trailing-comma tolerance.

* feat(validator): formalize JSON example contract

Document the bespoke JSON capability set used in spec examples and
align the validator with that contract. The author-facing guide and
the implementation-facing module docstring describe the same
three-layer model and MUST stay in sync.

Author guide (docs/documentation/schema-authoring.md):
  New "Documenting JSON Examples" section covering annotation grammar,
  supported authoring conveniences (line comments, template variable,
  HTTP envelope, elision markers), explicitly-unsupported features,
  skip-reason taxonomy, and common patterns.

Validator (scripts/validate_examples.py):
  - Replace module docstring with the full three-layer contract.
  - Restructure pipeline into named layer functions:
      reduce_to_canonical_json (Layer 1 -> 2)
      parse_example (Layer 2 boundary)
      process_block (Layer 3 orchestrator)
  - Drop trailing-comma stripping. Was tolerated; now rejected to keep
    wire-format honesty. The four corpus violations were fixed in the
    previous commit.
  - Reject unknown annotation attribute keys (catches typos like
    "shema=" before they become confusing errors).
  - Reject multiple stacked annotations before a fence.
  - Track non-json fence state -- annotations inside any fenced block
    are documentation, not directives, and are now ignored.

  Run: python3 scripts/test_validate_examples.py

* feat(validator): validate profile and docs examples

   Make the docs validator explicit about the small amount of authoring syntax it
   accepts and route final validation back through `ucp-schema`. `extract=` selects
   the nested UCP payload from a displayed envelope, `target=` splices fragments
   into a scaffold and selects the matching sub-schema, and `def=` selects profile
   or schema variants without requiring reader-facing docs to expose JSON Pointer
   machinery. Full examples no longer need fake empty scaffolds; fragments still
   require a real parent scaffold so the final object under validation is
   meaningful.

   Treat profile documents as first-class protocol artifacts rather than a
   discovery implementation detail. Move the canonical profile schema to
   `source/schemas/profile.json`, remove the duplicate discovery schema, and inline
   the reader-facing profile contract into Overview where profile negotiation is
   already explained. The schema keeps a variant-neutral wrapper with
   business/platform validation definitions, requires the registries needed for
   negotiation, keeps `capabilities` optional, and constrains `signing_keys` to
   public EC JWKs so the profile surface stays aligned with the signature spec.

   Convert profile, capability, MCP/JSON-RPC, and fragment examples to validated
   annotations and repair examples where stricter validation exposed drift. MCP
   text fallbacks now use abbreviated serialized JSON instead of unhelpful prose,
   and remaining skipped blocks are limited to non-UCP payloads, schema-authoring
   examples, or formats that do not get a validation schema in this change.

   Add dependency-free validator contract tests for the annotation grammar,
   extraction, target insertion, ellipsis lowering, template expansion, HTTP body
   unwrapping, implicit full-example scaffolds, and trailing-comma rejection.

   Validated with `python3 scripts/test_validate_examples.py`,
   `python3 scripts/validate_examples.py --schema-base source/schemas/ --audit`,
   `python3 scripts/validate_examples.py --schema-base source/schemas/`,
   `ucp-schema lint source/`, and pre-commit on the changed files.

* feat(validator): transport envelope schemas

   Add four narrow envelope schemas under source/schemas/transports/ so
   documentation examples that previously had to skip with reason="JSON-RPC
   transport binding" / "embedded protocol binding" / "A2A transport binding"
   can be machine-validated:

     - jsonrpc.json: JSON-RPC 2.0 request / success response / error response
     - mcp_tool_call.json: UCP's MCP tools/call mapping (params.name,
       params.arguments, result.structuredContent)
     - embedded_message.json: Embedded Protocol envelope and ec.* / ep.cart.*
       method namespaces
     - a2a_message.json: UCP's A2A mapping points (Agent Card extension
       advertisement, Message request wrappers, JSON-RPC responses carrying
       A2A Message results)

   Each schema validates only UCP's mapping into the underlying transport,
   not the transport protocol itself. Operation payloads inside these
   envelopes remain validated by the relevant capability schema.

* docs: validate transport binding examples

   Convert ~30 previously skipped transport-binding blocks to validated
   annotations against the new transports/* schemas:

     - cart-mcp, order-mcp JSON-RPC tools/call requests now validate against
       transports/mcp_tool_call def=request
     - embedded-cart, embedded-checkout, embedded-protocol messages validate
       against transports/embedded_message (def=request / response /
       error_response)
     - overview.md JSON-RPC error responses validate against
       transports/jsonrpc def=error_response
     - signatures.md JWK validates against profile.signing_keys via target=;
       JSON-RPC error response against transports/jsonrpc

   Drive-by cleanups in the same files:

     - cart-rest.md, order-rest.md: HTTP-headers-only blocks moved from
       ```json (with skip) to ```http fences
     - cart-rest.md: cancel block converted from skip to real op=cancel
       validation
     - embedded-checkout.md: JWT structure example moved to ```text fence
       (not JSON)
     - embedded-cart, embedded-checkout, embedded-protocol: replace
       `{/* ... */}` JS-comment elision pseudo-syntax with the validator's
       canonical bare-form `{ ... }` / `[ ... ]`

* a2a: wrap examples in JSON-RPC envelopes

   A2A `message/send` is JSON-RPC 2.0. Prior examples on this page showed
   bare A2A Message payloads, which was internally consistent but didn't
   match what an implementer sees on the wire — and couldn't be validated
   end-to-end against the A2A binding's envelope shape.

   Rewrap every example as a full JSON-RPC request/response:

     jsonrpc: "2.0"
     id: <synthetic 1..N>
     method: "message/send"
     params: { message: { ... } }

   Also replace JS-spread pseudo-syntax (`{...checkoutObject}`,
   `...paymentObject`) with the validator's canonical bare-form elision
   markers (`{ ... }`), and flip skip annotations to validate against
   transports/a2a_message (def=message_request / def=message_response /
   def=agent_card).

   No normative change to the A2A binding contract — just bringing the
   shown payloads in line with what implementations actually exchange.

* point error_response annotations at common/types

   Upstream main moved 15 widely-shared types from source/schemas/shopping/types/
   to source/schemas/common/types/ (#436) to enable cross-vertical reuse — most
   recently the loyalty extension's common/loyalty.json depends on the relocated
   amount.json.

   Update every `schema=shopping/types/error_response` annotation on our branch
   to `schema=common/types/error_response` so the validator can resolve the
   schema at its new location. 31 annotations across 14 spec docs; pure path
   swap, no semantic change.

* validate loyalty extension examples

   The loyalty extension (#340) landed 12 ```json blocks in loyalty.md with
   no annotations, which trips the validator's "no unannotated blocks" rule.
   Annotate each block against the right schema; no changes to the example
   content itself.

* enforce example validation in pre-commit + CI

   CI (.github/workflows/docs.yml):
     - Add `scripts/**` to the pull_request paths filter so PRs touching the
       validator itself trigger the workflow.
     - After the existing `ucp-schema lint source/` step, run the full-corpus
       example validator and its unit tests. Both are fast (~13s combined)
       and use tooling already in setup-build-env (Python via uv, ucp-schema
       binary via cargo install). CI is the mandatory backstop.

   Validator API (scripts/validate_examples.py):
     - `--file` now accepts one or more paths (nargs='+') instead of a single
       path. Backward compatible — single-file usage still works — but enables
       incremental validation when a tool (or pre-commit) knows which files
       actually changed.

   pre-commit (.pre-commit-config.yaml):
     Three hooks at the pre-commit and pre-push stages, scoped by file filter
     for fast local feedback:

     - validate-examples-changed: fires on docs/*.md edits. Validates only
       the changed files via `--file <paths>` (~0.6s per file, typically
       <2s total). Catches direct annotation/schema-binding errors at commit
       time.

     - validate-examples-full: fires on source/schemas/* or validator-code
       edits. Runs the full corpus (~12s) because a single schema change
       can invalidate examples across many docs — the cross-file regression
       case the incremental hook intentionally trades off.

     - validate-examples-tests: fires on validator code/test edits. Runs the
       43 unit tests (~0.3s).

     All three use `language: system`, requiring Python (already needed for
     ruff/pre-commit) and the ucp-schema binary on PATH (cargo install
     ucp-schema). Contributors opt in by running:

         pre-commit install --hook-type pre-commit --hook-type pre-push

     Without the install step, contributors get caught at PR time by CI.
     With it, they get early feedback locally. CI remains the mandatory
     gate; pre-commit/pre-push is the opt-in early-warning system.

* document validator setup, hooks, and CI enforcement

   The "Running the validator locally" subsection of the schema-authoring
   guide was a three-line code snippet. Now that the validator is enforced
   via pre-commit hooks and CI, authors need to know:

     - How to install the tooling (uv sync + cargo install ucp-schema +
       pre-commit install --hook-type pre-commit --hook-type pre-push).
     - Why both hook types matter: pre-commit only installs the pre-commit
       stage hook by default, but this repo also uses pre-push hooks as a
       safety net against --no-verify bypasses.
     - What runs automatically and where — a table covering the three
       enforcement surfaces (pre-commit changed-files, pre-commit/pre-push
       full-corpus, CI full-corpus) with scope and trigger.
     - Why the full-corpus check fires on schema/validator edits but not on
       pure doc edits — the cross-file regression case the incremental
       hook intentionally trades off.
igrigorik added a commit that referenced this pull request May 30, 2026
* feat: schema-validated documentation examples

  Introduce a validation harness that ensures every JSON example in the spec
  docs is either validated against UCP schemas or explicitly skipped. This
  catches documentation drift — when schemas change, stale examples break CI
  instead of silently misleading readers.

  ## Contract

  Every ```json block requires an annotation on the preceding line:

      <!-- ucp:example schema=shopping/checkout op=read -->
      <!-- ucp:example schema=shopping/checkout path=$.totals op=read -->
      <!-- ucp:example schema=shopping/cart op=create direction=request -->
      <!-- ucp:example skip reason="JSON-RPC transport binding" -->

  Unannotated blocks are hard failures.

  Principle: "if you open it, you own it." When an example opens an object's
  braces, every required field (per the resolved schema for the declared
  op + direction) must be shown or acknowledged. This applies recursively.

  Three-tier value contract for annotated (non-skip) examples:
  - Full value: validated against schema (types, constraints, enums)
  - Required fields ({ ... }, [ ... ], "..."): field must exists but
    values are optional; scaffold fills it for validation.
  - Absent fields only permitted for optional schema fields

  ## Pipeline

  1. Extract — find annotated ```json blocks (handles tab indentation)
  2. Unwrap — strip HTTP headers from request/response examples
  3. Expand — substitute {{ ucp_version }} with valid date
  4. Preprocess — convert bare ... to valid JSON ({ ... } → {"...":"..."})
  5. Coverage — walk resolved schema, verify required fields acknowledged
  6. Strip — remove ellipsis markers
  7. Merge — deep-merge into scaffold (example wins, scaffold fills gaps)
  8. Validate — ucp-schema validate on merged payload
  9. Report — map errors to source file:line

  ## Bugs caught and fixed

  - checkout.md: item missing required `price` field in disclosure example
  - checkout.md: totals array missing required `subtotal` entry
  - checkout-rest.md: business outcome used non-schema `available_quantity`
    field on line_item; replaced with message-based pattern
  - discount.md (4 instances): `quantity` wrongly nested inside `item`
    object instead of on `line_item` — schema requires quantity at line_item
    level
  - discount.md: `price` and `title` shown in create requests where schema
    omits them (request-only fields: only `item.id` is valid)

  ## Current state

  268 blocks across 39 files:
  - 52 validated (REST request/response pairs, core spec examples, error
    responses, extension examples)
  - 216 skipped (transport bindings, schema definitions, configs — future
    work: payload_path= for JSON-RPC unwrap)

  Run: python3 scripts/validate_examples.py --schema-base source/schemas/

* fix: validate empty request bodies instead of skipping

Address review feedback (drewolson-google): empty `{}` request bodies
for GET and cancel operations are valid requests, not skip-worthy.

The harness now recognizes `{}` as trivially valid — no coverage or
schema validation needed for empty bodies. Changed annotations from
`skip reason="empty request body"` to proper schema declarations.

* feat: validate catalog examples, add def= and ellipsis suppression

Add schema $defs targeting via def= annotation parameter for schemas
that define request/response types in $defs (catalog_search,
catalog_lookup). The harness resolves the schema, extracts the named
$def, and validates against it.

Fix ellipsis handling: [ ... ] and { ... } now preserve field presence
(as [] and {}) instead of stripping the key entirely. Validation errors
at ellipsis-acknowledged paths are suppressed — the coverage check
already verified the field was acknowledged.

Catalog bugs caught and fixed:
- catalog/rest.md: variant missing required description field
- catalog/index.md: product missing description and price_range
- catalog/rest.md: error response wrongly annotated as get_product

14 new validated blocks (catalog REST + index).

* chore: improve overview.md skip annotations with precise reasons

Replace blanket "conceptual/profile fragment" with specific reasons:
service endpoint fragment, schema definition, profile structure,
JSON-RPC transport/error format, payment handler advertisement, etc.

Validate the one block that maps cleanly to a schema (version
unsupported error response → error_response).

1 validated, 30 skipped with descriptive reasons.

* chore: overview.md — validate 2 blocks, precise skip reasons

Validate: complete checkout request (line 1282), version error response
(line 1910). The rest have specific skip reasons:

- 8x JSON-RPC transport (needs payload_path= feature)
- 8x profile documents (ucp metadata nested under "ucp" key, no
  wrapper schema — needs payload_path=$.ucp to extract inner object)
- 3x schema definitions (meta-schemas, not data payloads)
- 2x invalid JSON (contains // comments)
- 6x fragments and declarations

* chore: reconcile annotations after main rebase

Main has moved 28 commits since branch point. Reconcile in two parts:

1. Annotate 11 new doc blocks introduced upstream:
   - core-concepts.md (2): capability declaration + profile fragment
   - schema-authoring.md (3): cancellation_reason / status / totals
     anti-pattern from the new "extensibility" section
   - identity-linking.md (5): scope metadata, OAuth token response,
     profile documents, incomplete checkout fragment (#354 OAuth rewrite)
   - overview.md (1): attribution fragment (#391)

   All map cleanly to existing skip-reason taxonomy.

2. Fix checkout-rest.md:1298 — upstream rewrote the example in #371
   but dropped required top-level fields. Added "currency" and "links"
   to satisfy the schema. The validator caught the bug; this is exactly
   the drift-prevention this work exists for.

After this commit:
  69 passed, 1 failed, 0 errors, 209 skipped

The single remaining failure (discount.md:336 trailing comma) is fixed
by the next commit (// comment + trailing comma stripping).

* feat: strip // comments and trailing commas, validate 8 more blocks

Documentation examples occasionally use JSONC conventions — line
comments to annotate fields, trailing commas for diff hygiene. The
content is still meaningful UCP payload; only the syntax is non-strict.

Add a preprocessing step that strips // comments (outside strings) and
trailing commas before json.loads. Pattern-matching, not a full JSONC
parser — the doc corpus is well-behaved enough that the simpler approach
holds. If we ever hit a false positive (e.g. a string containing `//`
or `,]`), swap in a real JSONC tokenizer.

Convert 8 previously-skipped blocks to validated:
  - ap2-mandates.md (2): merchant_authorization checkout response,
    AP2 complete payment request
  - buyer-consent.md (1): consent-bound checkout response
  - checkout-rest.md (4): three update requests + one read response
  - overview.md (2): two complete checkout requests with // annotations

After this commit:
  79 passed, 0 failed, 0 errors, 200 skipped

* fix(docs): remove trailing commas in JSON examples

Four examples used trailing commas before closing braces, which is
invalid RFC 8259 JSON. The validator's previous lenient stripper
masked these — they parse correctly with strict JSON.

  - ap2-mandates.md: card display.description
  - checkout-rest.md: line_items[0].quantity in update example
  - discount.md: item.id in cart create (introduced via #371)
  - overview.md: payment.instruments token, ap2.checkout_mandate

These fixes stand on their own as bug fixes against strict JSON.
A follow-up commit removes the validator's trailing-comma tolerance.

* feat(validator): formalize JSON example contract

Document the bespoke JSON capability set used in spec examples and
align the validator with that contract. The author-facing guide and
the implementation-facing module docstring describe the same
three-layer model and MUST stay in sync.

Author guide (docs/documentation/schema-authoring.md):
  New "Documenting JSON Examples" section covering annotation grammar,
  supported authoring conveniences (line comments, template variable,
  HTTP envelope, elision markers), explicitly-unsupported features,
  skip-reason taxonomy, and common patterns.

Validator (scripts/validate_examples.py):
  - Replace module docstring with the full three-layer contract.
  - Restructure pipeline into named layer functions:
      reduce_to_canonical_json (Layer 1 -> 2)
      parse_example (Layer 2 boundary)
      process_block (Layer 3 orchestrator)
  - Drop trailing-comma stripping. Was tolerated; now rejected to keep
    wire-format honesty. The four corpus violations were fixed in the
    previous commit.
  - Reject unknown annotation attribute keys (catches typos like
    "shema=" before they become confusing errors).
  - Reject multiple stacked annotations before a fence.
  - Track non-json fence state -- annotations inside any fenced block
    are documentation, not directives, and are now ignored.

  Run: python3 scripts/test_validate_examples.py

* feat(validator): validate profile and docs examples

   Make the docs validator explicit about the small amount of authoring syntax it
   accepts and route final validation back through `ucp-schema`. `extract=` selects
   the nested UCP payload from a displayed envelope, `target=` splices fragments
   into a scaffold and selects the matching sub-schema, and `def=` selects profile
   or schema variants without requiring reader-facing docs to expose JSON Pointer
   machinery. Full examples no longer need fake empty scaffolds; fragments still
   require a real parent scaffold so the final object under validation is
   meaningful.

   Treat profile documents as first-class protocol artifacts rather than a
   discovery implementation detail. Move the canonical profile schema to
   `source/schemas/profile.json`, remove the duplicate discovery schema, and inline
   the reader-facing profile contract into Overview where profile negotiation is
   already explained. The schema keeps a variant-neutral wrapper with
   business/platform validation definitions, requires the registries needed for
   negotiation, keeps `capabilities` optional, and constrains `signing_keys` to
   public EC JWKs so the profile surface stays aligned with the signature spec.

   Convert profile, capability, MCP/JSON-RPC, and fragment examples to validated
   annotations and repair examples where stricter validation exposed drift. MCP
   text fallbacks now use abbreviated serialized JSON instead of unhelpful prose,
   and remaining skipped blocks are limited to non-UCP payloads, schema-authoring
   examples, or formats that do not get a validation schema in this change.

   Add dependency-free validator contract tests for the annotation grammar,
   extraction, target insertion, ellipsis lowering, template expansion, HTTP body
   unwrapping, implicit full-example scaffolds, and trailing-comma rejection.

   Validated with `python3 scripts/test_validate_examples.py`,
   `python3 scripts/validate_examples.py --schema-base source/schemas/ --audit`,
   `python3 scripts/validate_examples.py --schema-base source/schemas/`,
   `ucp-schema lint source/`, and pre-commit on the changed files.

* feat(validator): transport envelope schemas

   Add four narrow envelope schemas under source/schemas/transports/ so
   documentation examples that previously had to skip with reason="JSON-RPC
   transport binding" / "embedded protocol binding" / "A2A transport binding"
   can be machine-validated:

     - jsonrpc.json: JSON-RPC 2.0 request / success response / error response
     - mcp_tool_call.json: UCP's MCP tools/call mapping (params.name,
       params.arguments, result.structuredContent)
     - embedded_message.json: Embedded Protocol envelope and ec.* / ep.cart.*
       method namespaces
     - a2a_message.json: UCP's A2A mapping points (Agent Card extension
       advertisement, Message request wrappers, JSON-RPC responses carrying
       A2A Message results)

   Each schema validates only UCP's mapping into the underlying transport,
   not the transport protocol itself. Operation payloads inside these
   envelopes remain validated by the relevant capability schema.

* docs: validate transport binding examples

   Convert ~30 previously skipped transport-binding blocks to validated
   annotations against the new transports/* schemas:

     - cart-mcp, order-mcp JSON-RPC tools/call requests now validate against
       transports/mcp_tool_call def=request
     - embedded-cart, embedded-checkout, embedded-protocol messages validate
       against transports/embedded_message (def=request / response /
       error_response)
     - overview.md JSON-RPC error responses validate against
       transports/jsonrpc def=error_response
     - signatures.md JWK validates against profile.signing_keys via target=;
       JSON-RPC error response against transports/jsonrpc

   Drive-by cleanups in the same files:

     - cart-rest.md, order-rest.md: HTTP-headers-only blocks moved from
       ```json (with skip) to ```http fences
     - cart-rest.md: cancel block converted from skip to real op=cancel
       validation
     - embedded-checkout.md: JWT structure example moved to ```text fence
       (not JSON)
     - embedded-cart, embedded-checkout, embedded-protocol: replace
       `{/* ... */}` JS-comment elision pseudo-syntax with the validator's
       canonical bare-form `{ ... }` / `[ ... ]`

* a2a: wrap examples in JSON-RPC envelopes

   A2A `message/send` is JSON-RPC 2.0. Prior examples on this page showed
   bare A2A Message payloads, which was internally consistent but didn't
   match what an implementer sees on the wire — and couldn't be validated
   end-to-end against the A2A binding's envelope shape.

   Rewrap every example as a full JSON-RPC request/response:

     jsonrpc: "2.0"
     id: <synthetic 1..N>
     method: "message/send"
     params: { message: { ... } }

   Also replace JS-spread pseudo-syntax (`{...checkoutObject}`,
   `...paymentObject`) with the validator's canonical bare-form elision
   markers (`{ ... }`), and flip skip annotations to validate against
   transports/a2a_message (def=message_request / def=message_response /
   def=agent_card).

   No normative change to the A2A binding contract — just bringing the
   shown payloads in line with what implementations actually exchange.

* point error_response annotations at common/types

   Upstream main moved 15 widely-shared types from source/schemas/shopping/types/
   to source/schemas/common/types/ (#436) to enable cross-vertical reuse — most
   recently the loyalty extension's common/loyalty.json depends on the relocated
   amount.json.

   Update every `schema=shopping/types/error_response` annotation on our branch
   to `schema=common/types/error_response` so the validator can resolve the
   schema at its new location. 31 annotations across 14 spec docs; pure path
   swap, no semantic change.

* validate loyalty extension examples

   The loyalty extension (#340) landed 12 ```json blocks in loyalty.md with
   no annotations, which trips the validator's "no unannotated blocks" rule.
   Annotate each block against the right schema; no changes to the example
   content itself.

* enforce example validation in pre-commit + CI

   CI (.github/workflows/docs.yml):
     - Add `scripts/**` to the pull_request paths filter so PRs touching the
       validator itself trigger the workflow.
     - After the existing `ucp-schema lint source/` step, run the full-corpus
       example validator and its unit tests. Both are fast (~13s combined)
       and use tooling already in setup-build-env (Python via uv, ucp-schema
       binary via cargo install). CI is the mandatory backstop.

   Validator API (scripts/validate_examples.py):
     - `--file` now accepts one or more paths (nargs='+') instead of a single
       path. Backward compatible — single-file usage still works — but enables
       incremental validation when a tool (or pre-commit) knows which files
       actually changed.

   pre-commit (.pre-commit-config.yaml):
     Three hooks at the pre-commit and pre-push stages, scoped by file filter
     for fast local feedback:

     - validate-examples-changed: fires on docs/*.md edits. Validates only
       the changed files via `--file <paths>` (~0.6s per file, typically
       <2s total). Catches direct annotation/schema-binding errors at commit
       time.

     - validate-examples-full: fires on source/schemas/* or validator-code
       edits. Runs the full corpus (~12s) because a single schema change
       can invalidate examples across many docs — the cross-file regression
       case the incremental hook intentionally trades off.

     - validate-examples-tests: fires on validator code/test edits. Runs the
       43 unit tests (~0.3s).

     All three use `language: system`, requiring Python (already needed for
     ruff/pre-commit) and the ucp-schema binary on PATH (cargo install
     ucp-schema). Contributors opt in by running:

         pre-commit install --hook-type pre-commit --hook-type pre-push

     Without the install step, contributors get caught at PR time by CI.
     With it, they get early feedback locally. CI remains the mandatory
     gate; pre-commit/pre-push is the opt-in early-warning system.

* document validator setup, hooks, and CI enforcement

   The "Running the validator locally" subsection of the schema-authoring
   guide was a three-line code snippet. Now that the validator is enforced
   via pre-commit hooks and CI, authors need to know:

     - How to install the tooling (uv sync + cargo install ucp-schema +
       pre-commit install --hook-type pre-commit --hook-type pre-push).
     - Why both hook types matter: pre-commit only installs the pre-commit
       stage hook by default, but this repo also uses pre-push hooks as a
       safety net against --no-verify bypasses.
     - What runs automatically and where — a table covering the three
       enforcement surfaces (pre-commit changed-files, pre-commit/pre-push
       full-corpus, CI full-corpus) with scope and trigger.
     - Why the full-corpus check fires on schema/validator edits but not on
       pure doc edits — the cross-file regression case the incremental
       hook intentionally trades off.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

TC review Ready for TC review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants