Skip to content

Capability map records no body fields for array-bodied endpoints (schema walk never descends through items) #82

Description

Summary

The capability map's schema walk never descends through an array schema's items, so
every array-bodied endpoint records "bodyProperties": {}. Four endpoints are affected
and 23 body fields are invisible to both the runtime capability gate and the drift report.

Unlike #71 (which records fields with a wrong introducedVersion), these fields are
recorded not at all, and the defect reproduces on the current spec rather than
self-healing at 2.17.

Affected endpoints

All four array-bodied operations in fb2.28, all with bodyProperties: {}:

Endpoint minVersion Element schema Recoverable fields Recorded
POST /nodes/batch 2.18 NodePost 12 0
POST /resource-accesses/batch 2.19 ResourceAccessPost 2 0
PUT /workloads/tags/batch 2.23 TagBatch 5 0
POST /fleets/members/batch 2.27 FleetMemberBatchPost 4 0

The fields exist in the spec, one hop below where the walk stops:

NodePost              capacity, chassis_serial_number, data_addresses, details, id,
                      management_address, name, node_key, raw_capacity, serial_number,
                      status, unique
ResourceAccessPost    resource, scope
TagBatch              copyable, key, namespace, resource, value
FleetMemberBatchPost  authentication_credentials, ca_certificate, ca_certificate_group,
                      management_address

Root cause

Add-PfbSchemaPropertyNodes in tools/lib/PfbSpecTools.ps1 — the recursive helper behind
Get-PfbSchemaPropertyNames/Get-PfbSchemaPropertyDetails — has branches for $ref
(via Resolve-PfbRef), allOf, properties and required. It has no branch for
items
, and none for oneOf/anyOf.

The request body for PUT /workloads/tags/batch in fb2.28:

{ "maxItems": 30, "minItems": 1, "uniqueItems": true,
  "type": "array",
  "items": { "$ref": "#/components/schemas/TagBatch" } }

Resolve-PfbRef returns that array node unchanged. It has no properties key and no
allOf, so the accumulator adds nothing and the walk terminates. NodePost is
additionally allOf [Node, {node_key}], so it needs the existing allOf handling to run
after the items hop — the two compose.

Why it is easy to miss

"bodyProperties": {} is indistinguishable from "this endpoint legitimately has no body
fields."
Nothing warns, nothing logs, and no test fails. There is no error state to
notice — only an absence that reads as a valid answer.

Runtime impact

Private/Assert-PfbApiCapability.ps1 reads bodyProperties to version-gate body fields.
For these four endpoints the map is empty, so no field-level gate can fire — a caller
supplying a field that requires a newer REST version gets an opaque wire error instead of
the local, actionable message every other endpoint produces.

This is why PR #81, which taught Assert-PfbApiCapability to accept array bodies at all,
deliberately guards the field loop on -is [System.Collections.IDictionary] rather than
validating the union of element field names: against an empty map such a check could never
fire, and a gate that cannot fire is worse than none, because it looks like coverage.
Endpoint minVersion and query-parameter gating do run for these endpoints today; only
field-level gating is missing.

Reporting impact

The drift report's addable-body-parameter axis reads bodyProperties, so these four
endpoints contribute zero addable fields and cannot appear as gaps.

That lands directly on #44 (batch-operation cmdlets for nodes, resource-accesses,
workload-tags and fleet-members) — the drift report cannot inform the design of precisely
the cmdlets #44 proposes. Set-PfbWorkloadTag already ships with a free-form
[hashtable[]]$Tags, and nothing in the tooling can tell a user that namespace,
copyable or resource are the valid keys.

readOnlyBodyProperties is null for all four. NodePost carries obviously read-only
members (id, status, capacity, raw_capacity), so that field should populate too
once the walk descends — worth asserting explicitly rather than assuming.

Suggested fix

Add an items branch to Add-PfbSchemaPropertyNodes that resolves the element schema and
recurses into it with the existing accumulator, so allOf composition inside the element
continues to work. Then rebuild Data/PfbCapabilityMap.json and diff.

Expected diff: four endpoints gain populated bodyProperties (23 fields total, each with
an introducedVersion derived per the normal first-sight rule), plus whatever
readOnlyBodyProperties resolves to. Nothing else should move.

Two constraints on the fix:

  1. Depth budget. Descending through items consumes a level, and Capability map records 5 body fields with introducedVersion one release too late (MaxDepth=8 truncation in fb2.12-2.16) #71 is already about
    the walk running out of depth on the fb2.12–2.16 allOf chains. Capability map records 5 body fields with introducedVersion one release too late (MaxDepth=8 truncation in fb2.12-2.16) #71's suggested fix
    (an explicit -MaxDepth 32 at the Get-PfbSpecCapabilities call sites) supplies the
    headroom this needs. See the sequencing note in the tracking issue.

  2. The "nothing vanishes" invariant. Tests/Build-PfbApiDriftReport.Tests.ps1 asserts
    that every field the map lists for an endpoint an existing cmdlet calls lands in
    exactly one of the report's addable / read-only buckets or the phantom-exclusion set.
    Newly-populated fields must be accounted for there in the same change, or that test
    goes red. No child issue on the map carries this requirement, so it is easy to trip.

A regression test wants a synthetic fixture whose request body is an array of an
allOf-composed element schema, asserting the element's fields are found — a fixture that
fails today.

Relationship to #71

Same function, different keyword, and neither fix subsumes the other:

#71 this
Keyword allOf — handled, walk stops early items — no branch exists
Cause MaxDepth defaults to 8 missing code
Symptom introducedVersion one release late on 5 fields fields absent entirely, 4 endpoints
Visible on current spec? No — self-heals from 2.17 Yes

Raising MaxDepth does nothing for this; adding items does nothing for #71. They share a
call site, a data file and a verification burden, so they are worth landing together — see
the tracking issue.

Out of scope

The array-level constraints on the same schemas (minItems, maxItems, uniqueItems)
are a separate gap requiring a new map field rather than a walker fix. Filed separately.

Activity

  1. juemerson-at-purestorage commented on Aug 2, 2026

    @juemerson-at-purestorage
    CollaboratorAuthor

    Tracked in #84, sequenced as PR 1 together with #71 (which supplies the depth headroom this fix needs). The array-cardinality half is #83.

  2. juemerson-at-purestorage commented on Aug 5, 2026

    @juemerson-at-purestorage
    CollaboratorAuthor

    Fixed at the map-and-reporting scope in #97, reaching main via #98 (which carries the
    closing keyword — #97 targeted an integration branch, and closing keywords only fire on merges
    into the default branch).

    Recovered, exactly as this issue predicted: 23 body properties — POST /nodes/batch +12,
    POST /resource-accesses/batch +2, PUT /workloads/tags/batch +5,
    POST /fleets/members/batch +4. Additionally readOnlyBodyProperties on POST /nodes/batch
    resolves from empty to 8 fields (capacity, chassis_serial_number, data_addresses,
    details, id, raw_capacity, status, unique).

    One correction to this issue's framing

    The four endpoints did not record "bodyProperties": {}. Each recorded exactly one entry
    keyed by the empty string
    — the nameless artifact of a walk that reached the array node and
    could not descend it. Harmless to the diagnosis, but worth correcting because an empty map and
    a map with a blank-named key fail differently, and anyone grepping for {} would have found
    nothing.

    Why the fix is not in the shared walker

    Worth recording, because the obvious fix is wrong. Teaching Add-PfbSchemaPropertyNodes to
    descend items would have silently corrupted Data/PfbResponseShapeMap.json: that walker is
    shared with Get-PfbSpecResponseShapes, whose entire contract is that an envelope's properties
    and its items[] element's properties stay two deliberately-separate levels. Collapsing them
    would have made the response-shape map's cross-version removal detection compare incomparable
    sets, with nothing to warn.

    So the items hop happens at the Get-PfbSpecCapabilities call site instead, matching the
    precedent Get-PfbSpecResponseShapes already sets. The guard that proves no leak:
    Data/PfbResponseShapeMap.json is byte-identical throughout — 423DE668…FEF6 before, after,
    and after the integration merge.

    This also needed #71's MaxDepth fix to work at all: descending through items consumes a
    depth level, so at the old default of 8 the recovered fields would have been truncated again.
    Neither fix does anything alone, which is why they landed together.

    The runtime half is #95, and that split is deliberate

    This issue's stated scope included the runtime capability gate, and that half is not fixed
    here.
    Assert-PfbApiCapability's body-field loop is guarded on
    -is [System.Collections.IDictionary], and an array body arrives as [hashtable[]], so the
    loop is skipped before bodyProperties is ever consulted. The 23 fields are present in the map
    and inert at runtime today.

    Confirmed live rather than reasoned about — Set-PfbWorkloadTag against FB-A (REST 2.26)
    returned:

    FlashBlade API error (HTTP 400): Workload does not exist.
    

    A resource rejection, not a body-field one. The request reached the wire and was never gated
    client-side, which is direct evidence the type guard short-circuits ahead of bodyProperties.

    Closing this at the map/reporting scope and letting #95 own the runtime half: recovering
    the fields is a generator fix, whereas making the gate act on them means changing a type guard
    in a live path that every write cmdlet traverses — different change, different risk, different
    review. #95 now has real data to test against, which it did not before.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions