Skip to content

feat(codegen): add capability reachability, composition, and generate_types() API - #83

Merged
gsmith85 merged 12 commits into
mainfrom
feat/codegen-compile-types
Oct 15, 2026
Merged

gsmith85 merged 12 commits into
mainfrom
feat/codegen-compile-types

Conversation

@gsmith85

@gsmith85 gsmith85 commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Implements Task 3 (PR 1c) of the two-layer ucp-schema code-generation pipeline: capability/extension reachability discovery, $defs hoisting & collision qualification, extension composition, and the public compile_types() / generate_types() library API.

Pipeline Stages Added

  • Stage 1 (src/codegen/reachability.rs):
    • Discovers all .json schemas in --schema-dir using schema structure (name/version capability identity, extends metadata, ucp_request/ucp_response directional annotations, and $defs reverse-domain parent inference), supports reverse-domain FQDN (dev.ucp.shopping.checkout) matching, reclassifies extension schemas passed via --capability, rejects non-extension capabilities passed via --extension, seeds ambient protocol roots (profile, ucp, capability, service, payment_handler, error_response), and computes the transitive $ref closure while pruning inactive extension $defs.
  • Stage 2 (src/codegen/hoist.rs):
    • Classifies and hoists $defs entries into the top-level $defs bundle, skips pure re-exports, extracts self-named capability role schemas (*PlatformSchema, *BusinessSchema, *ResponseSchema), and parent-qualifies colliding local $defs (PaymentAp2MandateErrorCode, JsonrpcErrorResponse, A2aMessageMessage).
  • Stage 3 (src/codegen/compose.rs):
    • Composes active extensions into root capabilities (resolving root definitions via schema structure or self-named definitions), in-place standalone sub-type overlays (Buyer, Payment, PaymentInstrument, Cart -> Checkout), and container capability operations (CatalogSearch*, CatalogLookup*, supporting both nested $defs and oneOf/allOf extension shapes).
  • Stages 5, 6, 8 & Public API (src/codegen/mod.rs, src/lib.rs):
    • Orchestrates directional slicing, $ref alignment, and empty-object pruning (retaining ErrorResponse and definitions referenced via $ref anywhere in $defs or root properties), and exposes GenerateTypesOptions, TypesBundleDoc, CompiledTypes, CodegenError, compile_types(), and generate_types().

Stacked PR Series Roadmap

  1. PR 1a (feat(codegen): add identifier naming, UCP keyword stripping, and $defs ref rewriting #81): Identifier qualification, UCP keyword stripping, $ref rewriting (src/codegen/normalizer.rs)
  2. PR 1b (feat(codegen): add directional schema slicing and $ref alignment #82): Directional schema slicing & $ref alignment (src/codegen/normalizer.rs)
  3. PR 1c (this PR): Capability/extension reachability, composition & generate_types() library API (src/codegen/{mod,reachability,hoist,compose}.rs)
  4. PR 1d (feat(cli): add generate-types subcommand #84): CLI generate-types subcommand (src/bin/ucp-schema.rs, tests/generate_types_test.rs)
  5. PR 2 (feat(codegen): add self-contained .well-known/ucp profile scoping for generate-types #85): Self-contained .well-known/ucp --profile scoping (src/codegen/profile.rs)
  6. PR 3a (feat(codegen): add AST shape normalizers and inline conditional variant hoisting #86): Codegen AST shape normalizers & inline conditional variant hoisting (src/codegen/normalizer.rs, src/codegen/reify.rs)
  7. PR 3b (feat(codegen): lower open string discriminator unions into ordered anyOf with <Union>Base #87): Open string discriminator union lowering (<Union>Base) (src/codegen/reify.rs)
  8. PR 3c (feat(codegen): discover and register decentralized extended subtypes into open unions #88): Cross-file extended subtype discovery & registration (src/codegen/reify.rs)
  9. PR 4a (python-sdk#101): Python SDK direct JSON Schema codegen (generate_models.sh, postprocess_models.py, src/ucp_sdk/models/)
  10. PR 4b (js-sdk#87): JS/TS SDK direct JSON Schema codegen (generate_models.sh, scripts/generate-zod-from-types.mjs, src/spec_generated.ts)
  11. PR 5 (feat(openapi): add profile-driven OpenAPI 3.1 service and type binding (generate-openapi) #89): OpenAPI 3.1 REST service & type binding (ucp-schema generate-openapi, src/openapi/mod.rs)

Test Plan

  • cargo fmt --check
  • cargo test --all-targets
  • cargo clippy --all-targets -- -D warnings

@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Oct 1, 2026
gsmith85 added a commit that referenced this pull request Oct 1, 2026
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline:
- Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty.
- Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>.
- Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes.

Stacked PR Roadmap:
- PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting
- PR 1b (#82): Directional schema slicing and $ref alignment
- PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API
- PR 1d (this PR): CLI generate-types subcommand
- PR 2: Self-contained .well-known/ucp --profile scoping
- PR 3: Inline conditional variant hoisting and ordered anyOf union lowering
- PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
gsmith85 added a commit that referenced this pull request Oct 1, 2026
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline:
- Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty.
- Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>.
- Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes.

Stacked PR Roadmap:
- PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting
- PR 1b (#82): Directional schema slicing and $ref alignment
- PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API
- PR 1d (this PR): CLI generate-types subcommand
- PR 2: Self-contained .well-known/ucp --profile scoping
- PR 3: Inline conditional variant hoisting and ordered anyOf union lowering
- PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
gsmith85 added a commit that referenced this pull request Oct 1, 2026
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline:
- Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty.
- Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>.
- Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes.

Stacked PR Roadmap:
- PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting
- PR 1b (#82): Directional schema slicing and $ref alignment
- PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API
- PR 1d (this PR): CLI generate-types subcommand
- PR 2: Self-contained .well-known/ucp --profile scoping
- PR 3: Inline conditional variant hoisting and ordered anyOf union lowering
- PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
@gsmith85
gsmith85 force-pushed the feat/codegen-slicing branch from b6fcada to 88c578b Compare October 1, 2026 23:45
gsmith85 added a commit that referenced this pull request Oct 1, 2026
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline:
- Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty.
- Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>.
- Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes.

Stacked PR Roadmap:
- PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting
- PR 1b (#82): Directional schema slicing and $ref alignment
- PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API
- PR 1d (this PR): CLI generate-types subcommand
- PR 2: Self-contained .well-known/ucp --profile scoping
- PR 3: Inline conditional variant hoisting and ordered anyOf union lowering
- PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
@gsmith85
gsmith85 force-pushed the feat/codegen-compile-types branch from 19346d4 to 1ab4348 Compare October 1, 2026 23:45
gsmith85 added a commit that referenced this pull request Oct 2, 2026
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline:
- Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty.
- Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>.
- Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes.

Stacked PR Roadmap:
- PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting
- PR 1b (#82): Directional schema slicing and $ref alignment
- PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API
- PR 1d (this PR): CLI generate-types subcommand
- PR 2: Self-contained .well-known/ucp --profile scoping
- PR 3: Inline conditional variant hoisting and ordered anyOf union lowering
- PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
gsmith85 added a commit that referenced this pull request Oct 2, 2026
Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline:
- Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty.
- Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>.
- Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes.

Stacked PR Roadmap:
- PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting
- PR 1b (#82): Directional schema slicing and $ref alignment
- PR 1c (#83): Capability/extension reachability, composition, and generate_types() library API
- PR 1d (this PR): CLI generate-types subcommand
- PR 2: Self-contained .well-known/ucp --profile scoping
- PR 3: Inline conditional variant hoisting and ordered anyOf union lowering
- PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
@gsmith85
gsmith85 force-pushed the feat/codegen-compile-types branch from a85a726 to 8cac42b Compare October 2, 2026 16:37
@gsmith85
gsmith85 force-pushed the feat/codegen-slicing branch from 88c578b to 4ed1ea1 Compare October 2, 2026 16:37
@gsmith85
gsmith85 force-pushed the feat/codegen-slicing branch from 4ed1ea1 to 7b74335 Compare October 7, 2026 06:00
@gsmith85
gsmith85 force-pushed the feat/codegen-compile-types branch from 8cac42b to 9d93d16 Compare October 7, 2026 06:00
@gsmith85
gsmith85 force-pushed the feat/codegen-slicing branch from 7b74335 to 9380b3c Compare October 7, 2026 22:04
@gsmith85
gsmith85 force-pushed the feat/codegen-compile-types branch from 9d93d16 to 33ec2f4 Compare October 7, 2026 22:04
@gsmith85
gsmith85 force-pushed the feat/codegen-slicing branch from 9380b3c to e626830 Compare October 8, 2026 00:32
@gsmith85
gsmith85 force-pushed the feat/codegen-compile-types branch from 33ec2f4 to 31aae2f Compare October 8, 2026 00:32

@damaz91 damaz91 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.

Great work on the reachability, $defs hoisting, and extension composition stages!

Left a few inline comments on open edge cases and clippy::nonminimal_bool lints that are still present at the top of the stack.

(Note: Also noticed a few items here—such as normalize_lexical_path dropping leading .. on relative paths, recursive allOf handling in merge_extension_into_schema, removing unused schema_remote_base, force_slice on response-only capabilities in Stage 5, and relaxing required on extension-contributed overlay properties—that are already addressed up-stack in #85, #86, and #88, so skipping those here. Minor nit: the Stage 1 bullet in the PR description still mentions short-name checkout matching, which was removed in d1ca9e4.)

Comment thread src/codegen/compose.rs Resolved
Comment thread src/codegen/reachability.rs Resolved
Comment thread src/codegen/mod.rs Resolved
Comment thread src/codegen/mod.rs Resolved
@gsmith85
gsmith85 force-pushed the feat/codegen-compile-types branch from 31aae2f to b1e72bf Compare October 14, 2026 22:04
@gsmith85
gsmith85 force-pushed the feat/codegen-slicing branch from e626830 to dd19a22 Compare October 14, 2026 22:04
Base automatically changed from feat/codegen-slicing to main October 14, 2026 22:19
…_types() API

Implement Task 3 (PR 1c) of the ucp-schema codegen pipeline:
- Stage 1 (src/codegen/reachability.rs): Schema discovery, short/reverse-domain capability and extension selection, extension reclassification, ambient protocol root seeding, and transitive $ref reachability closure with inactive extension $defs pruning.
- Stage 2 (src/codegen/hoist.rs): Upfront $defs hoisting, pure re-export skipping, self-named role container extraction (*PlatformSchema, *BusinessSchema, *ResponseSchema), and collision-aware parent qualification.
- Stage 3 (src/codegen/compose.rs): Active extension composition into root capabilities, in-place standalone sub-type overlays (Buyer, Payment, PaymentInstrument, Cart->Checkout), and container capability operations (both nested $defs and oneOf/allOf shapes).
- Stages 5, 6, 8 & Public API (src/codegen/mod.rs, src/lib.rs): Directional slicing, $ref alignment, empty-object pruning (retaining ErrorResponse), and public GenerateTypesOptions, TypesBundleDoc, CompiledTypes, CodegenError, compile_types(), and generate_types() APIs.

Stacked PR Roadmap:
- PR 1a (#81): Identifier qualification, UCP keyword stripping, $ref rewriting
- PR 1b (#82): Directional schema slicing and $ref alignment
- PR 1c (this PR): Capability/extension reachability, composition, and generate_types() library API
- PR 2: CLI generate-types subcommand and --profile loading
- PR 3: Inline conditional variant hoisting and ordered anyOf union lowering
- PR 4: OpenAPI 3.1 REST binding assembly (generate-openapi)
@gsmith85
gsmith85 force-pushed the feat/codegen-compile-types branch from b1e72bf to ef101e9 Compare October 14, 2026 23:23
@gsmith85
gsmith85 force-pushed the feat/codegen-compile-types branch from ef101e9 to bebd94f Compare October 15, 2026 01:18

@damaz91 damaz91 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 for addressing all comments - LGTM!

@jingyli jingyli left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Thanks @gsmith85! Approving the PR but added 3 comments to account for some noticeable edge cases that might come up in the future, but should mostly be a no-problem today 😃.

Comment thread src/codegen/mod.rs Outdated

pub(super) fn collision_qualified_name(parent_pascal: &str, def_key: &str) -> String {
let def_pascal = to_pascal_case(def_key);
if def_pascal.starts_with(parent_pascal) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One potential edge case to keeping def_pascal unchanged if it already starts with the parent's name would be if the def_key truly collides with a standalone type - this would result in unintentional overwrites if we are not being careful.

An example in shopping/fulfillment.json:

  • The def keys include fulfillment_option, fulfilment_group, fulfillment_method, etc., which match exactly with the actual schema file names in shopping/types/*.
  • Although today the def definitions contain pure 1:1 $ref exports to the type schemas and would be a no problem, if we ever add any new properties to the def keys directly in the future, this function would actually output the exact same qualified name for these def keys as the types schemas (and cause any unintentional overwrites down the line).

Maybe always returning the else-clause may be better here so we guarantee a new name is always returned (even if it may lead to duplicated parent prefixes like FulfillmentFulfillmentMethod)..?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Great catch! Fixed in d685121: collision_qualified_name now unconditionally prefixes parent_pascal (format!("{parent_pascal}{}", to_pascal_case(def_key))) so any local $defs entry that collides with a standalone type or sibling $defs key is guaranteed to receive a distinct name.

Comment thread src/codegen/reachability.rs Outdated
.and_then(Value::as_str)
.filter(|s| is_reverse_domain_name(s))
.map(str::to_string);
let in_types_dir = path.components().any(|c| c.as_os_str() == "types");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Another edge case: It looks like path holds the local absolute path, so if for whatever reason the schema files are located under a root path that already includes types (e.g., /home/user/types/ucp/), then this check will result in in_types_dir = true for all the files.

When we then do the is_capability check below, it will always return false and then all the capability related processing (e.g., request type resolution, extension composition, etc.) will be skipped.

The same edge case applies to the in_transports_dir check as well.

Maybe we should do another mutation on top of file_path to strip out the prefix before storing it back into LoadedSchema::path?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good call! Fixed in d685121: LoadedSchema::from_file now checks the immediate parent directory name (path.parent().and_then(Path::file_name)) for "types" and "transports", so ancestor workspace directories with those names no longer misclassify capability schemas.

Comment thread src/codegen/compose.rs
};
for key in ["$ref", "ucp_request", "ucp_response", "description"] {
if let Some(val) = ext_obj.get(key) {
base_obj.insert(key.to_string(), val.clone());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

One small thing here is that this would always result in an overwrite, especially given extensions are processed via active_ext_indices (basically the last processed extension's value for these keys will remain).

While for most of the extensions we have today, this wouldn't be a big problem they shouldn't' be operating on the same base property - the only exception would be any payments related extensions (e.g., if I have both split payments and payment terms extensions active, since both of them modify on top of payment property in capabilities like Checkout, the description would effectively be overwritten).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense! Overwriting description and ucp_request when an extension refines a base capability property is needed for single-property refinements (such as buyer_consent.json refining Checkout.properties.buyer.description and ucp_request.complete), and today the payment extensions touch disjoint levels (payment_split_payments.json refines payment.properties.instruments, which is redirected onto Payment, while payment_terms.json sets payment.description and merges terms / selected_term_id into Payment). If future extensions ever supply competing descriptions on the same base property, we can concatenate or prioritize them in a follow-up.

Implement Task 4 (PR 1d) of the ucp-schema codegen pipeline:
- Wire Commands::GenerateTypes in src/bin/ucp-schema.rs with --profile, --capability, --extension, --schema-dir (alias --schema-local-base), --schema-remote-base, --output, and --pretty.
- Implement CliExitCode for CodegenError and generalize write_json_output to <T: serde::Serialize + ?Sized>.
- Add CLI integration tests in tests/generate_types_test.rs covering stdout pretty/compact output, file output, repeatable/comma-delimited flags, and error exit codes.
@gsmith85
gsmith85 merged commit 2155380 into main Oct 15, 2026
19 checks passed
@gsmith85
gsmith85 deleted the feat/codegen-compile-types branch October 15, 2026 22:41
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.

3 participants