Skip to content

Audit every pipeline-bound selector against the response schema, and guard the ones that cannot bind (#90) - #122

Merged
juemerson-at-purestorage merged 30 commits into
dmann000:mainfrom
juemerson-at-purestorage:fix/issue-90-pipeline-selector-fixes
Aug 18, 2026
Merged

juemerson-at-purestorage merged 30 commits into
dmann000:mainfrom
juemerson-at-purestorage:fix/issue-90-pipeline-selector-fixes

Conversation

@juemerson-at-purestorage

Copy link
Copy Markdown
Collaborator

Closes #90.

What this does

#90 asked for three things: audit every pipeline-bound selector against the field its resource
actually returns, decide per cmdlet whether to guard / alias / drop, and add a rail so a future
cmdlet cannot reintroduce the defect quietly. All three are here.

The audit is observational, not inferred. tools/Build-PfbPipelineSelectorMap.ps1 imports the
shipped module, shadows the request layer inside module scope with a capture shim, pipes a
schema-derived probe object into the real cmdlet, and records what actually reached the wire.
Nothing in the report is a conclusion drawn from reading source.

Result over 1,247 probe pairs (1,213 evaluated, 629 genuine candidates, control leakage 0):

Outcome Rows Meaning
Bound 578 bound correctly, no coercion
Coerced 264 the defect: whole object stringified into the selector
Unbindable 234 PowerShell declined to bind this probe object
Guarded 116 a coercion guard fired
NoSelector 47 nothing pipeline-bound to coerce
CmdletError 6 the cmdlet threw before a request was built
BindError 2 the only unmeasured outcome, pinned so it cannot grow silently

A correction to the issue's premise

#90 (and the tooling written for #64) states that binding resolves in three passes. It resolves
in four — ByValue, ByPropertyName, ByValue with coercion, then ByPropertyName with
coercion
. Measured on pwsh 7.6.4.

This is not pedantry, it changes a remedy. The three-pass model implies that removing
ValueFromPipeline makes coercion structurally impossible, so Unbindable was documented as
immunity. It is not: a ValueFromPipelineByPropertyName-only parameter whose alias matches an
object-valued property still binds that object stringified at pass 4. Such a parameter needs a
guard, not an attribute removal. The tooling's comments and the Unbindable verdict text are
corrected accordingly.

What changed, by kind

A coercion guard — the one new runtime component. Private/Assert-PfbSelectorNotCoerced.ps1,
called as the first statement of process in 18 read cmdlets. It turns a silently unfiltered
result into an actionable error naming the property to pipe instead.

Two constraints in it are load-bearing and commented as such: it must stay imperative in
process
, because a ValidateScript failure on a pipeline-bound argument is non-terminating
and end would still issue the unfiltered request; and its message must keep the substring
stringified object, which is how the map generator classifies the Guarded outcome.

Its predicate is structural rather than textual. It compares the bound selector against the
original pipeline item's own ToString() and suppresses the check when the item exposes a property
that could legitimately have bound. That catches a Hashtable and a generic Dictionary, which
render as bare type names and were invisible to the previous @{ test, without the type-name
pattern matching that would falsely reject a resource legitimately named e.g. System.backup.

Selector keys corrected where a cmdlet sent a generic names/ids to an endpoint that declares
a resource-specific key instead, and parent names preserved so policy-family pipelines bind at
pass 2 rather than falling through to coercion.

Dead selectors removed — parameters the endpoint declares no key for, which were therefore
either dropped on the wire or sent as a key the array ignores.

Reports regenerated: selector map, dead-key report, field/cmdlet map, API drift report.

Breaking changes

Please read this section rather than skimming it. Every removal below is a parameter that could
not work
— the endpoint declares no corresponding query key at any cached spec version, so the
parameter either silently did nothing or put a key on the wire that the array ignores while
returning the unfiltered collection. The authoritative pinned list is
Tests/PfbDeadSelectorRemoval.Tests.ps1.

Renamed, old name still callable — not breaking

  • Update-PfbBucketAuditFilter: -MemberName → -BucketName, -MemberId → -BucketId,
    -FilterNames → -Name. All three old names retained as aliases.
  • Get-PfbLocalGroupMember: -Group → -GroupName, with Group retained as an alias.

Parameters removed — breaking

Cmdlet Removed
Get-PfbBucketAccessPolicy -Id, -MemberId, -MemberName, -PolicyId, -PolicyName
Get-PfbBucketAccessPolicyRule -Id, -MemberName
Get-PfbBucketAuditFilter -MemberId, -MemberName
Get-PfbBucketCorsPolicy -Id, -MemberId, -MemberName, -PolicyName
Get-PfbBucketCorsPolicyRule -Id, -MemberName
New-PfbBucketAccessPolicy -MemberName, -PolicyName
New-PfbBucketAccessPolicyRule -MemberName
New-PfbBucketAuditFilter -MemberName
New-PfbBucketCorsPolicy -MemberName, -PolicyName
Remove-PfbBucketAccessPolicy -MemberName, -PolicyName
Remove-PfbBucketAuditFilter -MemberId, -MemberName
Remove-PfbBucketCorsPolicy -MemberId, -MemberName, -PolicyName
Get-PfbCertificateGroupCertificate -Name
Get-PfbObjectStoreTrustPolicyRule -PolicyId
Get-PfbOpenFile -Filter, -Name, -Sort
Remove-PfbOpenFile -Name

On Get-PfbOpenFile, filter and sort are declared at no version 2.17–2.28 (the endpoint
does not exist before 2.17), so they were inert.

Note one deliberate asymmetry: the Remove-Pfb* bucket cmdlets underwent the same
MemberName/MemberId rename but were given no compatibility aliases, while Update-* was.
Their legacy parameters wrote undeclared keys and so over-deleted; aliasing those onto a key that
does work would quietly change what a DELETE removes, which is worse than the break. The
rationale is recorded in the code so it does not read as an oversight.

Regression protection

  • Tests/PfbPipelineSelectorRail.Tests.ps1 — Rail A asserts no unwaived selector coercion,
    and surrounds it with waiver hygiene: every waiver must name a cmdlet, parameter, issue and
    reason, be used exactly once, still cover exactly the producer count it was granted for, and —
    the sharp one — carry no waiver for a pair that no longer coerces, so a stale waiver fails
    rather than lingering. Rail B regenerates the map and requires byte-identical JSON and
    Markdown.
  • Tests/Fixtures/PfbSelectorWaivers.psd1 — 101 entries, one per remaining coercion pair, each
    with Cmdlet / Parameter / Issue / Producers / Scope / Why. 100 are Family-scoped. These are
    pairs that cannot be fixed within one cmdlet: the coercion originates at a cross-endpoint
    producer, so the ceiling is documented per pair rather than waved at.
  • Six new test files (~2,600 lines) pinning the specific decisions.

Rail B self-skips when the gitignored tools/specs cache is absent, which is worth knowing when
reviewing from a fresh clone.

Verification

Unit — scoped, both editions. Per-task and final runs were scoped to the touched files and run
under pwsh 7 and Windows PowerShell 5.1, judged by the container column rather than the counts.
Final re-review run: pwsh 7 292 passed / 0 failed / container ok; WinPS 5.1
281 passed / 0 failed / 11 skipped / container ok.

The aggregate full suite was not run locally — it exceeds the local tool timeout and CI owns it.

Aggregate, cross-OS — CI, and it caught something the scoped runs could not. The first CI run
on this branch failed on all four OS/edition jobs: 3,162 passed, exactly 2 failed. Both failures
were in tests this PR itself pins, and neither was a defect in the shipped code — but they were
real failures and are worth reading rather than skipping to the green result below.

Both were baseline figures pinned during the Stage 1 audit that the Stage 2 fixes deliberately
invalidated — probePairs 1179→1247, findings 389→264, coerced pairs 127→101 in
Tests/Build-PfbPipelineSelectorMap.Tests.ps1, and the module-wide pipeline-bound population
303→311 with ValueFromPipeline 214→209 in Tests/PfbPipelineSelectorTools.Tests.ps1.

These escaped every scoped per-task run because they assert on the module and the committed
artifact as a whole rather than on the files any one task touched — the exact blind spot scoping
trades away, and the reason CI runs the aggregate.

Both are now re-baselined, and neither figure was simply overwritten. The population change was
verified by diffing Get-PfbPipelineBoundParameter between origin/main and this branch: 26 rows
added, 18 removed, net +8. Every added row is the new name of a renamed selector, a selector
replacing a removed dead one, or a parent-name selector added so a policy-family pipeline binds by
property name instead of coercing; every removed row is one of those old names or a dead selector
this branch deleted. No row changed its ValueFromPipeline value, which independently
corroborates the four-pass correction above — the attribute removal was rejected as a remedy, so
nothing used it. The coerced-pair figure is cross-checked rather than free-standing: 101 is also the
waiver fixture's entry count, and Rail A fails on a waiver for a pair that no longer coerces, so the
two cannot drift apart quietly. The reasoning is recorded in the tests, so a later reader can tell a
justified re-baseline from a silenced tripwire.

The re-run is green on all four jobs: 3164 passed / 0 failed / 2 skipped on pwsh 7 across
ubuntu, macos and windows, and 2908 passed / 0 failed / 258 skipped on Windows PowerShell 5.1.

That also settles the one thing only a full-suite result can measure. The coverage gate reported
Coverage gate passed for both the pwsh7 and winps51 profiles on every job — so the
Tests/coverage-baseline.psd1 ceilings hold as-is, and no ceiling was raised against an unmeasured
figure.

Live — a lab FlashBlade at REST 2.26, reads and scoped writes. Mocked tests pass while a wire
contract is wrong, so the guard and the corrected keys were exercised against a real array.

Gaps, stated rather than omitted:

  • The array holds no data for network-interface neighbors, certificate-group certificates, local
    groups/members, NFS export rules, or bucket access policies
    , so those paths are verified only
    for request construction and guard behaviour, not for correct rows.
  • 13 of the 18 guarded cmdlets were not individually exercised live.
  • Both POST cmdlets returned Skipped-Policy from the live harness's safety rails — the rails
    working as designed, not evidence of success.
  • The claim that Get-PfbOpenFile's removed -Filter/-Sort were inert is spec-derived, not
    measured on the wire.

Found along the way, filed separately

  • [P0] Empty list results return a coercing total_item_count wrapper; piping it can silently return the full collection #121 (P0) — Invoke-PfbApiRequest returns a one-element total_item_count wrapper on any
    empty list result, which is truthy, reports Measure-Object count 1 when the answer is zero, and
    coerces on the pipeline into names=@{total_item_count=0}. On at least one endpoint the array
    ignores that key and returns the full collection. Pre-existing on main, untouched here, and
    in the shared response layer, so it needs its own change and its own decision about -TotalOnly.
    The guard in this PR happens to catch it on its 18 cmdlets — useful evidence the approach
    generalises, but not a fix.
  • Three sibling guards (Assert-PfbAdminNameNotCoerced, Assert-PfbRemoteNameNotCoerced,
    Assert-PfbFileSystemReplicaLinkTransferNameNotCoerced) still document the falsified three-pass
    model. Pre-existing on main and out of scope here; worth a follow-up sweep.
  • One known gap is documented in the guard itself rather than papered over: a Hashtable-valued
    property binding at pass 4 via an alias passes both signals. Closing it means comparing against
    each binding property's ToString(), which widens the false-positive surface — a maintainer's
    call, not a drive-by. Reachability is low because ConvertFrom-Json yields PSCustomObject,
    whose ToString() does contain @{.

Two upstream specification defects were also established during this work and are being routed
to the API team rather than filed here, since neither is a module bug:

  • GET /file-systems/open-files advertises protocols=nfs, which the array rejects with
    Unsupported protocols specified in the parameters while accepting smb.
  • Ten collections declare ids and eight declare names over item schemas carrying no such field,
    so the selector silently does nothing and the caller gets an unfiltered 200.

Not included, deliberately

No version bump and no CHANGELOG entry — those are maintainer decisions in this repo.

Diff size

58 files, +10,231 / −4,933. The bulk is regenerated report artifacts and new tests; the runtime
change is one new private helper plus selector corrections across 35 files under Public/.

juemerson-at-purestorage and others added 30 commits August 16, 2026 21:35
Replace the nonexistent member selector with explicit policy/filter and bucket
selectors across bucket policy operations.

Co-Authored-By: Claude <noreply@anthropic.com>
…mann000#90)

Round-1 review fixes on the bucket policy/filter selector work:

- New-PfbBucketAuditFilter now sends the spec-required `names` on every POST,
  defaulting it from -BucketName, so naming a filter and identifying its bucket
  are combinable rather than mutually exclusive.
- Remove-PfbBucketCorsPolicy drops -PolicyName: `policy_names` is undeclared on
  the DELETE endpoint and is silently ignored, so combining it with a valid
  `bucket_names` deleted every CORS association on the bucket. The
  ShouldProcess target now states that blast radius.
- -MemberId/`member_ids` renamed to [string[]]-BucketId/`bucket_ids` on the
  five cmdlets that carried it; the endpoints declare `bucket_ids` only.
- -PolicyName/-PolicyId removed from Get-PfbBucketAccessPolicy and
  Get-PfbBucketCorsPolicy; those keys exist only on the /rules variants.
- ValueFromPipeline moved to -BucketName on the Remove-* pair so a piped bucket
  name means the same thing there as on the Get-* siblings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Expose certificate, realm, and local-port selectors under names matching their
published wire contracts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lift parent policy and role names on emitted objects so property-name pipeline
binding sends scalar selectors instead of stringified references.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lift scalar names from nested response references without rebuilding or
projecting API response objects.

Converges Get-PfbObjectStoreAccessPolicyRole onto the Task 3 enrichment
contract for both parent references, policy and member: the lift is now
conditional on the nested object and its name being non-null, and it no
longer -Force-overwrites an existing top-level property. The previous
unconditional -Force lift created PolicyName/MemberName holding $null when
the parent was absent, which still binds downstream by property name and
sends an empty selector, making the consumer return everything.

Get-PfbLocalGroupMember and Get-PfbArrayConnectionKey are unchanged; see
task-4-report.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lift group.name to a top-level GroupName property, and rename the selector
parameter from Group to GroupName so the lift can actually bind. Group and
group_name are retained as aliases, so -Group keeps working.

The rename is what makes the lift effective rather than cosmetic. During
by-property-name binding PowerShell resolves one property per parameter and
prefers the property matching the parameter NAME over any property matching
an alias, so a parameter named Group always resolved to the response's own
'group' reference object and never reached the lifted GroupName. Because
binding an object to [string[]] needs a conversion, the by-property-name pass
then failed outright and the next pass bound the whole piped item, putting a
stringified membership record on group_names. The report's own evidence for
the primary producer shows exactly that:

  group_names=@{context=; group=; group_gid=...; member=; member_id=...}

Naming the parameter after the lifted property is the idiom the eight
already-converged cmdlets use (PolicyName <- policy.name), so this makes the
lift surface consistent rather than introducing a special case.

Restates the alias assertion as what now holds -- GroupName is the parameter,
Group and group_name are its aliases -- and adds a case proving -Group is
still bindable on the command line, which is the compatibility half the
original assertion existed to protect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Drop public parameters that emitted query keys their endpoints never declared,
so a filtered call no longer silently returns the unfiltered collection.
)

Removing the dead -Name selectors took the only pipeline entry point on the
open-file and resource-access cmdlets with them, leaving
Get-PfbOpenFile | Remove-PfbOpenFile a hard binding error. The piped path never
actually worked -- names was silently ignored by these endpoints -- but ending an
issue about pipeline selector binding with a net loss of pipeline capability reads
as a regression on upgrade.

Add ValueFromPipelineByPropertyName to -Id on Get-PfbOpenFile,
Remove-PfbOpenFile, Get-PfbResourceAccess and Remove-PfbResourceAccess. The
response items carry id in every version the endpoints exist in (open-files
2.17-2.28, resource-accesses 2.19-2.28), and ids is the only declared DELETE
selector on both, so this binds a real selector rather than resurrecting a dead
key. Bare ValueFromPipeline is deliberately NOT added: piping a bare string has
no correct meaning now that the name selector is gone, and that coercion path is
what this work exists to remove.
Replace the nonexistent member selector on the remaining bucket operations and
drop the undeclared generic id selector, so a filtered call no longer returns
the unfiltered collection.

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

Follow-ups from task review: examples referenced fields their items do not
carry, and the no-bare-pipeline guarantee was asserted narrowly.

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

POST buckets/cross-origin-resource-sharing-policies/rules declares names as a
required query parameter, but the cmdlet sent no query parameters at all and so
was unable to satisfy it. Add the bucket, policy and rule name selectors the
operation declares, with the required key reachable in every parameter set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@
fix: correct example body shapes and guard empty required selectors (dmann000#90)

Examples on the two bucket policy rule cmdlets advertised request-body fields
and shapes the endpoints do not accept. New-PfbBucketAccessPolicyRule passed
principals as a string array, but BucketAccessPolicyRulePost declares
principals as an object shaped { all: boolean }, and constrains actions to
s3:GetObject alone; effect is readOnly, so no caller can send it. The
New-PfbBucketCorsPolicyRule examples used method and origin subsets the schema
documents as unsupported. All examples now show only content a caller could
really send, verified against every spec version the endpoints exist in
(REST 2.12 through 2.28), and the access-policy-rule test fixture no longer
implies a caller can send the readOnly effect.

Separately, a required selector could be silently dropped by an empty array.
Update-PfbBucketAuditFilter -Name is not Mandatory, so -Name @() and
-Name @() with an empty string bound successfully, ContainsKey reported true,
and the endpoint-required names query key went on the wire empty --
reconstructing the dead-selector defect this work exists to remove.
ValidateNotNullOrEmpty now rejects both forms, and is applied to the required
key parameters of the two rule cmdlets as well.

Access policy examples now use the module predominant fully-qualified form
mybucket/myaccount:policy. The per-parameter-set names assertion now asserts
per set rather than by total invocation count.
@
…dmann000#90)

New-PfbBucketAccessPolicy and New-PfbBucketCorsPolicy gained ValueFromPipeline on
their Mandatory -BucketName during this work, which let a piped bucket object bind
stringified into a selector on a write path -- an invocation that was previously
impossible. Both now match their sibling rule cmdlets and accept pipeline input by
property name only.

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

Seventeen read cmdlets declare ValueFromPipeline on a name selector. A piped object
matching none of their parameters by name falls through to by-value coercion and the
whole object goes on the wire stringified -- HTTP 200, and the array returns the
unfiltered collection, which is the defect this work exists to remove. The earlier
lifts fixed the self-chain; they cannot help when the piped item comes from a
different endpoint in the same family.

A shared guard now rejects that value with an actionable module-level error naming
the property to pipe instead. Bare-string piping and by-property-name binding are
unaffected. Removing ValueFromPipeline was rejected: it would break bare-string
piping, and it does not eliminate coercion where an alias matches an object-valued
property.

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

A fourth site still asserted that a ValueFromPipelineByPropertyName-only parameter
cannot coerce, contradicting the correction made alongside the new guard. The
Guarded legend and its help text credited every guarded row to dmann000#64, which will be
wrong for the majority of rows once the reports are regenerated. Also corrects the
new helper's inaccurate backtick rationale, a test title that named a distinction it
does not test, and a hint that missed the chain the guard actually fires on.

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

The seventeen guarded read cmdlets now classify as Guarded and the two bucket POST
cmdlets as Unbindable, dropping Coerced from 371 to 268 and Primary coerced pairs
from 21 to 2. Waivers for pairs the guard now covers are removed. The unresolved
wire-name ceiling moves to 126 for New-PfbBucketAuditFilter|Name, a parameter added
during this work that was never evaluable rather than one that stopped being so.

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

Several conclusions in this work are counter-intuitive enough to invite a well-meaning
future change that would reintroduce the defect: a cross-endpoint chain cannot filter
correctly and can only be made to fail loudly, the runtime lifts work but are
invisible to the selector rail by construction, and removing ValueFromPipeline is not
the general remedy because an alias matching an object-valued property still coerces.
Each affected cmdlet now carries a short in-line note pointing at the full reasoning.

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

Eight guarded read cmdlets carried the reasoning only in their runtime Hint text, which
addresses a caller who mis-piped rather than a maintainer deciding whether the guard is
worth keeping. Each now states in-line that a cross-endpoint chain has no correct generic
binding, that a loud failure is therefore the best available outcome, and that adding an
alias would turn the rail green without fixing anything.

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

The example-chain scanner reads every line of every Public cmdlet, comments included, so
the maintainer notes added for dmann000#90 registered "A | B" prose as documented example chains
and flipped FromExample on eight rows, reddening the regeneration rail. The notes now say
the same thing without a pipe between two cmdlet names, and the scanner's docstring records
the hazard. Also refreshes stale prose in the waiver register: a retained waiver's mechanism
sentence, an emptied cluster heading, and two claims the four-pass finding falsified.

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

Piping an array connection object into Get-PfbArrayConnectionKey silently
returned every key. GET /array-connections items carry no `name`, so the piped
object matched nothing by property name, fell through to PowerShell's
ByValue-with-coercion pass, and went onto the wire stringified as
`names=@{...}`. The array answers that with HTTP 200 and the UNFILTERED
collection, so the caller believes they are looking at one connection's key.

Two changes, both in Public/Replication/Get-PfbArrayConnectionKey.ps1:

- Add -Id, bound ValueFromPipelineByPropertyName. The endpoint declares the
  generic `ids` query key from REST 2.0 and an array connection carries `id`,
  so this is a real fix rather than a workaround: the object now binds at
  binding pass 2 and never reaches coercion. -Name and -Id occupy exclusive
  parameter sets because the spec states `ids` "cannot be provided together
  with the `name` or `names` query parameters" -- an illegal key combination
  is precisely the request that can come back 200 and unfiltered, so it is
  refused at bind time instead. Both keys are generic on this endpoint, so
  Add-PfbCommonQueryParams carries them unchanged.

- Add the Assert-PfbSelectorNotCoerced guard as the first statement of
  process, matching the seventeen read cmdlets guarded in 8efb810. The
  residual it closes is the self-chain: this endpoint's own items carry
  neither name nor id, so they still coerce.

-Name stays waived and un-aliased for the original reason -- the endpoint's
items are connection_key/created/expires, so no name can ever bind. The
param-block comment now states both halves: -Name remains impossible,
identity-based piping is supported via -Id.

The Get-PfbArrayConnectionKey|Name waiver in Tests/Fixtures/PfbSelectorWaivers.psd1
is deleted, not downgraded. Its four Coerced rows become one Guarded (the
connection-key producer itself) and three NoSelector, so the pair is no longer
a finding, and Rail A fails on a waiver whose pair no longer coerces. The new
Get-PfbArrayConnectionKey|Id rows measure Bound on the three producers that
carry `id` and Guarded on the one that does not. Report totals move from
268 rows / 102 pairs to 264 / 101; the Cluster 2 heading and the register
preamble are updated to match.

Reports/PfbPipelineSelectorMap.{json,md} regenerated in the same commit. The
only other delta is FromExample flipping true on the Get-PfbArrayConnection
producer row, which is the new .EXAMPLE chain being correctly detected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
b7ddff9 added -Id so that array-connection output piped into this cmdlet
would filter. That cannot work, and the published spec settles it:
components.schemas.ArrayConnectionKey -- the item type
GET array-connections/connection-key returns -- declares exactly
connection_key, created and expires at every version 2.0 through 2.28,
and is a flat object with no allOf, so it never inherits the base schema
that supplies id and name to ordinary resources. The generic ids and
names query keys select resources of the endpoint they are given to, by
the identifier those resources carry. An item carrying neither id nor
name gives ids nothing to match, so ids cannot select on this endpoint,
and the same reasoning applies to names.

The authoring error was reasoning from the PRODUCER endpoint's response
shape (GET /array-connections items do carry id) to the CONSUMER
endpoint's query capability. Those are different endpoints.

-Name stays -- the spec declares the key, so the cmdlet sends it -- but
its help no longer promises filtering the endpoint cannot do. The
process-block coercion guard is therefore the entire remedy: it turns a
silently unfiltered result into a loud error.

Also: reword the guard hint away from the -Id chain it now mispoints at,
drop the parameter sets and the pipeline .EXAMPLE that documented the
chain, correct the waiver fixture's prose to credit the guard alone, and
regenerate Reports/PfbPipelineSelectorMap.{json,md}. The four
Get-PfbArrayConnectionKey|Name rows go Coerced/NoSelector -> Guarded, the
four |Id rows disappear, and the FromExample flip reverts to false.
Reports/PfbDeadKeyReport.json regenerates unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Compare bound selector text with the original process input so object types whose ToString output lacks @{ cannot bypass the guard, while legitimate type-like resource names remain valid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restore legacy audit-filter aliases, remove undeclared open-file keys, document lift trade-offs, and regenerate the affected reports with tightened regression ceilings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Keep the monotone gate's explanatory counts aligned with its newly tightened issue-90 ceilings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Remove the last executable help example while retaining a one-line pointer to the existing drift record for the spec-required protocols gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correct the baseline comment to reflect that the monotone destructive allowlist intentionally retains seven obsolete ceiling entries beyond the 13 current findings.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Tighten the destructive and no-surviving-selector allowlists to the regenerated issue-90 artifact now that their stale entries are proven absent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The review fix removed every example from this cmdlet, leaving it as one of
only three public cmdlets with none. The pipeline example in particular
documented why -Id declares ValueFromPipelineByPropertyName, which is the
behaviour this branch exists to establish.

Both examples are reworded to avoid asserting how the array responds to a
request that omits the spec-required protocols key. The gap is named and
pointed at the drift report instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three documentation corrections from the scoped re-review of the fix round.

The BindingPropertyName docstring claimed the '@{' signal still catches an
object-valued property that pass 4 stringifies. That is false for exactly the
types the widened guard was written for: a Hashtable and a generic Dictionary
render as bare type names, so piping an object with a Hashtable-valued property
binds at pass 4 via an alias and passes both signals. The whole-input comparison
cannot cover it either, because the item's own ToString() is not the bound value.
The gap is now stated, along with why closing it is a maintainer's decision and
why a response object piped from a real cmdlet stays covered.

The restored Get-PfbOpenFile pipeline example attributed the -Id binding to this
cmdlet when it belongs to Remove-PfbOpenFile. Reworded, and the -Id example is
restored so the cmdlet's only selector is demonstrated.

The Update-PfbBucketAuditFilter aliases now record why the Remove-Pfb* cmdlets in
the same family deliberately received none, so the asymmetry does not read as an
oversight to the next reader.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI failed on all four OS/edition jobs with 3162 passed and exactly 2 failed.
Both failures were baseline figures pinned during the Stage 1 audit that the
Stage 2 fixes deliberately invalidated:

  Build-PfbPipelineSelectorMap.Tests.ps1  probePairs 1179 -> 1247,
                                          findings    389 -> 264,
                                          coerced pairs 127 -> 101
  PfbPipelineSelectorTools.Tests.ps1      bound rows  303 -> 311,
                                          ValueFromPipeline 214 -> 209

Neither number was simply overwritten. The module-wide population change was
verified by diffing Get-PfbPipelineBoundParameter between origin/main and this
branch: 26 rows added, 18 removed, net +8. Every added row is either the new
name of a renamed selector (MemberName -> BucketName across the bucket-policy
cmdlets, Name -> CertificateName / GroupName / LocalPortName / RealmName), a
selector replacing a removed dead one (-Id on the open-file and resource-access
cmdlets), or a parent-name selector added so a policy-family pipeline binds by
property name rather than coercing. Every removed row is the old name of one of
those renames or a dead selector this branch deleted. No row changed its
ValueFromPipeline value, which matches the branch's finding that removing the
attribute grants no structural immunity under four-pass binding.

The coerced-pair figure is cross-checked rather than free-standing: 101 is also
the entry count of Tests/Fixtures/PfbSelectorWaivers.psd1, and Rail A fails on a
waiver for a pair that no longer coerces, so the two cannot drift apart quietly.

The reasoning behind each figure is recorded in the tests themselves, so a future
reader can tell a justified re-baseline from a silenced tripwire.

Also renames the first test from 'reproduces the Stage 1 headline numbers' to
'reproduces the measured headline numbers', since it no longer pins Stage 1.

The rest of the run was clean: the real coverage gate reported "Coverage gate
passed" for both pwsh7 and winps51 with a tree-wide skip count of 2 against a
ceiling of 5, so no baseline ceiling needed raising.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@juemerson-at-purestorage
juemerson-at-purestorage merged commit a30e398 into dmann000:main Aug 18, 2026
5 checks passed
@juemerson-at-purestorage
juemerson-at-purestorage deleted the fix/issue-90-pipeline-selector-fixes branch August 18, 2026 05:14
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.

Audit pipeline-bound selectors against actual response fields (pass-3 coercion silently sends stringified objects as filters)

1 participant