Skip to content

Fusion context_names Phase 1: session-scoped context and client-side validation (#25) - #109

Merged
juemerson-at-purestorage merged 53 commits into
dmann000:mainfrom
juemerson-at-purestorage:feat/fusion-context-phase-1
Aug 13, 2026
Merged

Fusion context_names Phase 1: session-scoped context and client-side validation (#25)#109
juemerson-at-purestorage merged 53 commits into
dmann000:mainfrom
juemerson-at-purestorage:feat/fusion-context-phase-1

Conversation

@juemerson-at-purestorage

Copy link
Copy Markdown
Collaborator

Phase 1 of Fusion context_names support (#25). Adds session-scoped context so a single
connection can address other arrays in a fleet, with client-side validation that fails fast
instead of relaying the server's assorted rejection codes.

Phase 0 (#98) is the prerequisite and is already on main. This branch is rebased onto it.

What this adds

Three new cmdlets:

Cmdlet Purpose
Set-PfbContext Set the session-default context on a connection
Clear-PfbContext Remove it
Invoke-PfbInContext Run a scriptblock under a temporary context, restoring the previous one afterwards

Connect-PfbArray also gains -Context, -Kind and -AllArrays, so a connection can be
established with a context already in place.

Context is session state, not a per-cmdlet parameter. No existing cmdlet gains a
-ContextNames parameter. Resolution happens once, at the single choke point in
Invoke-PfbApiRequest, which is what keeps 500+ cmdlets from each needing to know about
contexts.

Design points worth a reviewer's attention

One resolution point, one predicate. Resolve-PfbRequestContext is the only place a
context is resolved, with a documented precedence order. The $null (unset) vs @()
(explicit "run this one call locally") distinction is a tri-state that is pinned at every
layer that consumes it, not just at the resolver — the two states behave differently on
purpose and must not be unified.

Injection is ordered before the version gate. context_names is added to the query
params before Assert-PfbApiCapability runs, so the version check sees the parameter it is
meant to be checking. A test drives this through the real injection path and asserts on what
the mocked capability gate actually received — asserting on the request URI cannot detect the
bug, because an empty injected value is discarded downstream by two separate code paths.

Four client-side gates, all wired at the choke point: capability (does this endpoint
declare context_names at this REST version), cardinality (single vs multi-value),
kind-vs-scope (is a fleet name valid here, or an array name), and required-context (some
fleet-scoped operations cannot be performed without one). Each has a call-site wiring test
that goes red if the call is deleted — this branch previously shipped a gate that was wired
but could not fire, twice, so those tests are deliberate.

Generated help cannot drift from enforcement. tools/Update-PfbContextHelp.ps1 renders
each affected cmdlet's context .NOTES block from the same contextScope field the runtime
gates read, and refuses to be quietly incomplete: an endpoint with no cmdlet, or a scope value
with no render arm, is reported rather than skipped. A test asserts full accounting over every
non-default-scope endpoint.

Scope decisions

contextScope's array default is deliberate and is not a guess. provenance: default
means the upstream spec carries no domain override and the operation is not flagged
x-pure-incomplete-gre — upstream's own marker for where its remote-execution annotation is
unfinished. A fleet-scoped operation with a complete annotation would carry an override.
Endpoints upstream flags as incomplete are recorded unknown, which suppresses the
kind-vs-scope check rather than throwing. The design doc's justification for this default was
corrected during review: mis-marking is not harmless (the gate throws, so both directions of
error can block a call); the default rests instead on coverage — nearly every endpoint accepts
an array context, so an array default leaves a working form available even where it is
mistaken.

Preset operations are fleet-scoped by architecture, not by configuration: a preset is a
fleet-database template, so a non-fleet array can neither create nor hold one. This is
documented in the published FlashBlade CLI Reference; the OpenAPI spec does not carry the rule.
An unfiltered Get-PfbPresetWorkload still works with no context, returning the locally
replicated list, and the generated help says so.

Topology-group cmdlets remain out of scope (#38). This branch owes #38 exactly one thing:
the contract that Get-PfbTopologyGroup emit a top-level Name for pipeline binding.

#25 stays open. Phases 2 and 3 remain.

Report artifacts

Regenerated after the rebase, with the full chain in order — capability map, field-cmdlet map,
response-shape map, drift report. Data/PfbCapabilityMap.json came back byte-identical to the
committed copy, which confirms the one-line generator change does only what it claims.
Data/PfbResponseShapeMap.json is unchanged.

Two deltas that will look odd without explanation:

  • PfbFieldCmdletMapping.md: "typed but unresolved wire name" rises 40 → 51. The 11
    additions are the context parameters on Connect-PfbArray, Invoke-PfbInContext and
    Set-PfbContext. These configure session state rather than a request body, so having no
    wire field is the correct classification, not a coverage gap.
  • PfbApiDriftReport.json lists Resolve-PfbAdminLocality under GET /admins. It does
    call that endpoint, to read is_local. Note it lives in Private/ and is not exported, so
    the drift scanner is counting a private helper as a cmdlet — arguably a generator issue
    worth its own ticket rather than anything this branch should change. Update-PfbArray's
    recorded paramBlockLine also moves, because the generated help block sits above the param
    block.

Testing

Both PowerShell editions, per-Describe, via the repo's scoped runner. Unit coverage is
mocked; the wire behaviour behind the scope and cardinality rules was measured against a live
FlashBlade during development, and those measurements are what the capability map's
live-tested provenance records.

No version bump and no CHANGELOG.md change, per project convention — those are the
maintainer's call.

Follow-ups, not included here

juemerson-at-purestorage and others added 30 commits August 12, 2026 17:30
The user-facing context surface, kept whole: the PfbContext object,
connection context state, central injection in Invoke-PfbApiRequest, the
three client-side gates (capability / cardinality / kind-vs-scope),
Set-PfbContext, Clear-PfbContext, Invoke-PfbInContext, and -AllArrays.

Depends on Phase 0 -- both the component resolver (dmann000#74) and the
contextScope map field are load-bearing. Rebase onto main after Phase 0
merges rather than stacking.

Six of the design doc's seven open questions are resolved here; OQ7
ships the client-side throw and stays open pending confirmation that
one-fleet-per-array is guaranteed rather than a current limitation.

Records that the design doc's line citations for Invoke-PfbApiRequest
are stale -- the Assert-PfbApiCapability call is at :46, not :41 -- since
injecting after it silently disables the version gate this design leans
on.

Refs dmann000#25

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The x-pure-remote-execution-context-domains-override declares ARRAY|FLEET,
but the ARRAY half is satisfied only by the universal local-context
short-circuit; measured from a remote member, a remote array name and
<fleet>.arrays both return code 13 and only the bare fleet name is accepted.
Recording 'array' made the Phase 1 kind-vs-scope gate throw on the only
working context and permit one that silently targets the local replica.

The operation also carries x-pure-block-remote-execution AND
x-pure-incomplete-gre alongside the override -- block=true is itself
falsified by a fleet context returning 200, and incomplete-gre is upstream's
own statement that this annotation is unfinished.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ules

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
$script:PfbContextKinds and $script:PfbContextForms were never read: the live
vocabulary is the ValidateSet literals on the Kind/Form parameters. Editing the
constants changed no behavior, making them a fake source of truth.

ValidateSet cannot take a variable, so the literals must stay duplicated. A new
meta-test discovers every Kind/Form ValidateSet by parsing the AST of all .ps1
under Private/ and Public/, classifies each set by its own contents ('Fleet' =>
Kind, 'AllArrays' => Form), and asserts each category agrees. It also asserts at
least one site of each was found, so a no-match scan cannot pass silently. New
sites added by later tasks are covered with no edit to the test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Both cmdlets return a NEW connection object rather than mutating the
caller's, and make no network call. Set-PfbContext accumulates piped
names across process{} and emits exactly one connection in end{}, so
N piped fleet members yield one union-scoped connection instead of N
copies that each lose the others' entries. A missing -Context raises
an explicit throw rather than using [Parameter(Mandatory)], which
would prompt and hang under -NonInteractive.

Also, from review:

- Fix the Kind/Form ValidateSet drift meta-test's discriminator. It
  classified each discovered site by its CONTENTS ('Fleet' => Kind,
  'AllArrays' => Form), which silently left a site that DROPPED a
  token unclassified and therefore never compared -- missing exactly
  the drift the test exists to catch. It now classifies by the name
  of the parameter the attribute decorates, read from the AST. The
  scan stays fully dynamic, still asserts it located at least one
  site of each kind, and still compares Kind only against Kind.

- Add [ValidateNotNull()] to Connect-PfbArray's -Context. $null
  previously flowed into ConvertTo-PfbContextEntryList -Name, which
  is [Parameter(Mandatory)] -- the interactive-prompt/hang risk. Not
  added to Set-PfbContext's -Context: that is the ValueFromPipeline
  slot, where $null must keep falling through to the friendly throw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review fixes for Set-PfbContext/Clear-PfbContext.

Important 1 -- the switch->Form mapping
`if ($AllArrays) { 'AllArrays' } else { 'Object' }` existed in both
Connect-PfbArray and Set-PfbContext with no test over either copy.
Extracted to a private Resolve-PfbContextForm beside the other context
primitives and called from both, so adding a third Form value cannot
update one copy and silently leave the other emitting 'Object'. No
ValidateSet on it -- it takes a switch, so it adds no site to the
vocabulary drift scan. One test per branch. Verified both call sites
actually route through it by mutating the helper to return 'Object'
unconditionally: the -AllArrays tests of BOTH cmdlets went red.

Important 2 -- a comment stating a falsified claim
Connect-PfbArray's -Context comment claimed $null into a mandatory
parameter "prompts and hangs under -NonInteractive". Measurement
disproved that: PowerShell prompts only when the argument is ABSENT;
an explicitly-supplied $null binds and is then rejected. Rewritten to
the true rationale -- the guard is about error ATTRIBUTION, moving the
failure to the binder naming -Context instead of a downstream -Name
the caller never typed. The ValidateNotNull-vs-NotNullOrEmpty sentence
is kept. Same correction applied to the matching test comment.

Important 3 -- the tri-state assertion
Clear-PfbContext's test used Should -BeNullOrEmpty, which passes for
$null, for @(), and for a context object with empty Entries -- so a
change emitting @() would not red, even though @() has a different
documented meaning at the Invoke-PfbInContext layer. Now asserts
$null -eq directly, matching the idiom in the Connect-PfbArray tests.

Also:
- Comment on Set-PfbContext's `if ($Context)` recording that the
  truthiness test is deliberate: $null -ne $Context would admit ''
  and mint an entry with an empty name. Behaviour unchanged.
- Cache-repointing test for Clear-PfbContext, closing the asymmetry
  with Set-PfbContext's equivalent.
- Test that Connect-PfbArray -Context @() yields a NON-null
  DefaultContext with zero entries -- the headline tri-state at the
  connect layer, and the regression guard proving the new
  ValidateNotNull still admits @().

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Also restores module connection-cache state after every mocked-connect test in
Tests/Connect-PfbArray.Context.Tests.ps1, and adds a non-null guard so the
unset-DefaultContext test cannot pass vacuously.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…p dead cast

Connect-PfbArray mutates $script:PfbArrays in place, so capturing the reference
and reassigning the same object left the 'fb.test' key leaking to later test
files. Capture a shallow copy instead, and add a final ordered test asserting
the key is absent so a silently-failing restore fails loudly.

Also drop the [object[]] cast on New-PfbContext -Entries and its misleading
comment: the @() wrapper around ConvertTo-PfbContextEntryList is the only thing
required for the -Context @() escape hatch. Realign the -Array guard brace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y distinction

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Add two mutation-killing tests for Resolve-PfbRequestContext: a falsy-but-present
ContextOverride that a truthiness check would skip, and an explicitly empty
QueryParams context_names that requires the @() wrapper. Comment-only clarifications
in the implementation; no behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…sion gate

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The URI cannot observe an injected context_names = '' -- both downstream
sinks discard empty-string values -- so assert on what Assert-PfbApiCapability
actually receives. Also corrects Get-PfbEndpointKey's doc comment: a drifted
key silently disables the version gate, it does not throw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…xt_names

Fix round 1 on Task 9.

Important 1: Resolve-PfbParameterComponent returns the map's DEFAULT component for
entries that do not declare context_names, so the cardinality rule read $false for
them and the gate advised narrowing a context the endpoint cannot take at all --
firing precisely where Assert-PfbContextCapability deliberately abstains (array
beyond the map's scanned range). Extract Test-PfbEndpointDeclaresContextNames as the
one home for that question and have both gates call it. This also makes the
cardinality gate order-independent.

Important 2: pin both gate call sites in Invoke-PfbApiRequest, and their relative
order, which were completely unpinned -- deleting either call left every test green.

Minors: recurse the single-home invariant sweep into Private/ subdirectories, add
-Because explaining a 0-count refactor, and correct the if (-not $entry) comment
which named an ordering dependency that measurement shows does not exist.
…rtition

The GET partition's -Because claimed it detects a deleted scope early-return
independently of the verb branch. Measured false: the allowlist added in fix
round 2 returns before the throw for every unknown-scope GET, so the non-GET
partition is what kills that mutation. The assertion is still needed -- it keeps
the GET probe from vacuously passing on a $null key -- but its stated reason was
wrong, the same defect class as the map-pin comment corrected in fix round 1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the connect capture

Fix round 1 for task 11.

Critical: Resolve-PfbAuthorizationModel read .items off Invoke-PfbApiRequest's
return value, which already unwraps the envelope and hands back an object[] of
admin objects. The resolver therefore returned $null on every real array and
the gate was permanently inert in production behind a green suite. Read the
admin objects directly and match on name rather than taking row 0.

The test that should have caught it mocked Invoke-PfbApiRequest and fed it the
wire envelope -- a shape that function can never emit. Re-pinned at the
Invoke-RestMethod boundary so the real unwrap runs.

Also: pin the connect-time capture with a -Credential harness (deleting that
one line disabled the whole feature and reddened nothing); move the gate below
Assert-PfbApiCapability so a firmware blocker wins over an admin-model one,
per Task 10's measured ruling, with a five-element ordering test; add an
un-mocked request-path throw test; name the offending context values in the
message; build one context object in Set-PfbContext instead of a throwaway;
document the -ApiToken inertness and the 403 reconnect amplification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t is set

Maintainer ruling 2026-08-05: GET /admins is the only route to
authorization_model (POST /api/login returns just {"username":...}), so the
call is necessary -- but resolving it at every connect made sessions that never
touch Fusion pay for it.

Two sites only: Connect-PfbArray inside the -Context block, and Set-PfbContext's
end{}. Both are one-shot session setup, so no cache is needed and
AuthorizationModel stays two-state-plus-null. Invoke-PfbInContext is
deliberately excluded -- a per-call wrapper would mean one probe per loop
iteration.

Connect-PfbArray's cache assignment moved BELOW the context block so a rejected
context can no longer leave a connection the cmdlet never returned installed as
the default array. Set-PfbContext takes its copy before resolving, so the model
is written onto the copy and copy-on-write holds for it as it does for context.

Reworked FIX 2's capture test to pin the -Context path and added the test that
pins the point of the change: a bare connect issues no admin call at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…s broke

Fix round 3. Both Importants are the same mistake: round 2 added call sites
without re-deriving what the old single site relied on.

- Connect-PfbArray gated on $contextRequested alone, which is TRUE for
  -Context @() -- this codebase's explicit no-context state -- so a static
  admin's @() connect paid a probe and hard-threw with an empty value in the
  message. Now gates on both halves, mirroring Invoke-PfbApiRequest's
  $hasContext.

- Resolve-PfbAuthorizationModel relied on "DefaultContext is still $null when
  I run", true only of the connect site. Set-PfbContext's $copy inherits the
  existing context and GET /admins declares context_names, so the identity
  probe was routed to a remote array -- or the kind gate threw inside the
  resolver and silently downgraded a known 'dynamic' to $null. The probe now
  strips both context slots from a copy, inside the resolver, so the invariant
  holds for every call site present and future. Docstring rewritten: it still
  asserted the false precondition.

Also: Set-PfbContext's help said "No network call is made" and its guard test
passed only because the fixture had no Username; both corrected. Comment at
the connect block corrected -- the reconnect cache guards are endpoint-keyed,
so the reordering narrows the window rather than closing it. Rejected -Context
now releases the session it minted, without evicting a prior connection's
cache entry.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…obe copy is benign

Fix round 4, final for task 11.

- The logout added last round had no detector: the existing test's throw-message
  and cache assertions all hold with the entire try/catch deleted. Now asserted
  at the Invoke-RestMethod boundary, -Times 1 -Exactly, with a second assertion
  requiring TimeoutSec so the timeout is pinned too rather than merely added.
- The logout carries the connection timeout. An endpoint that accepts TCP and
  never answers would otherwise stall a cmdlet already on its way to failing.
- Corrected the reconnect-window comment: since the context strip those writes
  repoint the caches at the resolver's PROBE COPY, not $connection.
- Recorded at the strip site why the probe copy landing in the caches is
  benign -- the substitution only persists on a gate throw, a gate throw needs a
  static admin, and a static admin can never have had a context cached -- and
  stated the dependency: this stops being benign if a static admin is ever
  allowed to hold a context, or if either gate moves after its cache write.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
juemerson-at-purestorage and others added 20 commits August 12, 2026 17:30
…on model

A controlled experiment on FB-A (REST 2.26) falsified Task 11's premise:
flipping authorization_model on a remote admin changed nothing, while the
local pureuser was denied cross-array regardless. Remote => permitted,
local => cross-array denied. The shipped gate emitted false positives that
blocked working sessions.

Retarget the resolver at Admin.is_local (present from 2.17, the same floor
as context_names, so no version guard), rename
Resolve-PfbAuthorizationModel -> Resolve-PfbAdminLocality,
Assert-PfbContextAuthorizationModel -> Assert-PfbContextAdminLocality and
the connection property AuthorizationModel -> AdminLocality, and correct
the docstrings, throw message and error annotation that asserted the
falsified rule. Values stay tri-state strings 'local'/'remote'/$null.

Update-PfbAdmin is untouched: authorization_model is a real request-body
parameter there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…03 fixture, correct the falsified gate docstring

Task 12c review round 1. Three medium findings, all mechanical:

1. Tests/Invoke-PfbApiRequest.ContextInjection.Tests.ps1 asserted
   Should -Not -BeLike '*Reconnect as a dynamic-model*', which the rename
   made unmatchable and therefore unfalsifiable. It is the mirror guard for
   the "do NOT re-merge these two clauses" invariant. Retargeted at the
   current production wording and verified armed by temporarily merging the
   clauses: it reds, on both editions.

2. Tests/Connect-PfbArray.Context.Tests.ps1's 'leaves AdminLocality null
   when the admin lookup fails' fixture still lacked DefaultContext and
   ContextOverride, so the resolver's probe strip crashed into its own catch
   and returned $null before the mocked 403 was reached. Declared both
   properties and added Should -Invoke Invoke-PfbApiRequest -Times 2
   -Exactly, which reports "called 0 times" without them.

3. Assert-PfbContextAdminLocality's .DESCRIPTION claimed the gate "can never
   turn a would-be wrong-target success into a failure". Live testing
   falsified that: a local admin can successfully target its own array, so
   for a self-targeting context the gate does reject a call the server would
   have served. Stated honestly, with the ruling that makes it intended.

Also: three stale model/cross-array wordings reconciled with Set-PfbContext's
framing, AdminLocality hashtable alignment, and a pre-existing banned
Should -BeNullOrEmpty in Tests/Set-PfbContext.Tests.ps1 converted to the
tri-state idiom. The report's false claim that finding 2 was already fixed is
corrected in place, marked as a correction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… pins

Task 13 review Minor 1: the comment named the per-item-context test as the
guard for "added AS RECEIVED -- never project or rebuild", but that test only
pins that the per-item `context` field survives. A PSObject.Copy() or a
rebuild forwarding `context` would both pass it, so a maintainer could read
the comment as enforcing reference identity when nothing does.

Reference identity is not a spec requirement, so narrow the comment rather
than over-constrain the implementation with a new assertion.

Comment-only; no executable change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…auth path

POST /api/login returns the authenticated admin's name in its 200 body, and has
done in every REST version 2.0-2.28. Both login paths captured only the
x-auth-token header and discarded the body, so Username was whatever the caller
typed -- and on the default -ApiToken set, which has no -Username parameter, it
was never populated at all. Resolve-PfbAdminLocality early-returns without a
username, so the Fusion admin-locality gate was permanently inert for the most
common way people connect.

Invoke-PfbApiTokenLogin now returns @{ AuthToken; Username } instead of a bare
token string, and the native username/password path parses the body too. Where
both a response username and a caller-supplied one exist the response wins: the
array's own spelling is what GET /admins?names= has to match. Certificate/OAuth2
keeps its parameter value -- a JWT exchange returns no username and there is no
/user endpoint to look one up from. Username is also normalized to $null rather
than the '' an unbound [string] parameter yields, so "no username known" is one
value on every path.

Comments in Resolve-PfbAdminLocality, Add-PfbContextErrorAnnotation and
Connect-PfbArray that documented the gate as ApiToken-inert are corrected, since
this change falsifies them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…in assertions

Task 12b review minors, all comment- or assertion-level:

- The docstring on Get-PfbLoginResponseUsername justified its PSObject.Properties
  read as required under StrictMode. That was false: Set-StrictMode appears nowhere
  in the module or the Pester harness, so a direct .Content read would return $null
  rather than throwing. The read is worth keeping, but as a deliberate defence
  against test doubles and rewritten responses -- not as a forced workaround. The
  test comment that repeated the claim is corrected too, and now says what the test
  actually pins: the observable contract, not the implementation choice.

- 'leaves Username $null on the ApiToken set' asserted the exact value any swallowed
  failure would also produce. Added an AuthToken co-assertion so a green proves the
  login succeeded and the $null is the parse's answer rather than wreckage.

- Two Username assertions used -Be where their four siblings use -BeExactly. Case
  is the whole point of this property; the file now teaches one idiom.

- Connect-PfbArrayInternal is a third /api/login site and is now the only one that
  does not read the username from the body. Documented why that is correct (reconnect
  mutates the connection in place, so there is nothing to refresh) plus the one
  marginal gap it leaves, so the next reader does not take it for an oversight.
…not the caller's

The best-effort read/mint of a long-lived API token matched $item.admin.name against
$Username -- what the caller typed. That name comes from the same array as the login
body, so the array's own spelling is the only one that can be relied on to match it.
Now keys on $resolvedUsername. Maintainer's call.

Be precise about what this closes, because the first description of it was wrong.
PowerShell's -eq is CASE-INSENSITIVE, so 'pureuser' vs 'PUREUSER' always matched and
case was never the failure mode. What did miss is a name differing beyond case: a
directory-service admin logging in as 'jdoe' against an array that records
'jdoe@corp.example'. The match failed, no token was cached, and the session silently
lost auto-reconnect with only a -Verbose line to say so.

The new test therefore uses a directory-qualified name rather than a case difference.
A case-only fixture would have passed against the old code too, proving nothing.

?names= on the mint POST is switched over for consistency only -- the endpoint ignores
it and acts on the authenticated admin regardless.
…pe cmdlets

Adds tools/Update-PfbContextHelp.ps1, which renders a delimited
.NOTES block into every cmdlet whose endpoint carries a non-default
(fleet or unknown) contextScope in Data/PfbCapabilityMap.json -- the
same field the four client-side context gates validate against, so
the help cannot drift from the enforced behaviour.

24 Public/ cmdlets affected (8 fleet-scoped preset endpoints, 16
unknown-scoped). Three fleet-scoped topology-group endpoints have no
cmdlet in Public/ at all and are reported as MissingCmdlet rather
than silently skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The MissingCmdlet assertion only checked that the property existed, so
deleting the reporting entirely would have survived it. Replaces that
with an invariant: every non-array contextScope endpoint in the
capability map must appear verbatim in a generated help block or in
MissingCmdlet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…them

Task 14 review findings I1, M1, M2 and M4.

I1: a non-default contextScope value with no arm in Get-PfbContextHelpBody was
dropped silently -- no warning, no summary entry -- which is the same "a silent
skip reads as covered everything" failure the MissingCmdlet path already exists
to prevent. The only difference is which half of the pairing is absent: there
the cmdlet, here the render arm. Generate mode now warns and records the
endpoint in a new UnrecognisedScope collection. -EmitLineOnly still returns a
bare $null, which is what its array-scope test pins.

This is defence in depth rather than a live bug: Build-PfbCapabilityMap maps an
unrecognised domain token to 'unknown' rather than inventing a scope value, and
Build-PfbCapabilityMap.Tests.ps1 asserts every endpoint's scope is one of
fleet/array/unknown, so a new value cannot reach the shipped map without a
deliberate edit in two other places. Hard to reach is not a reason to drop the
endpoint quietly once it is reached.

M2 and the phase's tri-state ruling on an explicitly empty scope: the selection
test was a truthiness check, folding an empty scope in with an absent one and
treating both as the 'array' default. An ABSENT scope is unset and correctly
needs no note; a scope that is PRESENT and empty is a recorded value that says
nothing, so it is now reported rather than guessed at. The test mirrors the
generator's explicit null test for the same reason.

M1: replaced the one banned Should -Not -BeNullOrEmpty, which also makes the
file header's claim about it true. The negated form is unambiguous on its own,
but the ban stays case-analysis-free.

M4: the orphan-.NOTES limitation was documented only in the SDD report, which
does not travel with the code. It is now in the generator's .DESCRIPTION.

Tests: 13/13 both editions, 0 skipped (11 existing + 2 new).
…et-Help

Get-Help -Full renders the opening delimiter verbatim, so users were shown
"generated from Data/PfbCapabilityMap.json contextScope. Do not edit." -- an
instruction addressed to a maintainer editing the .ps1. Shortened to
"<!-- PfbContext (generated; do not edit) -->", which still marks the text as
generated without the maintainer-facing detail, and regenerated all 24 blocks.

Because that line is now expected to be reworded again, the strip phase keys on
the stable "<!-- PfbContext" prefix instead of the full opening literal. Matching
the whole literal would make any reword strip nothing and insert a second block
beside the first, compounding on every later run -- and the idempotency test
cannot see it, because by then the tree is already current and nothing needs
stripping. Guarded by a new test whose fixture carries the previous real wording.
I5: the bare Should -Throw in 'does not repoint either cache when the
composition is invalid' was satisfied by any throw raised before the cache
write, including a login or mock-harness failure -- pin it to the composition
message the way the sibling It 15 lines above already does.

I6: replace all three -BeNullOrEmpty uses. The consequential one is the
positive-polarity assertion on the tri-state AllowErrors, which passed for ''
and @() and so never pinned the contract it reserves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
I2: the Assert-PfbContextAdminLocality call in the request path cannot fire in
production -- both AdminLocality resolution sites gate before returning a
connection -- so describe it as defence-in-depth against a future third site or
a hand-constructed connection, and point at the code-20 annotation for the
Invoke-PfbInContext-only session. Call kept.

M1: AdminLocality is not 'reserved for a later phase'; it is populated in the
same function.

Record the adjudicated I1 ruling on Get-PfbEndpointContextScope: dropping
provenance is deliberate, because 'default' means no override AND not flagged
x-pure-incomplete-gre, which is evidence rather than absent metadata.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on GET

I3: Get-PfbContextHelpBody emitted 'targets a fleet-scoped resource and requires
a bare fleet context' for every verb. Assert-PfbContextRequired is narrower for
GET, twice over and both narrowings measured: an unfiltered read works with no
context (served from the locally replicated, list-only copy), and a name-scoped
read needs a context only on the endpoints in the measured allowlist. So the
shipped .NOTES in Get-PfbPresetWorkload told the reader a fleet context was
required six lines below an .EXAMPLE making exactly that context-free call.

Add a GET arm keyed on the method parsed out of the endpoint key, split by the
module's own $script:PfbNameScopedContextRequiredEndpoints -- dot-sourced, not
copied, since a local copy is the very help/validation drift this generator
exists to prevent. Regenerated Public/; one file changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…-map key

I4: Assert-PfbContextCapability treats 'no map entry at all' identically to
'entry without context_names', so with a session context set a cmdlet whose
endpoint is absent from the map throws a false 'does not support the
context_names parameter' before the wire. This feature is what makes a map gap
fatal rather than merely uninformative, so it owes the invariant that catches
the next one. AST discovery, no hardcoded files or lines, with a mandatory
count guard so a scan that matched nothing cannot pass forever.

The expected-unresolvable set is an exact-match tripwire in both directions and
is deliberately self-retiring: GET /smtp and PATCH /smtp both go away with
issue dmann000#80, and the failure message says to delete the set rather than update it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tions

M9: the shipped-artifact test pinned scope and the fleet count but not
provenance, so a regeneration that kept 'fleet' and flipped 'declared' ->
'default' would pass. The kind-vs-scope gate's whole design rests on that
distinction, so pin GET /presets/workload as fleet/declared and pin the
distribution of the three evidence-bearing provenances.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…has landed

The endpoint-coverage invariant shipped with a deliberately self-retiring
allowlist of GET /smtp and PATCH /smtp, asserted by exact equality so that it
would fail in both directions: on a new map gap, and on the SMTP consolidation
landing.

dmann000#80 landed in PR dmann000#108 and the tripwire fired as designed. Get-PfbSmtp and
Update-PfbSmtp are gone, consolidated onto /smtp-servers, and the module now
carries no REST 1.x surface at all -- zero -ApiVersionOverride sites. The
expected set is therefore deleted rather than updated, per its own failure
message. Tests/RemovedCmdlets.Tests.ps1 (from dmann000#108) guards the removal itself.

Every endpoint literal under Public/ now resolves to a capability-map key with
no exceptions.
Phase 1 modifies 28 Public/ cmdlets and touches tools/Build-PfbCapabilityMap.ps1
but changes nothing under Reports/, so the committed artifacts went stale the
moment the branch rebased onto main. Regenerated with the full chain in order:
capability map, field-cmdlet map, response-shape map, drift report.

Data/PfbCapabilityMap.json came back byte-identical to the committed copy,
confirming the generator change does only what it claims. Data/PfbResponseShapeMap.json
is unchanged.

Two deltas, both expected and attributable:

  * Drift report -- Resolve-PfbAdminLocality now appears alongside Get-PfbAdmin
    for GET /admins, because it probes is_local there; and Update-PfbArray's
    recorded paramBlockLine moves 29 -> 37, because the generated context help
    block sits above the param block.

  * Field-cmdlet map -- "typed but unresolved wire name" rises 40 -> 51. The 11
    additions are the context parameters on Connect-PfbArray, Invoke-PfbInContext
    and Set-PfbContext. These configure session state rather than a request body,
    so having no wire field is correct rather than a gap.

Regenerated, never hand-edited: a prior integration produced a silent auto-merge
of Reports/PfbFieldCmdletMap.json matching no generator run on either side.
…p block

The generated context help block was assembled with hardcoded CRLF and spliced
into whatever the cmdlet file already contained. The repo commits LF blobs, so a
Windows checkout has CRLF and matches, while a Linux/macOS checkout has LF and
mismatches on every file. The generator therefore reported all 24 files as
changed and rewrote them, and the idempotency test failed with "Expected 0, but
got 24" on ubuntu-latest and macos-latest while both windows-latest legs passed.

Set-PfbContextHelpBlock now detects the newline from the content it is splicing
into and normalises the block to match, rather than trusting the platform: the
checked-out file decides this, not [Environment]::NewLine. The two separators the
function inserts itself follow the same value. The strip pattern already handled
both, so it is unchanged.

Behaviour-neutral on Windows -- a -WhatIf run against the committed tree still
reports zero changes -- so no Public/ file needed regenerating.

Adds two tests that build LF and CRLF fixtures explicitly via WriteAllText and
run the generator against each. The pre-existing idempotency test could never
catch this on a single runner, because it only ever sees the local platform's
convention; these fail on every platform when the splice stops adopting the
target's line ending. Verified by mutation: pinning the newline back to CRLF
reds the LF case and leaves the CRLF case green, which is exactly the asymmetry
that kept Windows CI green while Linux and macOS failed.
Phase 1 delivers context_names, so the shipped drift report claiming it is
"not yet implemented" contradicts the same change that implements it. The
report still lists the 269-endpoint gap -- the drift detector matches only a
literal $Var['key'] IndexExpressionAst, and the injection site in
Private/Invoke-PfbApiRequest.ps1 writes
$QueryParams[$script:PfbContextParameterName], so the scan cannot see it.
The note now says that, and points at issue dmann000#113, which owns teaching
Get-PfbCentralInjectionSites to resolve the constant. Removing the gap itself
is deliberately NOT done here: context_names is the largest single systemic
gap, and dropping it moves the Task 6 real-data invariants, whose top-10
aggregation ratio is presently 54.81% against a 55% ceiling. That needs its
own measurement and a reviewed re-tune.

allow_errors stays a gap and keeps its "not yet implemented" note -- it is
genuinely unimplemented, deferred to Phase 2 by this branch's own spec
(docs/design/fusion-context-phase-1-spec.md, "Phase 2 -- allow_errors
end-to-end"). Only the wording gains the deferral, so the distinction between
the two fields is visible in the report.

Both entries referenced docs/design/fusion-context-injection.md, which does
not exist in the repo and never did -- that design doc was never committed.
Both now point at the Phase 1 spec, which covers context_names' implementation
and records allow_errors' deferral.

Regenerated Reports/ so the committed artifacts match the annotation source:
10 changed lines total, all annotation propagation, no unrelated drift.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ding

The two Task 7 annotation tests pinned a substring of each note's prose
('not yet implemented', '403'). Rewording the context_names annotation in
f5d2ef3 turned that into a red build on all three pwsh legs -- one failure,
identical on ubuntu/macos/windows, and green on 5.1 only because the whole
Describe is PS7-gated.

Assert the wiring instead: read docs/drift-annotations.json, and require the
generator to attach each annotation's note to its matching finding verbatim.
Rewording, retiring, or adding an annotation is now a docs-only change, while
a broken or paraphrasing annotation path still fails. Each test carries a
non-vacuity floor, so a lookup that silently matches nothing fails rather than
passing over an empty loop.

Endpoint matching uses .Contains on lowered strings rather than -like, which
treats backtick and [ ] as pattern syntax.

Mutation-verified: paraphrasing the note in the generator, and making
Find-PfbDriftAnnotation match nothing, each turn both tests red; clean tree is
65/0/0 on pwsh7.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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