Skip to content

fix(server): recurse portable_schema into all draft-2020-12 positions - #126

Merged
plusky merged 1 commit into
mainfrom
fix/issue-120
Aug 18, 2026
Merged

fix(server): recurse portable_schema into all draft-2020-12 positions#126
plusky merged 1 commit into
mainfrom
fix/issue-120

Conversation

@plusky

@plusky plusky commented Aug 18, 2026

Copy link
Copy Markdown
Owner

What

portable_schema and its mirror walk now recurse into every draft
2020-12 subschema position a #[tool] param struct can emit:

  • properties (already visited)
  • items as an object or as an array of schemas
  • object-form additionalProperties
  • patternProperties values
  • prefixItems

Boolean additionalProperties (the two custom_fields params) is not
a subschema and is left alone. Other applicator keywords (anyOf /
oneOf / allOf / $ref / $defs / not / if / then / else
/ contains / propertyNames / dependentSchemas / …) stay absent
from served schemas; the mirror test fails if one appears.

Canaries plant a type union, "default": null, and format: "uint64"
under each newly covered keyword. Live #[tool] structs do not emit
those positions today, so deleting an arm would have shipped green
against served_tool_schemas_are_client_portable alone.

DESIGN.md's schemars bullet now attributes stray "default": null to
#[serde(default)] (not schemars' rendering of Option<T>), and
marks enum in PORTABLE_FORMATS as a dead keep-list entry — not
load-bearing.

Why

PR #119's pass and its mirror test shared the same recursion blind
spot. A future param like HashMap<String, u64> (object-schema
additionalProperties carrying format: "uint64") or a tuple
(prefixItems carrying a type union) would have been invisible to
both walks, so a Gemini/Vertex regression would ship green.

Schema advertisement only: serde still accepts an explicit null for
every Option<T> field. No tool's accepted input changed, and no
invariant (I1–I16) is involved.

How verified

  • cargo fmt --check clean
  • cargo clippy --workspace --all-targets -- -D warnings clean
  • cargo clippy -p bugwarden --features gen --all-targets -- -D warnings clean
  • cargo test --workspace --all-targets --locked green
  • cargo deny check clean (existing duplicate-crate warnings only)
  • optional_params_still_accept_an_explicit_null still passes

Mutation: delete the prefixItems arm

portable_schema's prefixItems block was removed and the canary
suite was rerun with --no-fail-fast. Only the planted prefixItems
node stayed dirty; the other new arms and the mirror walk still
passed:

$ cargo test -p bugwarden --lib --locked --no-fail-fast -- portable_schema_normalizes mirror_walk

running 5 tests
test server::tests::portable_schema_normalizes_array_form_items ... ok
test server::tests::portable_schema_normalizes_additional_properties ... ok
test server::tests::mirror_walk_rejects_a_dirty_node_under_every_new_draft_position ... ok
test server::tests::portable_schema_normalizes_pattern_properties ... ok
test server::tests::portable_schema_normalizes_prefix_items ... FAILED

---- server::tests::portable_schema_normalizes_prefix_items stdout ----
assertion `left == right` failed: prefixItems: type union was not collapsed: Object {"default": Null, "format": String("uint64"), "type": Array [String("integer"), String("null")]}
  left: Some(Array [String("integer"), String("null")])
 right: Some(String("integer"))

failures:
    server::tests::portable_schema_normalizes_prefix_items

test result: FAILED. 4 passed; 1 failed

The arm was restored before commit.

Adversarial review

  • The two walks visit the same keywords in the same order
    (properties, items object-or-array, object-form
    additionalProperties, patternProperties, prefixItems).
  • A deleted prefixItems arm is killed (evidence above).
  • DESIGN.md nits are corrected: #[serde(default)] attribution, and
    enum is explicitly a dead keep-list entry.
  • No accepted-input change (no Deserialize / handler edits).

Closes #120

The pass and its mirror test only visited `properties` and `items`,
so a future HashMap value or tuple slot could keep a type union or
`uint64` format that Gemini/Vertex would refuse. Recurse into
object-form `additionalProperties`, `patternProperties` values,
`prefixItems`, and array-form `items`. Boolean
`additionalProperties` is not a subschema and is skipped.

Canaries plant a non-portable node under each new keyword so
deleting an arm fails even though no live `#[tool]` struct emits
those positions today. DESIGN.md now attributes stray
`"default": null` to `#[serde(default)]` and marks `enum` as a
dead keep-list entry.

Closes #120
@plusky plusky added the ai-assisted Authored or substantially written with an AI coding agent label Aug 18, 2026
@plusky
plusky merged commit 2164d14 into main Aug 18, 2026
13 checks passed
@plusky
plusky deleted the fix/issue-120 branch August 18, 2026 18:33
@plusky plusky mentioned this pull request Aug 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-assisted Authored or substantially written with an AI coding agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

portable_schema and its mirror test share a recursion blind spot (additionalProperties, prefixItems)

1 participant