Suppress the bare total_item_count wrapper and stop empty pipelines from issuing unfiltered requests (#121) - #125
Merged
juemerson-at-purestorage merged 9 commits intoAug 19, 2026
Conversation
Keep count-only responses for explicit total-only reads while ordinary empty list responses, with or without an items key, emit no object. Co-Authored-By: Claude <noreply@anthropic.com>
Centralize the policy that distinguishes direct unfiltered calls from piped invocations that received no selector-bearing object. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Generate one explicit end-block guard for every collect-then-request cmdlet, including the two human-reviewed control-flow outliers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The count-only response now reaches a caller only on an explicit total-only request, which this cmdlet cannot make, so record the check as defence-in-depth rather than a live path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AST-walk the public surface for missing predicate calls and preserve the direct Invoke-PfbApiRequest call-shape invariant. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…00#121) Cover the multi-key case that a -contains rewrite would pass, assert the response mock was reached, and isolate the shape tests from the capability map. Also close the vacuous StrictMode test: Set-StrictMode inside the It does not cross into the module session state, so it is set inside InModuleScope, where deleting the $null clause from Test-PfbEmptyPipelineRead measurably reds it. Add the missing ExpectingInput pair and the trailing newline. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The AST rail proved a guard EXISTS, was not nested in a scriptblock, and read the same hashtable the request receives. Three shapes still passed it while doing nothing, and all three also satisfy the generator's AlreadyPresent recognizer, so the drift gate could not catch them either: - a guard placed BELOW the Invoke-PfbApiRequest it is meant to stop; - a bare `Test-PfbEmptyPipelineRead ...` whose boolean is discarded; - a guard hoisted ABOVE `Add-PfbCommonQueryParams -Into $queryParams`, which the old index-assignment-only write detector could not see. Adds GuardAfterSomeInvoke and GuardReturns (the call must be the condition of an if whose taken branch returns), and broadens the post-guard write detector to cover member assignment, IDictionary mutator calls, and command parameter writes such as -Into. All new allowlists are explicit and empty; $allowedPostGuardWrite still holds exactly one entry. Also adds a positive control for the one-way scriptblock-nesting detector and raises the population floors to pin the end-block invoke count. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…branches (dmann000#121) The no-items classifier is intentionally not restricted to total_item_count -eq 0, while the final-return gate below it deliberately keeps its items-count conjunct. The neighbouring comment argues for conservatism persuasively enough that a later reader could "align" one with the other; say why they differ instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…mann000#121) The two rails that make the guards load-bearing were subject to MaxSkipped only, which cannot see a Describe that disappears: a file filtered out of a run, or deleted outright, contributes neither a skip nor a pass. Breaking a guard was already a red build, but removing the rail that checks the guards was a green one. That is the issue-dmann000#63 failure shape one level down, and RequiredDescribes exists to catch it. Registered in both edition blocks. Neither rail carries a PS7 gate and neither reads the spec cache, and the generator deliberately declares 5.1 rather than 7.0 so its real-tree fixed-point check runs on either leg, so requiring them on Windows PowerShell 5.1 is not a false red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 19, 2026
juemerson-at-purestorage
added a commit
that referenced
this pull request
Aug 23, 2026
Define the policy Test-PfbEmptyPipelineRead has been approximating since PR #125: an empty pipeline must never become a request that returns or mutates objects the caller did not address. Classify the non-selector keys rather than the selectors, so that a key nobody classified degrades to a missed guard rather than a discarded request. Fifteen entries, against a selector allowlist that would need 23 today and grow with every endpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
juemerson-at-purestorage
deleted the
fix/issue-121-empty-result-wrapper
branch
August 25, 2026 20:25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Suppress the bare
total_item_countwrapper and stop empty pipelines from issuing unfiltered requests (#121)Fixes #121. The one instance of that issue's harm class this branch does not close is
tracked separately as #126 — see The guard's scope limit below.
Two further findings from this branch's own review are filed rather than folded in, so that
neither lives only in a merged PR body. Both are independent of whether this merges:
request and has its answer used, but not that it executes on every path. See Tooling and
rails.
Remove-PfbLocalGroupis the only write cmdlet in the module issuing its requestfrom an
endblock, which is why it is the one guard here placed by hand. See Deliberatebehaviours and named outliers.
Summary
Issue #121 reported that an empty list result does not come back as an empty
collection. Instead the caller receives a single object whose only property is
total_item_count = 0. That object is truthy,Measure-Objectcounts it as oneitem, and piping it into another cmdlet sends
names=@{total_item_count=0}on thewire — where the receiving endpoint ignores the malformed key and returns
everything it has. A read that matched nothing therefore turns into an unfiltered
read of the next resource.
This branch closes both halves of that behaviour:
write. A new private predicate,
Test-PfbEmptyPipelineRead, is consulted byevery public collect-in-
processcmdlet that issues its request fromend, andthose cmdlets now return without dispatching when a piped invocation produced no
selector at all.
total_item_countwrapper. It returns nothing. An explicit-TotalOnlycallstill returns the wrapper, unchanged — that shape is preserved, not introduced.
The second point is a behaviour change for any caller that was reading
.total_item_countoff an ordinary (non--TotalOnly) empty result. That is thecompatibility risk in this PR and it is deliberate.
The diff, with scopes attached
Both of the figures below are correct, for different scopes, and they are easy to
confuse:
Public/git diff --shortstat <base>..HEAD -- PublicPublic/git diff --shortstat <base>..<gen-commit> -- PublicThe branch total is the first row. The second row is the guard-generation commit on
its own, and it is the right number for the line-ending claim: 127 of those files
were rewritten programmatically by a generator, and the commit carries zero
deletions, so no file had its line endings flipped. The whole of the branch's delta
over that commit — the extra file, the extra ten insertions and all six deletions — is
one comment-only change, the corrected sentinel rationale in
Get-PfbFleetMember.ps1(10 6in--numstat). Separately, the generation commit's132 insertions are the 130 guard lines plus two placement-explaining comments in
Get-PfbRemoteArray.ps1(3 0), which is why 130 files carry 132 added lines.Across the whole branch, the added
Public/lines deduplicate to 13 distinctlines: 12 comments and exactly one line of code —
That single statement is byte-identical in all 130 files that received it. The
stronger form of that claim, which does not depend on how a shell sorts: a scan of
every file under
Public/finds 130 occurrences ofTest-PfbEmptyPipelineReadacross 130 files, and exactly one distinct line text among them (compared
ordinally, since
Sort-Object -Uniquecompares culture-linguistically and thisproject has already seen 5.1 and 7 disagree on that order). There is no other
mention of the predicate anywhere under
Public/.Outside
Public/, the branch touches ten files:Private/Invoke-PfbApiRequest.ps1(+45/-4, of which 7 added lines are comment), the new
Private/Test-PfbEmptyPipelineRead.ps1(23 lines), the newgenerator
tools/Update-PfbEmptyPipelineGuards.ps1(250 lines), five new test files,one modified test file, and
Tests/coverage-baseline.psd1. NoData/,Reports/or.github/file is touchedanywhere on the branch —
git diff --stat <base>..HEAD -- Data Reports .github toolsreturns only the generator. The module manifest, the module version and
CHANGELOG.mdare untouched; version and changelog decisions are yours.What suppresses the wrapper, and what does not
Private/Invoke-PfbApiRequest.ps1carries two changes, and it matters which one isresponsible for the measured effect.
1. The final-return gate — this is the change that suppresses the wrapper, and it
is live-exercised. It is one added flag plus one added conjunct, and both response
paths reach it:
The gate is not specific to the
items-present path. The no-itemsclassifierdescribed below assigns
$totalItemCountand falls through to this same return, soone gate covers both shapes — which is the reason the classifier is a normalizer
rather than an independent suppression mechanism.
The discriminator has to be the request, not the response. Measured against FB-A
at REST 2.26, a total-only response and an ordinary empty result are the same four
keys and differ only in the value of
total_item_count:So no test on the response can tell them apart, and a guard keyed on a non-zero
count would hand the wrapper straight back to an ordinary caller.
$QueryParamsalready carries the answer, because
Add-PfbCommonQueryParamswritestotal_onlythere when the caller passed
-TotalOnly. The gate reads the value rather than thekey, so it stays correct if that helper's separate
ContainsKeybehaviour is everchanged.
2. The no-
itemsclassifier narrowing — defensive, and entirely unexercisedlive. A body with no
itemskey used to be returned verbatim as direct data.It now returns verbatim unless it has exactly one property named
total_item_count, in which case it is treated as an empty list response. Theclassifier is deliberately not also restricted to
total_item_count -eq 0, while thefinal-return gate above deliberately keeps its
$allItems.Count -eq 0conjunct; thetwo apply opposite defaults on purpose, because a no-
itemsbody has no items topreserve and a total-only read might unexpectedly carry some. A comment in the file
records that asymmetry so it does not read as an oversight to be "aligned" later.
This is defensive code and should not be credited for the measured suppression. A
scan of all 210 literal GET endpoints the module's own
Get-*cmdlets reference,run against FB-A at REST 2.26, found zero count-carrying bodies without an
itemskey (180 haveitems, 29 returned the array's own 4xx/5xx, and exactly oneis genuine direct data —
keytabs/download, whose sole property isLength). Abranch-selection measurement confirms the classifier does not fire even on the two
reads whose behaviour actually changed: for
file-systems?filter=<no-match>,file-systems/open-filesandfile-systems?total_only=true,$null -ne $response.itemsisTrueand the count-only classifier evaluatesFalseon allthree. They take the
itemspath and fall through to the final return.The classifier is therefore neither confirmed nor refuted by live evidence. Its only
detector is the AST/behavioural rail added in
Tests/Invoke-PfbApiRequest.EmptyResult.Tests.ps1,which reds if the exactly-one-key test is ever silently broadened to
-contains 'total_item_count'. That rail is the reason the narrowness is safe toship unexercised, and it is worth knowing that it is the only thing holding it.
Live verification on FB-A (REST 2.26)
Measured against the same array, in the same session, with only the module path
differing between the two runs and the git ref confirmed on each side:
main(branch base)Get-PfbFileSystem -Filter "name='zzz-no-such-filesystem'"total_item_count = 0Get-PfbOpenFiletotal_item_count = 0Get-PfbFileSystem -TotalOnlytotal_item_count = 2total_item_count = 2(unchanged)Get-PfbFileSystem(control)The unfiltered control is what makes this a before/after comparison rather than two
readings — it rules out a connection or array-state difference between the runs.
Proving that no request was issued
A zero result count cannot distinguish "the guard fired and no request went out"
from "a request went out and came back empty". So
Invoke-PfbApiRequestwasshadowed by a counting stub in module scope. The guard returns before that call,
so any capture means the guard failed to fire.
@(): 0 requests.Remove-PfbLocalGrouppiped@()with-Confirm:$false: 0 requests, nothingtransmitted.
Get-PfbBucketcalled directly with the same shim: 1 request(
GET buckets).Without that control the zeros prove nothing, so it is included deliberately.
A full-population sweep against FB-A classified the 130 as: SUPPRESSED-ZERO 74,
UNINFORMATIVE-EMPTY 46, UNKNOWN 7, DEFERRED 3.
Get-PfbAuditwent from 759 objectsto 0,
Get-PfbSession237 to 0,Get-PfbPolicyAll45 to 0. Please read theUNINFORMATIVE-EMPTY and UNKNOWN rows as limits, not passes — see Known limits
below.
Pre-merge re-verification: the positive path
The evidence above is almost entirely the suppression path. A second live run was done
before requesting merge, aimed at the opposite question — that normal usage still works.
31 harness rows, all
Pass, array left exactly as found and no residue outstanding.The complete three-way discrimination, on a bucket created for the purpose:
Note the harness calls cmdlets directly, so every pipeline assertion here is a labelled
direct probe rather than a harness row; the rows cover the direct-call and selector
assertions. Both write lifecycles completed with each phase as its own call: bucket
create → read → shrink → destroy → eradicate → back to 1; and a fixture file system →
2 snapshots → eradicate each → destroy → eradicate → back to 2 file systems and 28
snapshots.
The fix itself, measured against its own control — same array, same session, only the
module path differing, with the
maincheckout ata30e398, this branch's exact base:main@a30e398branch@f3dca14@() | Get-PfbFileSystemSnapshot@() | Get-PfbFileSystemTwo apparent anomalies surfaced during the run and were both cleared as pre-existing
by re-running against
main. Recording them because each would read as a regression inthis branch without the control:
Get-PfbFileSystemSnapshot -Nameand-SourceNamedo not filter — the endpoint returnsall 30 either way, explicit or piped, identical on
main. This is not a new discovery:Reports/PfbDeadKeyReport.jsonalready records both asWRONG-RESULTS(the endpointdeclares
names_or_owner_names, notnames). The live run independently confirms thestatic report. The control that distinguishes a dead key from a broken endpoint is that
-Limit 5returns 5 and-Filter "source.name='…'"returns exactly the 2 fixturesnapshots, on the same endpoint in the same request shape.
Get-PfbArrayConnection -RemoteName 'FB-B'returns 3 objects where 2 are correct. Theextra object is a stray
$true:Private/Assert-PfbRemoteNameNotCoerced.ps1ends withreturn $true, and none of its four call sites suppress it —Get-PfbArrayConnection,Get-PfbArrayConnectionPath,Get-PfbArrayConnectionPerformanceReplicationandRemove-PfbArrayConnection. Itoriginates in fix(replication): select array connections by remote_names, not the dead names query key #91 (commit
c6a0f1a, theremote_namesfix for Deadnamesquery key breaks -Name on five array-connection cmdlets and Update-PfbAsyncLog (unfiltered-PATCH risk falsified by live testing) #64), not here: thisbranch's only change to that file is the one guard line, and it does not touch the helper
or the
processblock where the value leaks. Being filed separately.Worth flagging for its own sake, since it is a live defect on
main: the-RemoteNamepath emits one stray
$trueper supplied name ahead of the real objects, so a caller's| Measure-Objectovercounts and| Select-Object -First 1gets the boolean rather thana connection. The negative control confirms the query key itself is fine —
-RemoteName 'zzz-nonexistent'returns HTTP 400 "Array connection does not exist", and-Filter "remote.name='FB-B'"returns exactly 2.A working selector still shrinks correctly through the changed request path on this
branch:
-Filter "source.name='pslivetest-fs-121'"returns 2 of 30.Not covered:
Remove-PfbLocalGroupgot no live write coverage in this run. Itsempty-pipeline path is shim-proven at 0 requests above; what is unverified is only that a
real delete still succeeds.
The guard's scope limit — please read this one
The predicate is deliberately narrow, and its narrowness is not obvious from the
"130 guards" figure:
It is an emptiness test standing in for a selector policy that the design never
defines. Any bound non-selector key leaves the query hashtable non-empty and
defeats the guard. So:
The same applies to
-Filter,-Sort,-TotalOnly,-ContextNames,-Destroyedand the
-StartTime/-EndTime/-Resolutiontrio. "130 guards" is coverage of thepopulation of qualifying cmdlets — it is not coverage of every empty-pipeline
invocation. Two independent reviews raised this during development, which is why it
is stated here rather than left for you to derive from the predicate.
This is now tracked as #126, which sets out the sharper version of the problem: the
predicate cannot distinguish a key that addresses objects from one that merely scopes or
shapes the result, so it errs toward issuing the request in every case — correct for
-Filter, wrong for the rest. That issue also records why the current shape makes any fixcheap to adopt: the guard tests the built
$QueryParamsrather than the bound parameters, soa selector policy changes one file and none of the 130 call sites.
Deliberate behaviours and named outliers
@() | Get-PfbXreturns nothing and issues no request. Direct calls are neversuppressed —
Get-PfbXwith no arguments still performs its unfiltered read, asbefore.
@() | Get-PfbBucket -Name 'only-this-one'also returns nothing. The explicit-Nameis not revived.-Nameis the pipeline-bound parameter, so an emptypipeline means
processnever runs and the accumulator the query is built fromstays empty. This is consistent with the predicate but it is a sharp edge worth
knowing about.
inside
Invoke-PfbApiRequest. So an empty pipeline into a cmdlet that is too newfor the connected array, or that is context-gated, now returns empty silently
rather than raising the version error. Reordering that would mean moving the guard
below the request-builder, which defeats it.
Pre-branch,
@() | Get-PfbBucketAccessPolicyissued an unfiltered request, thearray refused it, and the
catchemitted guidance ("Bucket access policies requirethe -Name parameter with a fully-qualified 'bucket/policy' name…"). Post-branch the
guard returns first and the call is silent. The same applies to
Get-PfbBucketAccessPolicyRule,Get-PfbBucketCorsPolicy,Get-PfbBucketCorsPolicyRule,Get-PfbFileSystemStorageClass,Get-PfbRealmStorageClass,Get-PfbResiliencyGroup,Get-PfbLogTargetFileSystem,Get-PfbNode,Get-PfbNodeGroupandGet-PfbNodeGroupUse. All eleven warningslive inside
catchblocks reachable only after a request, so nothing is beingswallowed — but it is a real loud-to-silent conversion and is listed here rather
than left to be discovered.
Get-PfbNodehas one guard, placed so that it dominates both its primary(
nodes) and fallback (blades) calls, rather than one guard per call site.Remove-PfbLocalGroup— the only cmdlet in the population declaringSupportsShouldProcess— returns beforeShouldProcess, so an empty pipelineproduces no confirmation prompt and no request. The guard is on line 39,
ShouldProcesson line 42 and the request on line 43; measured by AST offset as thetie-breaker, 1588 / 1782 / 1853 on the branch head. It is also the only guard in
this branch placed by hand, because the generator routes any
SupportsShouldProcessfunction to
SkippedNeedsHuman. The underlying reason it lands in this population atall is structural and is filed as Remove-PfbLocalGroup is the only write cmdlet issuing its request from an end block #127: of 112
Remove-Pfb*cmdlets, 82 of whichaccept pipeline input, it is the only one issuing its request from an
endblockrather than from
process— so it is also the only mutating cmdlet among the 130(the rest are 127
Get-*and 2Test-*). Nothing here depends on that being changed;Remove-PfbLocalGroup is the only write cmdlet issuing its request from an end block #127 simply records that normalising the shape would remove the need for this guard.
Get-PfbRemoteArray's guard sits above itscurrent_fleet_onlywrite, notimmediately before the request. That write happens on both branches of an
if/else, so a guard in the generator's usual position would never fire andeffective coverage would have been 129 of 130 with every count still reading
green. Placing the guard above the write protects the empty-pipeline case and
changes no request that is still issued —
current_fleet_onlyis a scope flag, nota selector. This makes it the single named entry in the coverage rail's
post-guard-write allowlist (
$allowedPostGuardWrite = @('Get-PfbRemoteArray')),and emptying that allowlist on the unmutated tree names this function and nothing
else, which is how we know the one entry is load-bearing and that no second
function needs one.
Get-PfbNetworkConnectionStatistics,Get-PfbFleetKeyandSet-PfbContextarenot exclusions from the coverage rail. They simply do not satisfy its
qualifying predicate. The exclusion list is explicit and empty.
Tooling and rails
tools/Update-PfbEmptyPipelineGuards.ps1is the AST generator that inserted 127 ofthe guards and now serves as the drift check. It is
#Requires -Version 5.1, which isdeliberately unlike most of its
tools/siblings — 9 of the 11 scripts there are7.0— so that the drift check can run under either edition; the shipped guard lineand predicate are 5.1-clean for the same reason.
On the finished tree it reports a fixed point:
The generator's recognizer only proves a guard exists, not that it is correct — a
guard moved below the request, reduced to a bare call with no
if/return, hoistedabove an
Add-PfbCommonQueryParams -Into $queryParamswrite, nested inside ascriptblock, or pointed at the wrong hashtable would all still report
AlreadyPresent. That gap is closed in CI rather than in the generator, byTests/PfbEmptyPipelineGuardCoverage.Tests.ps1, which asserts placement, hashtableidentity, offset ordering relative to the request, that the guard is not nested inside
a scriptblock expression, and the absence of unconditional post-guard query writes.
Note the precise strength of the nesting assertion: it forbids nesting in a
ScriptBlockExpressionAst, which is the shape where areturnexits the scriptblockinstead of the cmdlet. It does not assert that the guard is reached on every path. A
guard hand-moved inside an
if {}or aforeach {}at the top of anendblock stillreturns from the cmdlet correctly, so it passes this rail and the ordering rail and the
GuardReturnsrail — and simply does not execute when the accumulator is empty, whichis the case the guard exists for. Measured on this branch, all 130 guards share exactly
one parent-chain shape —
CommandAst -> PipelineAst -> IfStatementAst -> end block—and the generator can only ever insert one there (
Get-PfbTopLevelStatementthrowsrather than inserting off-anchor), so the exposure is a hand edit and is empty today.
This is filed as #128 rather than fixed here. Worth noting for whoever takes it: the
fix is not a direct-child assertion, which would be too strict — a
tryorfinallybody always executes, so a guard placed there does still dominate the request. The
property wanted is dominance, which is a control-flow predicate needing its own
non-vacuity proof over 544 functions. Each of the existing assertions was
proven to discriminate by editing the tree and confirming the matching test reds and
names the offending file; guards were then restored with the generator rather than
git restore.Both rails are registered in
Tests/coverage-baseline.psd1'sRequiredDescribes, onboth edition blocks. This matters more here than the skip ceiling does.
MaxSkippedcannot see a
Describethat disappears — a file filtered out of a run, or deletedoutright, contributes neither a skip nor a pass — so before this, breaking a guard was
a red build while deleting the rail that checks the guards was a green one. That is
the issue-#63 failure shape one level down, which is precisely what that list exists
to catch. Neither rail carries a PS7 gate and neither reads the spec cache, and the
generator declares
#Requires -Version 5.1rather than 7.0 specifically so itsreal-tree fixed-point check runs on either leg — so requiring both on Windows
PowerShell 5.1 is not a false red. Verified by adding a deliberately bogus entry and
confirming
Tests/CiCoverageGate.Tests.ps1reds on both editions and names theoffending assertion, then removing it.
CI workflow effect on merge
.github/workflows/update-api-capability-map.ymltriggers onpushtomainwithpaths: ['Public/**']and commits withadd-paths: Data, Reports. This branchtouches 131
Public/files, so merging will fire it and it may open its normalbot PR.
The evidence that it will produce no artifact diff: all four regeneration gates pass,
and each artifact regenerates byte-identical to the committed copy. Regenerated to a
scratch directory outside the repo and compared by SHA-256 on raw bytes:
Reports/PfbFieldCmdletMap.jsonWrote 2003 entries)Reports/PfbApiDriftReport.jsonReports/PfbDeadKeyReport.json85 dead keys from 2164 inventoried parameters)Reports/PfbPipelineSelectorMap.jsonThe three Markdown companions are identical after newline normalisation — the
committed copies are CRLF from checkout and the generated ones are LF, proven by
full-string equality rather than by a diff with no hunks.
Note that the workflow does not build
Reports/PfbPipelineSelectorMap.*; thatmap is hand-run only, which is why it was regenerated manually. Its four pins all
hold, and the pair set was checked by symmetric difference rather than by equal
counts:
The mechanism for why nothing moved is what makes the null result trustworthy: the
probe harness pipes a real producer object into each candidate parameter, so a probed
pair either binds a selector — leaving the query non-empty and failing the
predicate's second condition — or fails to bind, in which case its outcome was
already
Unbindable/BindError/NoSelectorand never a request capture. The guardcannot pre-empt the harness on any probed pair.
The committed dead-key report's floors (2000 parameters inventoried, 1600 keys
evaluated) hold unchanged, and
Update-PfbContextHelpstill reportsChanged.Count = 0against the guard-modified tree.Test evidence
Every run scoped, one file per invocation, under both pwsh 7.x and Windows
PowerShell 5.1 with Pester 6.0.1,
Container okon every run and zero failures:Tests/Invoke-PfbApiRequest.EmptyResult.Tests.ps1Tests/Test-PfbEmptyPipelineRead.Tests.ps1Tests/PfbEmptyPipelineGuardCoverage.Tests.ps1Tests/Update-PfbEmptyPipelineGuards.Tests.ps1Tests/Get-PfbRemoteArray.Tests.ps1(new)Tests/Get-PfbFleetMember.Tests.ps1Tests/Update-PfbContextHelp.Tests.ps1Tests/CommittedDeadKeyReport.Tests.ps1Tests/Build-PfbPipelineSelectorMap.Tests.ps1Tests/Build-PfbFieldCmdletMap.Tests.ps1Tests/Build-PfbDeadKeyReport.Tests.ps1Tests/Build-PfbApiDriftReport.Tests.ps1Tests/Build-PfbCapabilityMap.Tests.ps1Tests/PfbPipelineSelectorRail.Tests.ps1The 5.1 skips are the existing deliberate PS7-only gating on generator-owner test
files, not failures — every one of those runs reports
Container ok, which is thesignal that distinguishes a healthy skip from a file that died before its tests ran.
None of the new test files add a skip on either edition.
Known limits
Stated plainly, because a maintainer should not have to discover these from the
evidence:
UNINFORMATIVE-EMPTYand are not counted as passes. Thosecmdlets return nothing on this lab array whether or not the guard fires, so the
sweep cannot distinguish the two. The request-counting shim covers all 130
regardless; the sweep classification is the weaker of the two pieces of evidence.
UNKNOWNand were preserved as unknowns rather than rounded up:five cmdlets with unbound mandatory parameters, one whose parameter set could not
be resolved (
Get-PfbAsyncLogDownload), and one that met an array-side HTTP 500(
Get-PfbNetworkInterfaceConnectorSettings). Nothing was skipped by policy;model-limited endpoints surfaced as the array's own 400s, which is a lab ceiling
rather than a defect.
itemscount-only classifier is unexercised live, as described above.Describeblocks areadded across the five new test files.
Tests/coverage-baseline.psd1pinsMaxSkippedceilings and aRequiredDescribespresence list and carries nototal-count pin, and the new Describes add zero skips on either edition — so
no ceiling needed raising. That much is an inference from the baseline file
plus the measured zero-skip runs, not a measurement of the aggregate suite; CI
settles it. The
RequiredDescribeslist was changed, deliberately — see below.RequiredDescribesenforcement is only observable in a full-suite run.scripts/Assert-PfbTestCoverage.ps1resolves each entry against the result treeby
Path[0], so the entries added here are proven to name real blocks (byTests/CiCoverageGate.Tests.ps1, locally, both editions) but are proven toresolve to executed tests only by CI.
Residual:
-TotalOnlystill yields the wrapper-TotalOnlyreturns the one-propertytotal_item_countobject by design, and pipingthat object onward still reaches the wire. On FB-A:
This is recorded as an observation, not an improvement claim, and it is not a
clean before/after. On
mainthe piped object carried a hand-builttotal_item_count = 0; on this branch it carried the live value2. The wire stringtherefore differs for a reason that has nothing to do with this branch. Whether
-TotalOnlyshould emit a plain integer, or a typed object, or should refuse to bindonward at all, is a separate design question and is left to you.
Files changed
Private/Invoke-PfbApiRequest.ps1— final-return gate plus the no-itemscount-only classifier narrowing.
Private/Test-PfbEmptyPipelineRead.ps1— new predicate.Public/— one byte-identical guard line each.Public/Replication/Get-PfbFleetMember.ps1— comment-only rationale correction(its
total_onlysentinel passthrough is now unreachable on today's responselayer and is retained as defence-in-depth; the old comment claimed a path that no
longer exists).
tools/Update-PfbEmptyPipelineGuards.ps1— new AST guard generator / drift check.Tests/coverage-baseline.psd1— registers the two guard rails inRequiredDescribeson both edition blocks. No ceiling raised.No version bump, no
CHANGELOG.mdedit, no manifest or export change, noData/,Reports/or.github/change.CI
Green on
f3dca14across all four OS/edition legs plus the spec-cache job(
macos-latestpwsh,ubuntu-latestpwsh,windows-latestpwsh,windows-latestWindows PowerShell 5.1). That run is also what settles the two items this body lists as
locally undeterminable: the aggregate suite passes, and the two new
RequiredDescribesentries resolve to executed tests rather than merely naming real blocks.
🤖 Generated with Claude Code