Skip to content

Design spec rev 3: cardinality is not the HTTP verb (stacked on #22) - #72

Closed
juemerson-at-purestorage wants to merge 7 commits into
dmann000:design/fusion-contextfrom
juemerson-at-purestorage:design/fusion-context-rev3
Closed

Design spec rev 3: cardinality is not the HTTP verb (stacked on #22)#72
juemerson-at-purestorage wants to merge 7 commits into
dmann000:design/fusion-contextfrom
juemerson-at-purestorage:design/fusion-context-rev3

Conversation

@juemerson-at-purestorage

@juemerson-at-purestorage juemerson-at-purestorage commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #22 — base is design/fusion-context, not main, so the diff here is exactly rev 2 → rev 3. Merging this updates #22; #22 still lands on main on your call.

Why

Rev 2's cardinality rule — every GET is multi-context-capable, mutations are size-1 — is false. Live testing against a Fusion fleet found four fleet-scoped GETs that reject a multi-value context outright.

Replacement rule, now data-driven from the capability map:

An endpoint is multi-context-capable iff its context_names parameter resolves to component Context_names_get and the endpoint also declares allow_errors.

Both facts are already recorded per endpoint, so this is a map lookup — no new derivation, no new tooling.

Evidence

Against FB-A (Purity//FB 4.8.2, REST 2.26), fleet coordinator of cc-test-fleet:

GET /presets/workload?context_names=FB-A,FB-C        -> 400 code 15 "Multiple location contexts are not allowed."
GET /topology-groups        (same query)             -> 400 code 15
GET /topology-groups/arrays (same query)             -> 400 code 15
GET /topology-groups/members(same query)             -> 400 code 15
GET /file-systems, /admins, /arrays  (same query)    -> 400 code 20 "Operation not permitted."   (controls)

code 15 fires before the cross-array authorization gate that produces code 20, so it is structural to the endpoint, not a permission artifact — and it is independent of name validity. These four are fleet-scoped: the only meaningful context is the fleet itself, so multi-value there is not merely restricted, it is meaningless. Their spec entries reference Context_names_get in error, which is confirmed upstream as incomplete remote-execution support (IR-268610, in review, fixes in flight against both the spec and the server) and still present at 2.28.

Re-verified independently against a remote fleet member — 9 of 9 endpoints match the rule. Probing the local array proves nothing, because the middleware short-circuits a local context before validating scope. The figures were also reproduced from the specs by a second agent implementing the predicate.

Other gaps rev 2 left open

  • allow_errors is specified. Rev 2 did not mention it, though it is one of the two parameters this feature exists to inject.
  • context_names is a family of context kinds — array, fleet, topology group, and fan-out via the undocumented .arrays suffix — not an array name. Rev 2 treated it throughout as "a specific fleet member."
  • HTTP 207 / the errors response array are specified as a response-layer requirement. Invoke-PfbApiRequest has no branch for errors today.
  • A cross-array authorization precondition (authorization_model: static vs dynamic) is specified.
  • Capability-map staleness is decided — permissive fallback, settled by a connect-time warning — with the dissenting view recorded rather than dropped.
  • Phasing is corrected. Rev 2 put Invoke-PfbInContext in Phase 2 while relying on it in Phase 1 as the documented escape hatch from the Phase 1 hard throw.

Worth your attention specifically

  • Signal counts are version-stamped throughout. They move between 2.27 and 2.28 (the 207-unknown set is 11 then 4), and single-version figures read as stable when they are not.
  • Implementation prerequisite 3 is new and load-bearing. Component resolution lives in tools/, which is not shipped with the module, so wiring the cardinality check before moving it into Private/ would silently force a second copy of the rule.
  • Open question 6 is a decision I made and would rather you confirmed — the no-signal fallback fails open (an unmapped GET is treated as capable). Rationale: a wrong "capable" yields a readable wire-400, a wrong "size-1" locally blocks a call that would have worked. It has one live instance, GET /snmp-managers/test.
  • Appendix B tracks each spec defect so its workaround can be retired with evidence. GET /audits is already fixed at 2.28.

Docs only — no code, no version bump, no CHANGELOG.

🤖 Generated with Claude Code

Rev 2's central claim -- that cardinality follows the HTTP verb, every GET
being multi-context-capable -- is false. Live testing against a Fusion fleet
disproved it for four fleet-scoped endpoints, and the replacement rule is
data-driven from the capability map:

  An endpoint is multi-context-capable if and only if its context_names
  parameter resolves to component Context_names_get AND the endpoint also
  declares allow_errors.

Verified 9 of 9 against a remote fleet member, and reproduced independently
from the specs by a second agent implementing the predicate.

Also closes gaps rev 2 left open:

- allow_errors is specified. Rev 2 did not mention it, though it is one of
  the two parameters this feature exists to inject.
- context_names is a family of context kinds -- array, fleet, topology group,
  and fan-out via the undocumented .arrays suffix -- not an array name.
- HTTP 207 and the errors response array are specified as a response-layer
  requirement.
- A cross-array authorization precondition (authorization_model) is specified.
- The capability-map staleness question is decided, with the dissent recorded.
- Phasing is corrected: rev 2 put Invoke-PfbInContext in Phase 2 while relying
  on it in Phase 1 as the escape hatch from the Phase 1 hard throw.

Signal counts are version-stamped throughout. They move between 2.27 and 2.28
and single-version figures read as stable when they are not.

Appendix A records the empirical basis, including that GET /topology-groups/arrays
cannot be probed bare -- its own parameter validation returns code 24 before the
context check is reached. Appendix B tracks the spec defects so each workaround
can be retired with evidence; GET /audits is already fixed at 2.28.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The predicate shipped in dmann000#73 without its input producer, so the prerequisite
is now concrete rather than anticipated. Records that the refactor belongs in
the implementation PR's first commit rather than as separate work, since only
the resolution step moves and the surrounding function stays in tools/.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@juemerson-at-purestorage

Copy link
Copy Markdown
Collaborator Author

Pushed one more commit: prerequisite 3 now points at #74.

Context — #73 merged after this PR was opened, and it shipped Private/Test-PfbContextMultiValueCapable.ps1 without the code that produces its -ContextComponent input. So the prerequisite this doc describes is no longer anticipated, it's live on main: the predicate is loaded into the module and nothing inside the module can feed it.

Test-PfbContextMultiValueCapable   exported: False   internal: True
Get-PfbContextParameterFact        internal: False

Nothing is broken — the predicate is inert, with no runtime callers — but it means the implementation PR has to do the tools/libPrivate/ move as its first commit rather than discovering it partway through, which is what the new note says.

This PR is not blocked by #74. It's docs-only, and it's what identifies the prerequisite in the first place. Reviewable as-is.

…ive testing

Rev 3 characterized fleet-scoped endpoints from GET evidence alone. Live testing
of POST/PUT/DELETE /presets/workload on FB-A (2026-08-02, dynamic-model admin,
full create -> PUT -> delete round trip) shows mutations differ in the row that
matters most.

Omitting context_names on a fleet-scoped endpoint does not resolve to a local
view -- it fails (code 13 on POST, code 6 on PUT/DELETE). The local-context
short-circuit is read-only. A member array name is rejected on every verb, so
only the bare fleet name is ever legal, confirming for writes what rev 3 had
established for reads. And a no-context GET is list-only: unfiltered returns the
object, but ?names= returns code 6, so name resolution is in the mutation case
regardless of verb.

This is not prospective. No cmdlet in Public/Presets/ has ever sent
context_names, so five of six preset operations are non-functional today.
Context injection is what makes those cmdlets work at all rather than an
enhancement to them.

Also corrects section 1. "Every request funnels through Invoke-PfbApiRequest"
was stated as settled in rev 2 and has five counterexamples: three connection
lifecycle cmdlets that are legitimately outside the choke point, plus two write
cmdlets that merely bypassed it. dmann000#76 closed one; dmann000#77 is the last, which makes it
a prerequisite for this design rather than a parallel cleanup.

Adds:
  - "Fleet-scoped mutations have no usable default" with the measured verb matrix
  - a read-only caveat on the local-context short-circuit
  - section 11: code 20 masks every other finding as a static-model admin, which
    already produced one confident wrong conclusion
  - Open Question 7: send-and-fail vs client-side throw vs default-to-fleet
  - Appendix A: the 2026-08-02 evidence block
  - Appendix B: the "array in the fleet or the fleet itself" description defect,
    and the absence of any fleet-scoped/array-scoped marker in the spec
  - two testing-plan cases, and a dynamic-admin precondition on live verification

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two sections already promised client-side kind-vs-scope validation without
saying where scope comes from, and the new fleet-scoped-mutation guidance needs
the same input. It is in no spec field, so the module has to carry it.

Checked at fb2.28: operation tags are resource groupings and do not separate
fleet-scoped from array-scoped endpoints. Cardinality is not a proxy either --
fleet scope implies size-1, but every array-scoped mutation is size-1 too, so
the implication does not run backwards.

Curation is affordable. Context-capable endpoints under fleet-level resource
families: Presets 5, Topology Groups 8, both live-confirmed; Realms 3, untested
and marked unknown; Fleets and Realm Connections take no context at all. Thirteen
confirmed entries across two families out of 376 endpoints recording
context_names -- fleet-scoped endpoints are not scattered, they are exactly the
resource types living in the fleet database.

Proposes a contextScope field per endpoint in the capability map, defaulting to
array, which is the fail-safe direction: mis-marking a fleet-scoped endpoint
costs guidance and leaves current behavior, while the reverse throws on a call
that would have worked.

Three consumers: data-driven kind-vs-scope validation, error and pre-call
messages that name the requirement and the cmdlet that satisfies it, and
generated .NOTES help so the requirement is discoverable before the failure. The
failures this replaces are code 13, code 42, and a code 6 that actively misleads
by reporting a preset that does exist as missing.

This subsumes open question 3, whose annotation half is the same mechanism, and
unblocks open question 7: throw-client-side is only attractive if the throw can
be specific, and scope metadata is what makes it specific. Default-to-fleet stays
open, blocked on discovering the fleet name without an extra call.

Lands in Phase 1, since kind-vs-scope validation cannot be implemented without it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Section 12 as committed in 337dcb3 claimed there is no scope signal in the spec
and proposed a fully hand-curated table. That claim was wrong. It was based on
checking tags and cardinality and stopping there, without looking at the vendor
extensions.

fb2.28 carries three x-pure-* extensions, all new or newly expanded at that
version:

  x-pure-remote-execution-context-domains-override   5 endpoints
  x-pure-block-remote-execution                    266 endpoints (2 at 2.24)
  x-pure-incomplete-gre                             28 endpoints

The override declares scope directly, and agrees with the 2026-08-02 live
testing on every one of the five preset operations including the read/write
asymmetry: GET is ARRAY|FLEET, the four mutations are FLEET. Two independent
derivations of the same fact.

It covers only those five, so curation does not disappear -- but
x-pure-incomplete-gre explains the gap and shrinks it. That flag marks the 28
operations whose remote-execution annotation upstream considers unfinished, and
it contains every endpoint this design has had to establish by live testing: all
four fleet-scoped GETs from section 8, all five preset operations, GET /realms,
and GET /workloads/tags. It is upstream's machine-readable version of the defect
Appendix B tracks as open. Curation drops from thirteen hand-maintained entries
to eight, each justified against a declared flag, with an exit condition as the
override is filled in.

Adds an encoding subsection: Build-PfbCapabilityMap.ps1 reads none of these
today and the map is schemaVersion 1, so this needs the generator to read the
extensions, an additive contextScope field, a schema bump to 2, the curated
table kept at build time rather than in Private/, and a drift test that retires
curated entries superseded by an override. Rides the existing capability-map
workflow. Ordering consequence: the generator work moves to the front of Phase 1.

Also records two further spec defects: block-remote-execution contradicts
context_names on 11 endpoints (all inside the flagged 28), and
info.x-pure-description-ref points at an introduction markdown that ships in
neither the JSON nor the public Redoc rendering.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The document had accumulated several rounds of live-testing findings patched
in inline. Design sections carried the archaeology of how each rule was
established -- dates, who found what and in what order, raw per-version
counts, and "what changed since rev N" corrections -- so a reader wanting the
design had to wade through the evidence trail to reach it.

Reorganisation only. No design decision, recommendation, or open question
changes substance, and no section numbering moves: sections 1-12, Open
Questions 1-7, and Appendices A and B keep their identities, so every existing
cross-reference still resolves.

The body now states each rule, its rationale, and its consequences in the
present tense, with a compact pointer to the evidence rather than the
discovery story. Three appendices absorb what moved out:

- Appendix C (new) -- spec and capability-map figures. The per-version signal
  populations pulled out of section 8's reliability table, the unknown-207
  endpoint lists, the four component-flip endpoints, the vendor-extension
  occurrence counts from section 12, and the spec-analysis and capability-map
  paragraphs relocated from Appendix A. Appendix A is now purely live-array
  measurement; Appendix C is purely static counts.
- Appendix D (new) -- live-testing preconditions. The self-context trap, the
  code-20 masking anecdote, the /topology-groups/arrays code-24 probing
  caveat, and the hand-assembled-request artifact behind ordering
  requirement 1.
- Appendix E (new) -- revision history. The former "What changed since rev 2"
  section from the top of the document, plus the rev-correction notes that
  were sitting inside sections 1, 7, Ergonomics, Phasing and Alternatives, and
  a table of the dated decisions and consults.

Nothing is dropped: every error code, endpoint, figure, caveat and open
question is still present. Verified by diffing the sets of error codes,
endpoint paths, numeric figures, vendor extensions, cmdlet names, issue
numbers and dates between the two revisions -- all empty.

Two consolidations. The GET /audits 2.28 fix was stated three times; the
full statement now lives only in the Appendix B row it belongs to. The
113-endpoint silent-accept audit figures were stated in both section 4 and
Appendix A; section 4 now states the behavior and points at Appendix A for
the proportion.

Two small editorial fixes: section 5's heading said "two non-obvious
requirements" over a list of three, and section 12 opened by asserting scope
"cannot come from the API" immediately before the subsection showing that the
2.28 spec declares it -- residue of the rev 3 correction. Both corrected.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rev 3 as first drafted modelled the .arrays suffix as a FanOut boolean on
each context entry, and offered Get-PfbTopologyGroupMember | Set-PfbContext
as reaching "the same outcome as <group>.arrays". Both are wrong.

The field becomes an enum-valued Form (Object | AllArrays). A boolean can
express exactly one alternative to the default, and the suffix vocabulary is
already known to be open -- an all-sub-groups equivalent and realm-as-context
would each need their own flag, and two booleans on one entry can encode a
meaningless both-set state. Retrofitting the enum later is a breaking change
to a published parameter, since -AllArrays and -Form cannot coexist cleanly.

"Fan-out" is retired as a name for .arrays and reserved for a possible future
client-side serial loop over N arrays, which differs in request count, failure
semantics and mutability. The switch is -AllArrays. A terminology note in the
Background separates the three senses; the non-goal already used the reserved
one.

The member-pipeline idiom is withdrawn as the whole-group form. A topology
group's members may themselves be groups, so the pipeline yields group names
where array names are required; .arrays is transitive over the sub-tree and is
re-resolved server-side per request rather than snapshotting membership. The
cmdlet stays correct for scoping to specific members, which is a different
request.

-AllArrays resolves membership before storing the context, via
GET /topology-groups/arrays (2.26) or GET /fleets (2.17) -- an asymmetry forced
by there being no /fleets/arrays. This is a bounded exception to the
no-hidden-network-calls preference and settles Open Question 2: the rejected
alternative was inferring Kind from topology on every context set, unbidden;
this confirms a name the caller explicitly supplied, once. Without it a
mistyped group name is stored and then silently misdirects every later call.

Adds an ownership boundary under Phasing. Topology-group object management
stays with issue dmann000#38; membership resolution is this design's; and dmann000#38 carries
a stated contract that Get-PfbTopologyGroup emit a top-level Name that binds
by property name -- without it the documented pipeline silently no-ops in
exactly the way Ergonomics describes for Get-PfbFleetMember. All seven
topology-group endpoints declare context_names and none declares allow_errors,
so they are uniformly single-context under section 8 and need nothing further.

Section 1 records Set-PfbWorkloadTag as closed pending merge (dmann000#77 / PR dmann000#81),
and corrects its open question: /workloads/tags/batch is array-scoped, the
exact inverse of /presets/workload, so it does not repeat the fleet-scoped
failure -- but it declares context_names at 2.23 and still needs injection.

Appendix A gains the CLI-versus-REST parity table for .arrays across seven
cases, Appendix B's .arrays row records that the CLI man page documents the
form the OpenAPI spec omits, and Appendix E carries the revision record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@juemerson-at-purestorage

Copy link
Copy Markdown
Collaborator Author

Closing this as superseded rather than merging it.

The work it was written for has shipped: Phase 0 in #98 and Phase 1 in #109, both on main. The specs that actually landed are docs/design/fusion-context-phase-0-spec.md and docs/design/fusion-context-phase-1-spec.md — implementation-scoped documents written against what was built, so this branch's docs/design/fusion-context-injection.md would now describe the design as it stood before that.

Rev 3's substantive conclusion was not lost. The component-based cardinality rule replacing the "every GET is multi-context" rule was implemented and merged in #73, and Phase 1's gates enforce it. The one later correction worth recording here: the rule is keyed on the value count, not on the parameter's form, so a single-element multi-value context behaves as size-1 — which is why -AllArrays -AllowErrors can never produce a partial result.

A rev 4 exists locally that carries the allow_errors design, the phasing, and the empirical appendices. Where that should live is a separate question from this PR, so it is not blocking here.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant