From 62eca05168cbde04527bdf688dd69451ed7ded82 Mon Sep 17 00:00:00 2001 From: Justin Emerson Date: Tue, 4 Aug 2026 17:02:23 -0700 Subject: [PATCH 1/5] fix(tools): walk request-body schemas at depth 32 and descend array items Two defects in Get-PfbSpecCapabilities' request-body walk, both changing Data/PfbCapabilityMap.json and both runtime-visible through Private/Assert-PfbApiCapability.ps1, which reads bodyProperties. #71 -- the two body-schema helper calls passed no -MaxDepth, so both took the helpers' own default of 8. The fb2.12-2.16 schemas compose through allOf chains deeper than that, so five PATCH /password-policies fields recorded introducedVersion 2.17 instead of 2.16 and would have been refused against a 2.16 array. Get-PfbSpecCapabilities now takes -MaxDepth, defaulting to 32 -- the value Get-PfbSpecResponseShapes already uses, chosen there by measuring this same truncation (184 false removals at 8 versus 7 true ones at 32). #82 -- a request body that is itself `type: array` carries its element schema on the `items` sibling keyword, not as a property, so the walk terminated immediately and four batch endpoints recorded an empty bodyProperties, hiding 23 fields from both the runtime gate and the drift report. The items hop is done at the CALL SITE, not by teaching Add-PfbSchemaPropertyNodes to descend `items`. That walker is shared with Get-PfbSpecResponseShapes, whose contract is that an envelope's properties and its items element's properties stay two separate levels; collapsing them would silently change Data/PfbResponseShapeMap.json and make its cross-version removal detection compare incomparable sets. This mirrors the hop Get-PfbSpecResponseShapes already performs at its own call site. Verified: Data/PfbResponseShapeMap.json regenerates byte-identical. Both regressions are covered by fixtures confirmed to fail before the fix. Artifact regeneration is deliberately NOT in this commit -- it is sequenced behind Fusion Phase 0 per issue #84. Refs #71, #82. Unblocks #44. Co-Authored-By: Claude Opus 5 --- Tests/PfbSpecTools.Tests.ps1 | 185 +++++++++++++++++++++++++++++++++++ tools/lib/PfbSpecTools.ps1 | 40 +++++++- 2 files changed, 222 insertions(+), 3 deletions(-) diff --git a/Tests/PfbSpecTools.Tests.ps1 b/Tests/PfbSpecTools.Tests.ps1 index 2d96c90..aafb410 100644 --- a/Tests/PfbSpecTools.Tests.ps1 +++ b/Tests/PfbSpecTools.Tests.ps1 @@ -725,3 +725,188 @@ Describe 'Get-PfbSpecResponseShapes -MaxDepth (regression: the 184-false-removal $shapes[0].ItemProperties | Should -Not -Contain 'deep_field' } } + +Describe 'Get-PfbSpecCapabilities -MaxDepth (regression: issue #71 body-schema allOf truncation)' { + BeforeAll { + # Same 10-level allOf chain as the response-shape regression above, but reached + # through a REQUEST BODY rather than a response. This mirrors the fb2.12-2.16 + # PATCH /password-policies body, whose allOf chain runs deeper than 8 and whose five + # properties therefore recorded introducedVersion 2.17 instead of 2.16. That is + # runtime-visible, not cosmetic: Private/Assert-PfbApiCapability.ps1 reads + # bodyProperties, so it would refuse those five parameters against a 2.16 array. + # The 2.17 spec restructuring flattened these chains, which is why the defect + # self-heals from 2.17 on and stays invisible against current data. + $schemas = [PSCustomObject]@{} + $schemas | Add-Member -NotePropertyName 'Level9' -NotePropertyValue ([PSCustomObject]@{ + properties = [PSCustomObject]@{ deep_field = [PSCustomObject]@{ type = 'string' } } + }) + foreach ($i in 8..0) { + $schemas | Add-Member -NotePropertyName "Level$i" -NotePropertyValue ([PSCustomObject]@{ + allOf = @([PSCustomObject]@{ '$ref' = "#/components/schemas/Level$($i + 1)" }) + }) + } + $script:deepBodySpec = [PSCustomObject]@{ + components = [PSCustomObject]@{ schemas = $schemas } + paths = [PSCustomObject]@{ + '/api/9.9/deep-body' = [PSCustomObject]@{ + patch = [PSCustomObject]@{ + requestBody = [PSCustomObject]@{ + content = [PSCustomObject]@{ + 'application/json' = [PSCustomObject]@{ + schema = [PSCustomObject]@{ + # 'shallow_field' sits inline alongside the deep allOf chain and is + # reachable at any MaxDepth. It is the positive anchor for the + # depth-8 test below -- see that It's own comment for why a negative + # assertion without one proves nothing. + properties = [PSCustomObject]@{ shallow_field = [PSCustomObject]@{ type = 'string' } } + allOf = @([PSCustomObject]@{ '$ref' = '#/components/schemas/Level0' }) + } + } + } + } + } + } + } + } + } + + It 'finds a body property nested deeper than the old default MaxDepth of 8' { + $caps = @(Get-PfbSpecCapabilities -Spec $script:deepBodySpec) + $caps.Count | Should -Be 1 + $caps[0].BodyProperties | Should -Contain 'shallow_field' + $caps[0].BodyProperties | Should -Contain 'deep_field' + } + + It 'also surfaces the deep property in BodyPropertyDetails, not only in BodyProperties' { + # BodyProperties and BodyPropertyDetails come from two SEPARATE helper calls + # (Get-PfbSchemaPropertyNames and Get-PfbSchemaPropertyDetails), each with its own + # MaxDepth default. Asserting only the former would leave a fix that raises depth on + # one call site and not the other looking green, while ReadOnlyBodyProperties and + # DeprecatedBodyProperties -- both projections of Details -- stayed truncated. + $caps = @(Get-PfbSpecCapabilities -Spec $script:deepBodySpec) + ($caps[0].BodyPropertyDetails | ForEach-Object Name) | Should -Contain 'shallow_field' + ($caps[0].BodyPropertyDetails | ForEach-Object Name) | Should -Contain 'deep_field' + } + + It 'would MISS that property at depth 8 -- proving the constraint is real' { + $caps = @(Get-PfbSpecCapabilities -Spec $script:deepBodySpec -MaxDepth 8) + + # POSITIVE ANCHORS FIRST, for the same reason as the response-shape regression above: + # a negative assertion about a collection is satisfied by every degenerate outcome as + # well as the intended one. If the walk returned nothing, $caps[0] would be $null and + # the -Not -Contain below would pass while testing nothing. These anchors fail on that + # mutant, so reaching the negative proves the walk ran, produced this endpoint's + # record, populated BodyProperties, and that the ONLY casualty at depth 8 is the + # property needing more than 8 levels to reach. + $caps.Count | Should -Be 1 + $caps[0].BodyProperties | Should -Contain 'shallow_field' + + $caps[0].BodyProperties | Should -Not -Contain 'deep_field' + } +} + +Describe 'Get-PfbSpecCapabilities array-bodied requests (regression: issue #82)' { + BeforeAll { + # Mirrors POST /nodes/batch on fb2.18+: the request body schema is ITSELF `type: array`, + # with `items` as a sibling keyword rather than a property. The walker's branches cover + # $ref/allOf/properties/required but not `items`, so such a body resolved to a node with + # no properties and no allOf, the accumulator added nothing, and the endpoint recorded + # "bodyProperties": {} -- hiding every field from the runtime gate and the drift report. + # Unlike #71 this does not self-heal on newer specs. + # + # The element schema is allOf-composed exactly like the real NodePost + # (allOf [Node, {node_key}]), because the existing allOf handling has to run AFTER the + # items hop. A fix that descends into items but stops composing allOf would find + # node_key and miss name/id/status; one that composes allOf but never hops items finds + # nothing at all. Asserting both members catches either half being wrong. + $script:arrayBodySpec = [PSCustomObject]@{ + components = [PSCustomObject]@{ + schemas = [PSCustomObject]@{ + BatchNode = [PSCustomObject]@{ + type = 'object' + properties = [PSCustomObject]@{ + name = [PSCustomObject]@{ type = 'string' } + id = [PSCustomObject]@{ type = 'string'; readOnly = $true } + status = [PSCustomObject]@{ type = 'string'; readOnly = $true } + } + } + BatchNodePost = [PSCustomObject]@{ + allOf = @( + [PSCustomObject]@{ '$ref' = '#/components/schemas/BatchNode' } + [PSCustomObject]@{ + type = 'object' + properties = [PSCustomObject]@{ node_key = [PSCustomObject]@{ type = 'string' } } + } + ) + } + BatchEnvelope = [PSCustomObject]@{ + type = 'array' + items = [PSCustomObject]@{ '$ref' = '#/components/schemas/BatchNodePost' } + } + } + } + paths = [PSCustomObject]@{ + # Inline array body -- the shape all four real endpoints actually use. + '/api/9.9/nodes/batch' = [PSCustomObject]@{ + post = [PSCustomObject]@{ + requestBody = [PSCustomObject]@{ + content = [PSCustomObject]@{ + 'application/json' = [PSCustomObject]@{ + schema = [PSCustomObject]@{ + type = 'array' + items = [PSCustomObject]@{ '$ref' = '#/components/schemas/BatchNodePost' } + } + } + } + } + } + } + # $ref'd array body -- not used by any endpoint today, but the 2.17 restructuring + # is precedent for this arrangement changing between spec versions. Distinguishes + # a fix that resolves the media schema before testing `type` from one that reads + # `.type` off an unresolved $ref node and finds $null. + '/api/9.9/nodes/batch-ref' = [PSCustomObject]@{ + post = [PSCustomObject]@{ + requestBody = [PSCustomObject]@{ + content = [PSCustomObject]@{ + 'application/json' = [PSCustomObject]@{ + schema = [PSCustomObject]@{ '$ref' = '#/components/schemas/BatchEnvelope' } + } + } + } + } + } + } + } + } + + It 'finds the element schema properties when the request body is itself an array' { + $caps = @(Get-PfbSpecCapabilities -Spec $script:arrayBodySpec) + $batch = $caps | Where-Object Path -eq '/nodes/batch' + $batch.BodyProperties | Should -Contain 'node_key' + $batch.BodyProperties | Should -Contain 'name' + $batch.BodyProperties | Should -Contain 'id' + $batch.BodyProperties | Should -Contain 'status' + } + + It 'resolves ReadOnlyBodyProperties for an array-bodied endpoint' { + # ReadOnlyBodyProperties is a projection of BodyPropertyDetails, which comes from a + # SEPARATE helper call than BodyProperties. Asserting it proves the items hop reached + # both call sites, not just the names one. All four real endpoints currently record + # null here, and NodePost carries obviously read-only members, so this is asserted + # rather than assumed. + $caps = @(Get-PfbSpecCapabilities -Spec $script:arrayBodySpec) + $batch = $caps | Where-Object Path -eq '/nodes/batch' + $batch.ReadOnlyBodyProperties | Should -Contain 'id' + $batch.ReadOnlyBodyProperties | Should -Contain 'status' + $batch.ReadOnlyBodyProperties | Should -Not -Contain 'name' + $batch.ReadOnlyBodyProperties | Should -Not -Contain 'node_key' + } + + It 'resolves a $ref''d array body schema before testing it for items' { + $caps = @(Get-PfbSpecCapabilities -Spec $script:arrayBodySpec) + $batch = $caps | Where-Object Path -eq '/nodes/batch-ref' + $batch.BodyProperties | Should -Contain 'node_key' + $batch.BodyProperties | Should -Contain 'name' + } +} diff --git a/tools/lib/PfbSpecTools.ps1 b/tools/lib/PfbSpecTools.ps1 index 8672400..0cabe33 100644 --- a/tools/lib/PfbSpecTools.ps1 +++ b/tools/lib/PfbSpecTools.ps1 @@ -563,7 +563,16 @@ function Get-PfbSpecCapabilities { [CmdletBinding()] param( [Parameter(Mandatory)] - $Spec + $Spec, + + # 32, not the helpers' own default of 8. The fb2.12-2.16 body schemas compose through + # allOf chains deeper than 8, and reading them at 8 silently drops the properties + # below the cut -- which surfaces as an inflated introducedVersion in the capability + # map (issue #71) and, through Private/Assert-PfbApiCapability.ps1, as a runtime + # refusal of parameters the array actually supports. 32 is not a guess: it is the + # value Get-PfbSpecResponseShapes already defaults to, chosen there by measuring the + # same fb2.12-2.16 truncation (184 false removals at 8 versus 7 true ones at 32). + [int]$MaxDepth = 32 ) $results = [System.Collections.Generic.List[object]]::new() @@ -629,8 +638,33 @@ function Get-PfbSpecCapabilities { $mediaKey = if ($mediaTypes -contains 'application/json') { 'application/json' } else { $mediaTypes | Select-Object -First 1 } if ($mediaKey) { $mediaSchema = $op.requestBody.content.$mediaKey.schema - $bodyPropNames = Get-PfbSchemaPropertyNames -Schema $mediaSchema -Spec $Spec - $bodyPropertyDetails = @(Get-PfbSchemaPropertyDetails -Schema $mediaSchema -Spec $Spec) + + # A request body that is itself `type: array` carries its element schema on + # the `items` SIBLING KEYWORD, not as a property, so the property walk has + # nothing to descend and the endpoint records an empty bodyProperties + # (issue #82). Hop items here, at the call site, rather than teaching + # Add-PfbSchemaPropertyNodes to descend `items` itself: that walker is shared + # with Get-PfbSpecResponseShapes, whose contract is that an envelope's + # properties and its items element's properties are two deliberately-separate + # levels. Teaching the walker to descend unconditionally would collapse them, + # silently changing Data/PfbResponseShapeMap.json and making its + # cross-version removal detection compare incomparable sets. This mirrors the + # hop Get-PfbSpecResponseShapes already performs at its own call site. + # + # Resolve before testing `type`: an unresolved $ref node has no `.type`, so + # reading it directly would silently fall through for a $ref'd array body. + # The non-array path deliberately passes the UNRESOLVED $mediaSchema through + # unchanged, so object bodies behave exactly as before. + $resolvedMedia = Resolve-PfbRef -Node $mediaSchema -Spec $Spec + $bodySchema = if ($null -ne $resolvedMedia -and $resolvedMedia.type -eq 'array' -and $resolvedMedia.items) { + $resolvedMedia.items + } + else { + $mediaSchema + } + + $bodyPropNames = Get-PfbSchemaPropertyNames -Schema $bodySchema -Spec $Spec -MaxDepth $MaxDepth + $bodyPropertyDetails = @(Get-PfbSchemaPropertyDetails -Schema $bodySchema -Spec $Spec -MaxDepth $MaxDepth) } } From 02582ab2a634b56a04f26cd8260665060a8291e6 Mon Sep 17 00:00:00 2001 From: Justin Emerson Date: Tue, 4 Aug 2026 17:10:23 -0700 Subject: [PATCH 2/5] chore: stop ignoring tools/ in .gitignore The rule has never had any effect. It was added in 69fe478 -- the same commit that first committed tools/ -- as a hedge while it was undecided whether the toolchain should be tracked ("Not yet decided whether this should be tracked"). Ignore rules do not apply to already-tracked files, so all 14 files under tools/ have been tracked from birth and the rule has been inert ever since. Removing it is a no-op for the working tree and a small improvement going forward: a NEW file added under tools/ now shows up in git status instead of being silently invisible, which is how tooling work here has previously gone missing. tools/specs/ keeps its own separate rule (.gitignore:36) and stays ignored -- verified that all 29 cached spec files remain ignored after this change, and that git surfaces no newly-untracked files. Co-Authored-By: Claude Opus 5 --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 992084a..63c4f28 100644 --- a/.gitignore +++ b/.gitignore @@ -42,9 +42,6 @@ tools/specs/ # into a workspace-relative path so every OS/edition shares one cache location). .psmodules/ -# Not yet decided whether this should be tracked -- excluded for now -tools/ - # OS Thumbs.db Desktop.ini From 36fe6fd7d2c6b94baac8fcda49e7a8547d002217 Mon Sep 17 00:00:00 2001 From: Justin Emerson Date: Tue, 4 Aug 2026 17:20:31 -0700 Subject: [PATCH 3/5] docs(private): correct the array-body note in Assert-PfbApiCapability The comment explaining why array bodies go unchecked is invalidated by the issue #82 map change in this branch, in both of its halves. It stated that every array-bodied endpoint carries "bodyProperties": {}, so a per-element check "could never fire". That is no longer true -- the four batch endpoints now carry real per-element fields. It then predicted that "this loop picks it up for free if a future map representation ever lands." That was wrong even when written: the loop is guarded on $Body being an IDictionary, and an array body arrives as [hashtable[]] (Set-PfbWorkloadTag passes -Tags straight through), so the loop is skipped before the map is consulted. A richer map alone changes nothing. Replaced with what is actually true now: the map records the fields, the type guard is the remaining blocker, and relaxing it is a real behaviour change that can refuse calls which succeed today -- deliberately not smuggled in with a generator fix. Also records that the blast radius is nil today (only Set-PfbWorkloadTag reaches such an endpoint, all its fields are 2.23) and that this stops holding once #44 adds cmdlets for the other three. Comment-only. No behaviour change. Co-Authored-By: Claude Opus 5 --- Private/Assert-PfbApiCapability.ps1 | 35 ++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/Private/Assert-PfbApiCapability.ps1 b/Private/Assert-PfbApiCapability.ps1 index 5045d85..65b7482 100644 --- a/Private/Assert-PfbApiCapability.ps1 +++ b/Private/Assert-PfbApiCapability.ps1 @@ -85,17 +85,30 @@ function Assert-PfbApiCapability { } } - # Dictionary bodies only, and deliberately so. An array body's per-element fields are NOT - # checked, because the capability map records nothing to check them against: - # tools/Build-PfbCapabilityMap.ps1 fills bodyProperties from Get-PfbSchemaPropertyNames, - # whose schema walk resolves $ref and allOf but never descends through an array schema's - # "items". Every array-bodied endpoint in the spec -- PUT /workloads/tags/batch, - # POST /nodes/batch, POST /resource-accesses/batch -- therefore carries - # "bodyProperties": {} in Data/PfbCapabilityMap.json, so a union-of-element-fields check - # would compare every field against an empty map and could never fire. Skipping is an - # honest no-op; the alternative is a gate that only looks like one. Teaching the map to - # record per-element fields is its own change, and this loop picks it up for free if a - # future map representation ever lands. + # Dictionary bodies only. An array body's per-element fields are NOT checked -- but note + # the reason has changed, and the note this comment used to carry was wrong about what + # would happen next. + # + # It previously read that array-bodied endpoints carry "bodyProperties": {} in + # Data/PfbCapabilityMap.json, because Get-PfbSchemaPropertyNames' walk resolves $ref and + # allOf but never descends an array schema's "items" -- so a check would compare every + # field against an empty map and could never fire. That was true, and it concluded that + # "this loop picks it up for free if a future map representation ever lands." + # + # That map representation has now landed (issue #82): Get-PfbSpecCapabilities hops "items" + # at its call site, and PUT /workloads/tags/batch, POST /nodes/batch, + # POST /resource-accesses/batch and POST /fleets/members/batch now carry real per-element + # fields. But it is NOT picked up for free, because of the type guard on the line below: + # an array body arrives as [hashtable[]] (see Set-PfbWorkloadTag, which passes its -Tags + # straight through), and [hashtable[]] is not an IDictionary, so this loop is skipped + # before the map is ever consulted. + # + # Enabling it therefore means relaxing this guard to iterate the elements and union their + # keys -- a deliberate behaviour change that can start refusing calls that succeed today, + # not a no-op. It is left as its own change rather than smuggled in with the map fix. + # Today the blast radius is nil: Set-PfbWorkloadTag is the only cmdlet reaching an + # array-bodied endpoint, and all five of its fields are 2.23 -- but that stops being true + # as soon as #44 adds cmdlets for the other three. # # Endpoint minVersion and query-parameter checks above still run for array-bodied calls, # which is gating those endpoints previously had none of. From 28c34f2050a6024677542fdf32f4c4d1fc64864b Mon Sep 17 00:00:00 2001 From: Justin Emerson Date: Tue, 4 Aug 2026 21:18:19 -0700 Subject: [PATCH 4/5] chore(reports): regenerate the drift report from the unchanged generator Pre-existing correction, isolated deliberately. This commit reverts nothing and fixes nothing in the generator -- it regenerates Reports/ with tools/lib/PfbSpecTools.ps1 held at the Phase-0 base, so the entire diff here is staleness that already existed on main before this branch. Reports/PfbApiDriftReport.json on main was one gap behind the committed capability map, which is why the "nothing vanishes" invariant in Tests/Build-PfbApiDriftReport.Tests.ps1 has been red locally. Root cause is New-PfbFileSystemReplicaLink's [Nullable[bool]]$RemoteDefaultExports being sent through a conditional assignment the drift tracer cannot follow, so confidence degrades high -> partial, enrichment is disabled, and systemicGaps/conventionStrength move 252 -> 246. Not spec staleness: analysedVersions is identical at 2.28. The diff here (271/19/14/5 lines across four files) is numerically identical to what origin/automated/update-api-capability-map already holds -- that workflow has failed at its "Open pull request" step on every run since 2026-07-24, so main never received this regeneration. Splitting it out means the next commit's diff contains only what this PR's code change actually causes. --- Reports/PfbApiDriftReport.json | 271 +++++-------------------------- Reports/PfbApiDriftReport.md | 19 ++- Reports/PfbFieldCmdletMap.json | 14 +- Reports/PfbFieldCmdletMapping.md | 5 +- 4 files changed, 58 insertions(+), 251 deletions(-) diff --git a/Reports/PfbApiDriftReport.json b/Reports/PfbApiDriftReport.json index cf99091..1965656 100644 --- a/Reports/PfbApiDriftReport.json +++ b/Reports/PfbApiDriftReport.json @@ -10548,114 +10548,16 @@ "context_names", "ids", "local_file_system_ids", + "remote_default_exports", "remote_ids" ], "missingBodyProperties": [ - { - "name": "direction", - "type": null, - "format": null, - "specRequired": false, - "synopsis": null, - "suggestedPowerShellType": "[object]", - "enumValues": [ - "inbound", - "outbound" - ], - "enumStatus": "matched", - "target": { - "file": "Public/Replication/New-PfbFileSystemReplicaLink.ps1", - "paramBlockLine": 55, - "payloadVariable": "body", - "assignmentStyle": "unknown", - "hasAttributes": false - } - }, - { - "name": "link_type", - "type": "string", - "format": null, - "specRequired": false, - "synopsis": "Type of the replica link.", - "suggestedPowerShellType": "[string]", - "enumValues": [], - "enumStatus": "no-spec-enum-found", - "target": { - "file": "Public/Replication/New-PfbFileSystemReplicaLink.ps1", - "paramBlockLine": 55, - "payloadVariable": "body", - "assignmentStyle": "unknown", - "hasAttributes": false - } - }, - { - "name": "local_file_system", - "type": null, - "format": null, - "specRequired": false, - "synopsis": "Reference to a local file system.", - "suggestedPowerShellType": "[object]", - "enumValues": [], - "enumStatus": "no-spec-enum-found", - "target": { - "file": "Public/Replication/New-PfbFileSystemReplicaLink.ps1", - "paramBlockLine": 55, - "payloadVariable": "body", - "assignmentStyle": "unknown", - "hasAttributes": false - } - }, - { - "name": "policies", - "type": "array", - "format": null, - "specRequired": false, - "synopsis": null, - "suggestedPowerShellType": "[object[]]", - "enumValues": [], - "enumStatus": "no-spec-enum-found", - "target": { - "file": "Public/Replication/New-PfbFileSystemReplicaLink.ps1", - "paramBlockLine": 55, - "payloadVariable": "body", - "assignmentStyle": "unknown", - "hasAttributes": false - } - }, - { - "name": "remote", - "type": null, - "format": null, - "specRequired": false, - "synopsis": "Reference to a remote array or realm.", - "suggestedPowerShellType": "[object]", - "enumValues": [], - "enumStatus": "no-spec-enum-found", - "target": { - "file": "Public/Replication/New-PfbFileSystemReplicaLink.ps1", - "paramBlockLine": 55, - "payloadVariable": "body", - "assignmentStyle": "unknown", - "hasAttributes": false - } - }, - { - "name": "remote_file_system", - "type": null, - "format": null, - "specRequired": false, - "synopsis": "Reference to a remote file system.", - "suggestedPowerShellType": "[object]", - "enumValues": [], - "enumStatus": "no-spec-enum-found", - "target": { - "file": "Public/Replication/New-PfbFileSystemReplicaLink.ps1", - "paramBlockLine": 55, - "payloadVariable": "body", - "assignmentStyle": "unknown", - "hasAttributes": false - } - } + "direction", + "link_type", + "local_file_system", + "policies", + "remote", + "remote_file_system" ], "readOnlyFields": [ "context", @@ -10666,10 +10568,17 @@ "status_details" ], "confidence": { - "level": "high", - "unresolvedParameters": [], + "level": "partial", + "unresolvedParameters": [ + { + "parameter": "RemoteDefaultExports", + "surface": "TypedUnresolved", + "file": "Public/Replication/New-PfbFileSystemReplicaLink.ps1", + "line": 57 + } + ], "escapeHatchOnly": [], - "caveat": "" + "caveat": "one or more parameters could not be traced to a wire name and have no -Attributes escape hatch; lists reflect typed-parameter coverage only, not full wire reachability" }, "annotations": [] }, @@ -14170,8 +14079,8 @@ "systemicGaps": [ { "name": "context_names", - "endpointCount": 270, - "queryEndpointCount": 270, + "endpointCount": 269, + "queryEndpointCount": 269, "bodyEndpointCount": 0, "endpoints": [ "DELETE /admins/api-tokens", @@ -14404,7 +14313,6 @@ "POST /directory-services/local/directory-services", "POST /directory-services/local/groups", "POST /file-system-exports", - "POST /file-system-replica-links", "POST /file-system-replica-links/policies", "POST /file-systems/audit-policies", "POST /file-systems/locks/nlm-reclamations", @@ -14592,8 +14500,8 @@ }, { "name": "ids", - "endpointCount": 43, - "queryEndpointCount": 43, + "endpointCount": 42, + "queryEndpointCount": 42, "bodyEndpointCount": 0, "endpoints": [ "DELETE /network-access-policies/rules", @@ -14637,8 +14545,7 @@ "PATCH /password-policies", "PATCH /smb-client-policies/rules", "PATCH /smb-share-policies/rules", - "PATCH /syslog-servers/settings", - "POST /file-system-replica-links" + "PATCH /syslog-servers/settings" ], "annotations": [] }, @@ -14848,8 +14755,8 @@ }, { "name": "remote_ids", - "endpointCount": 15, - "queryEndpointCount": 15, + "endpointCount": 14, + "queryEndpointCount": 14, "bodyEndpointCount": 0, "endpoints": [ "DELETE /array-connections", @@ -14865,8 +14772,7 @@ "GET /file-system-replica-links/policies", "GET /file-system-replica-links/transfer", "GET /policies-all/members", - "GET /policies/file-system-replica-links", - "POST /file-system-replica-links" + "GET /policies/file-system-replica-links" ], "annotations": [] }, @@ -14926,23 +14832,6 @@ ], "annotations": [] }, - { - "name": "local_file_system_ids", - "endpointCount": 8, - "queryEndpointCount": 8, - "bodyEndpointCount": 0, - "endpoints": [ - "DELETE /file-system-replica-links", - "DELETE /file-system-replica-links/policies", - "DELETE /policies/file-system-replica-links", - "GET /file-system-replica-links", - "GET /file-system-replica-links/policies", - "GET /policies-all/members", - "GET /policies/file-system-replica-links", - "POST /file-system-replica-links" - ], - "annotations": [] - }, { "name": "actions", "endpointCount": 7, @@ -14975,6 +14864,22 @@ ], "annotations": [] }, + { + "name": "local_file_system_ids", + "endpointCount": 7, + "queryEndpointCount": 7, + "bodyEndpointCount": 0, + "endpoints": [ + "DELETE /file-system-replica-links", + "DELETE /file-system-replica-links/policies", + "DELETE /policies/file-system-replica-links", + "GET /file-system-replica-links", + "GET /file-system-replica-links/policies", + "GET /policies-all/members", + "GET /policies/file-system-replica-links" + ], + "annotations": [] + }, { "name": "name", "endpointCount": 7, @@ -16409,16 +16314,6 @@ ], "annotations": [] }, - { - "name": "direction", - "endpointCount": 1, - "queryEndpointCount": 0, - "bodyEndpointCount": 1, - "endpoints": [ - "POST /file-system-replica-links" - ], - "annotations": [] - }, { "name": "discover_mtu", "endpointCount": 1, @@ -16759,16 +16654,6 @@ ], "annotations": [] }, - { - "name": "link_type", - "endpointCount": 1, - "queryEndpointCount": 0, - "bodyEndpointCount": 1, - "endpoints": [ - "POST /file-system-replica-links" - ], - "annotations": [] - }, { "name": "local_bucket_ids", "endpointCount": 1, @@ -16779,16 +16664,6 @@ ], "annotations": [] }, - { - "name": "local_file_system", - "endpointCount": 1, - "queryEndpointCount": 0, - "bodyEndpointCount": 1, - "endpoints": [ - "POST /file-system-replica-links" - ], - "annotations": [] - }, { "name": "local_host", "endpointCount": 1, @@ -17049,16 +16924,6 @@ ], "annotations": [] }, - { - "name": "policies", - "endpointCount": 1, - "queryEndpointCount": 0, - "bodyEndpointCount": 1, - "endpoints": [ - "POST /file-system-replica-links" - ], - "annotations": [] - }, { "name": "port", "endpointCount": 1, @@ -17229,16 +17094,6 @@ ], "annotations": [] }, - { - "name": "remote", - "endpointCount": 1, - "queryEndpointCount": 0, - "bodyEndpointCount": 1, - "endpoints": [ - "POST /file-system-replica-links" - ], - "annotations": [] - }, { "name": "remote_assist_active", "endpointCount": 1, @@ -17259,16 +17114,6 @@ ], "annotations": [] }, - { - "name": "remote_file_system", - "endpointCount": 1, - "queryEndpointCount": 0, - "bodyEndpointCount": 1, - "endpoints": [ - "POST /file-system-replica-links" - ], - "annotations": [] - }, { "name": "remote_host", "endpointCount": 1, @@ -19519,15 +19364,6 @@ "Update-PfbSubnet" ] }, - { - "name": "remote", - "cmdletCount": 3, - "cmdlets": [ - "New-PfbArrayConnection", - "Update-PfbArrayConnection", - "Update-PfbObjectStoreRemoteCredential" - ] - }, { "name": "remote_file_system_names", "cmdletCount": 3, @@ -20335,11 +20171,6 @@ "cmdletCount": 0, "cmdlets": [] }, - { - "name": "direction", - "cmdletCount": 0, - "cmdlets": [] - }, { "name": "directory_configurations", "cmdletCount": 0, @@ -20470,11 +20301,6 @@ "cmdletCount": 0, "cmdlets": [] }, - { - "name": "link_type", - "cmdletCount": 0, - "cmdlets": [] - }, { "name": "local_bucket_ids", "cmdletCount": 0, @@ -20485,11 +20311,6 @@ "cmdletCount": 0, "cmdlets": [] }, - { - "name": "local_file_system", - "cmdletCount": 0, - "cmdlets": [] - }, { "name": "local_host", "cmdletCount": 0, @@ -20610,11 +20431,6 @@ "cmdletCount": 0, "cmdlets": [] }, - { - "name": "policies", - "cmdletCount": 0, - "cmdlets": [] - }, { "name": "port", "cmdletCount": 0, @@ -20670,11 +20486,6 @@ "cmdletCount": 0, "cmdlets": [] }, - { - "name": "remote_file_system", - "cmdletCount": 0, - "cmdlets": [] - }, { "name": "remote_file_system_ids", "cmdletCount": 0, diff --git a/Reports/PfbApiDriftReport.md b/Reports/PfbApiDriftReport.md index ca94f04..5a6ecd6 100644 --- a/Reports/PfbApiDriftReport.md +++ b/Reports/PfbApiDriftReport.md @@ -23,11 +23,11 @@ This report accepts **false positives in order to eliminate false negatives**. A - Uncovered endpoints: 96 - Endpoints with parameter gaps: 438 - Missing body properties (addable): 422 -- Missing query parameters (addable): 952 +- Missing query parameters (addable): 953 - Read-only body fields (not addable -- see the Read-only fields section below): 382 - Phantom fields silently excluded (accumulated in the capability map, absent from the newest analysed spec): 40 -- Partial-confidence endpoints (see `How to read this report` above, and each row's marker in the Parameter gaps table): 59 -- Systemic gaps (distinct field names collapsed across high-confidence endpoints, detailed below): 252 +- Partial-confidence endpoints (see `How to read this report` above, and each row's marker in the Parameter gaps table): 60 +- Systemic gaps (distinct field names collapsed across high-confidence endpoints, detailed below): 246 - ValidateSet drift: 0 - New ValidateSet candidates: 1 - Context cardinality signal disagreements (fb2.28): 9 @@ -39,13 +39,13 @@ This report accepts **false positives in order to eliminate false negatives**. A One finding per distinct wire field name, collapsed across every endpoint where a high-confidence gap exists (decision 7) -- turns hundreds of per-endpoint rows into a handful of real, actionable decisions. "Cmdlets already using this name" is decision 8's convention-strength ranking: a high count means closing the remaining gaps for this name is a mechanical batch fix; zero means no established convention exists to extend at all -- closing it is an architectural decision, not a mechanical one. -Showing the top 25 of 252 findings by endpoint count -- the full list is in the JSON manifest's `systemicGaps`, nothing is dropped there. +Showing the top 25 of 246 findings by endpoint count -- the full list is in the JSON manifest's `systemicGaps`, nothing is dropped there. | Field name | Endpoints | Query | Body | Cmdlets already using this name | Annotation | |---|---|---|---|---|---| -| `context_names` | 270 | 270 | 0 | 0 | not yet implemented | +| `context_names` | 269 | 269 | 0 | 0 | not yet implemented | | `allow_errors` | 118 | 118 | 0 | 0 | not yet implemented | -| `ids` | 43 | 43 | 0 | 223 | | +| `ids` | 42 | 42 | 0 | 223 | | | `names` | 31 | 31 | 0 | 308 | | | `sort` | 28 | 28 | 0 | 180 | | | `bucket_ids` | 17 | 17 | 0 | 2 | | @@ -53,13 +53,13 @@ Showing the top 25 of 252 findings by endpoint count -- the full list is in the | `total_only` | 17 | 17 | 0 | 39 | | | `bucket_names` | 16 | 16 | 0 | 3 | | | `member_ids` | 15 | 15 | 0 | 85 | | -| `remote_ids` | 15 | 15 | 0 | 3 | | +| `remote_ids` | 14 | 14 | 0 | 3 | | | `policy_names` | 11 | 11 | 0 | 118 | | | `file_system_ids` | 9 | 9 | 0 | 9 | | | `remote_names` | 9 | 9 | 0 | 9 | | -| `local_file_system_ids` | 8 | 8 | 0 | 2 | | | `actions` | 7 | 0 | 7 | 2 | | | `limit` | 7 | 7 | 0 | 199 | | +| `local_file_system_ids` | 7 | 7 | 0 | 2 | | | `name` | 7 | 0 | 7 | 20 | | | `versions` | 7 | 7 | 0 | 5 | | | `enabled` | 6 | 0 | 6 | 10 | | @@ -437,7 +437,7 @@ Endpoints an existing cmdlet already calls, where the capability map knows of a | `POST /directory-services/roles` | New-PfbDirectoryServiceRole | | group, group_base, management_access_policies, role | `high` | | | `POST /dns` | New-PfbDns | context_names, names | ca_certificate, ca_certificate_group, domain, nameservers, services, sources | `partial` -- /!\ 1 unresolved param (see Partial-confidence detail below) | | | `POST /file-system-exports` | New-PfbFileSystemExport | context_names, member_ids, policy_ids | | `high` | | -| `POST /file-system-replica-links` | New-PfbFileSystemReplicaLink | context_names, ids, local_file_system_ids, remote_ids | direction, link_type, local_file_system, policies, remote, remote_file_system | `high` | | +| `POST /file-system-replica-links` | New-PfbFileSystemReplicaLink | context_names, ids, local_file_system_ids, remote_default_exports, remote_ids | direction, link_type, local_file_system, policies, remote, remote_file_system | `partial` -- /!\ 1 unresolved param (see Partial-confidence detail below) | | | `POST /file-system-replica-links/policies` | New-PfbFileSystemReplicaLinkPolicy | context_names | | `high` | | | `POST /file-system-snapshots` | New-PfbFileSystemSnapshot | context_names, source_ids, source_names | | `partial` -- /!\ 1 unresolved param (see Partial-confidence detail below) | | | `POST /file-systems` | New-PfbFileSystem | context_names, default_exports, discard_non_snapshotted_data, include_snapshot, overwrite, policy_ids, policy_names | fast_remove_directory_enabled, hard_limit_enabled, http, multi_protocol, nfs, node_group, smb, snapshot_directory_enabled, workload, writable | `partial` -- /!\ 15 unresolved params (see Partial-confidence detail below) | | @@ -572,6 +572,7 @@ Per the decision-6 procedure above: open each parameter at its `file:line` and f | `POST /data-eviction-policies` | `-Disabled` | TypedUnresolved | `Public/DataEviction/New-PfbDataEvictionPolicy.ps1:31` | one or more parameters could not be traced to a wire name and have no -Attributes escape hatch; lists reflect typed-parameter coverage only, not full wire reachability | | `POST /directory-services/local/groups/members` | `-Member` | TypedUnresolved | `Public/DirectoryService/New-PfbLocalGroupMember.ps1:35` | one or more parameters could not be traced to a wire name and have no -Attributes escape hatch; lists reflect typed-parameter coverage only, not full wire reachability | | `POST /dns` | `-Name` | AttributesOnly | `Public/Network/New-PfbDns.ps1:29` | body reachable only via -Attributes; lists reflect typed-parameter coverage, not wire reachability | +| `POST /file-system-replica-links` | `-RemoteDefaultExports` | TypedUnresolved | `Public/Replication/New-PfbFileSystemReplicaLink.ps1:57` | one or more parameters could not be traced to a wire name and have no -Attributes escape hatch; lists reflect typed-parameter coverage only, not full wire reachability | | `POST /file-system-snapshots` | `-SourceName` | TypedUnresolved | `Public/FileSystemSnapshot/New-PfbFileSystemSnapshot.ps1:36` | one or more parameters could not be traced to a wire name and have no -Attributes escape hatch; lists reflect typed-parameter coverage only, not full wire reachability | | `POST /file-systems` | `-FastRemoveDirectoryEnabled` | AttributesOnly | `Public/FileSystem/New-PfbFileSystem.ps1:139` | body reachable only via -Attributes; lists reflect typed-parameter coverage, not wire reachability | | `POST /file-systems` | `-HardLimit` | AttributesOnly | `Public/FileSystem/New-PfbFileSystem.ps1:93` | body reachable only via -Attributes; lists reflect typed-parameter coverage, not wire reachability | diff --git a/Reports/PfbFieldCmdletMap.json b/Reports/PfbFieldCmdletMap.json index df3fce1..b5587d7 100644 --- a/Reports/PfbFieldCmdletMap.json +++ b/Reports/PfbFieldCmdletMap.json @@ -12157,16 +12157,6 @@ "stableSinceOldestVersion": null, "recommendation": null }, - { - "cmdlet": "New-PfbFileSystemReplicaLink", - "parameter": "RemoteDefaultExports", - "wireName": "remote_default_exports", - "status": "no-spec-enum-found", - "matchedKey": null, - "specValues": null, - "stableSinceOldestVersion": null, - "recommendation": null - }, { "cmdlet": "New-PfbFileSystemReplicaLink", "parameter": "RemoteFileSystemName", @@ -20517,6 +20507,10 @@ "cmdlet": "New-PfbDataEvictionPolicy", "parameter": "Disabled" }, + { + "cmdlet": "New-PfbFileSystemReplicaLink", + "parameter": "RemoteDefaultExports" + }, { "cmdlet": "New-PfbFileSystemSnapshot", "parameter": "SourceName" diff --git a/Reports/PfbFieldCmdletMapping.md b/Reports/PfbFieldCmdletMapping.md index 86be9c2..e9277cc 100644 --- a/Reports/PfbFieldCmdletMapping.md +++ b/Reports/PfbFieldCmdletMapping.md @@ -9,7 +9,7 @@ Reporting only -- no `Public/` cmdlet is edited by this script. Every `matched` - matched: 1 - collision: 1 - not-found-in-resource: 29 -- no-spec-enum-found: 1981 +- no-spec-enum-found: 1980 | Cmdlet | Parameter | Wire name | Status | Spec values | Recommendation | |---|---|---|---|---|---| @@ -116,7 +116,7 @@ Reporting only -- no `Public/` cmdlet is edited by this script. Every `matched` - `Update-PfbSmbSharePolicy -Enabled` - `Update-PfbUserGroupQuotaPolicy -Enabled` -## Typed but unresolved wire name (needs manual inspection): 39 +## Typed but unresolved wire name (needs manual inspection): 40 - `Connect-PfbArray -ApiToken` - `Connect-PfbArray -ApiVersion` @@ -139,6 +139,7 @@ Reporting only -- no `Public/` cmdlet is edited by this script. Every `matched` - `Get-PfbUserGroupQuotaPolicy -Id` - `Get-PfbUserGroupQuotaPolicy -Name` - `New-PfbDataEvictionPolicy -Disabled` +- `New-PfbFileSystemReplicaLink -RemoteDefaultExports` - `New-PfbFileSystemSnapshot -SourceName` - `New-PfbLocalGroupMember -Member` - `New-PfbWorkloadPlacementRecommendation -Inputs` From e0d9a2be2751294742691e8038ad523ab49b240b Mon Sep 17 00:00:00 2001 From: Justin Emerson Date: Tue, 4 Aug 2026 21:24:57 -0700 Subject: [PATCH 5/5] chore(data): regenerate the capability map and drift report for #71 and #82 Everything in this diff is caused by the walker fix in the previous commits. Verified by construction: reverting tools/lib/PfbSpecTools.ps1 to the base and regenerating reproduces Data/PfbCapabilityMap.json byte-identical to the committed Phase-0 artifact, so nothing here is drift or nondeterminism. Capability map -- exactly 7 endpoint records change, in four groups: #71 5 x introducedVersion 2.17 -> 2.16 on PATCH /password-policies (name, id, enabled, is_local, location) #82 23 bodyProperties recovered across the 4 array-bodied endpoints: POST /nodes/batch +12, POST /resource-accesses/batch +2, PUT /workloads/tags/batch +5, POST /fleets/members/batch +4 #82 readOnlyBodyProperties on POST /nodes/batch resolves from empty to 8: capacity, chassis_serial_number, data_addresses, details, id, raw_capacity, status, unique --- 2 order-only records, no value changes: PATCH /ssh-certificate-authority-policies (bodyProperties key order) PATCH /file-systems (top-level key order; contextScope and readOnlyBodyProperties swap position) Note the four batch endpoints did not have an EMPTY bodyProperties before -- each had exactly one entry keyed by the empty string, the nameless artifact of a walk that reached the array node and could not descend it. That is what the 23 real fields replace. The map is deterministic: three consecutive regenerations produced identical SHA-256. Data/PfbResponseShapeMap.json is byte-identical (423de668a78356bad13b6d345bf9e834eca3438e7badf96577e78a5bbf93fef6). This is the guard that the fix stayed at the Get-PfbSpecCapabilities call site and did not leak into the shared Add-PfbSchemaPropertyNodes walker, whose contract is that an envelope and its items element are two separate levels. Phase 0 is undisturbed: schemaVersion still 2, 632 endpoints, and the full contextScope scope-x-provenance cross-tab is unchanged at array/default 604, array/declared 1, array/live-tested 1, fleet/declared 4, fleet/live-tested 3, unknown/unknown 19. Drift report: PUT /workloads/tags/batch gains 5 missingBodyProperties (copyable, key, namespace, resource, value); addable body properties 422 -> 427. Only this one of the four batch endpoints appears, because Set-PfbWorkloadTag is the only existing cmdlet calling any of them -- the other three are #44's scope. Reports/PfbFieldCmdletMap.json, Reports/PfbValueEnumMap.json and their .md siblings do not move under this change. --- Data/PfbCapabilityMap.json | 69 ++++++++++++++++++++++++++-------- Reports/PfbApiDriftReport.json | 8 +++- Reports/PfbApiDriftReport.md | 4 +- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/Data/PfbCapabilityMap.json b/Data/PfbCapabilityMap.json index 4822063..72c0c52 100644 --- a/Data/PfbCapabilityMap.json +++ b/Data/PfbCapabilityMap.json @@ -2146,16 +2146,16 @@ "quiesce": "2.28", "skip_quiesce": "2.28" }, - "contextScope": { - "scope": "array", - "provenance": "default" - }, "readOnlyBodyProperties": [ "created", "id", "promotion_status", "time_remaining" ], + "contextScope": { + "scope": "array", + "provenance": "default" + }, "parameterComponentOverrides": { "ignore_usage": "Ignore_usage" } @@ -7060,14 +7060,14 @@ "names": "2.14" }, "bodyProperties": { - "policy_type": "2.14", "name": "2.14", "id": "2.14", - "signing_authority": "2.14", - "static_authorized_principals": "2.14", "enabled": "2.14", "is_local": "2.14", "location": "2.14", + "policy_type": "2.14", + "signing_authority": "2.14", + "static_authorized_principals": "2.14", "realms": "2.19", "context": "2.24" }, @@ -7731,6 +7731,11 @@ "names": "2.16" }, "bodyProperties": { + "name": "2.16", + "id": "2.16", + "enabled": "2.16", + "is_local": "2.16", + "location": "2.16", "policy_type": "2.16", "lockout_duration": "2.16", "max_login_attempts": "2.16", @@ -7741,11 +7746,6 @@ "min_characters_per_group": "2.16", "enforce_username_check": "2.16", "enforce_dictionary_check": "2.16", - "name": "2.17", - "id": "2.17", - "location": "2.17", - "is_local": "2.17", - "enabled": "2.17", "max_password_age": "2.18", "realms": "2.19" }, @@ -9392,7 +9392,30 @@ "X-Request-ID": "2.18", "add_to_groups": "2.23" }, - "bodyProperties": {}, + "bodyProperties": { + "id": "2.18", + "name": "2.18", + "capacity": "2.18", + "data_addresses": "2.18", + "details": "2.18", + "management_address": "2.18", + "raw_capacity": "2.18", + "serial_number": "2.18", + "status": "2.18", + "unique": "2.18", + "chassis_serial_number": "2.23", + "node_key": "2.27" + }, + "readOnlyBodyProperties": [ + "capacity", + "chassis_serial_number", + "data_addresses", + "details", + "id", + "raw_capacity", + "status", + "unique" + ], "contextScope": { "scope": "array", "provenance": "default" @@ -10164,7 +10187,10 @@ "parameters": { "X-Request-ID": "2.19" }, - "bodyProperties": {}, + "bodyProperties": { + "resource": "2.19", + "scope": "2.19" + }, "contextScope": { "scope": "array", "provenance": "default" @@ -11862,7 +11888,13 @@ "resource_ids": "2.23", "resource_names": "2.23" }, - "bodyProperties": {}, + "bodyProperties": { + "copyable": "2.23", + "key": "2.23", + "namespace": "2.23", + "resource": "2.23", + "value": "2.23" + }, "contextScope": { "scope": "array", "provenance": "default" @@ -13539,7 +13571,12 @@ "fleet_names": "2.27", "validate_target_certificates": "2.27" }, - "bodyProperties": {}, + "bodyProperties": { + "authentication_credentials": "2.27", + "ca_certificate": "2.27", + "ca_certificate_group": "2.27", + "management_address": "2.27" + }, "contextScope": { "scope": "array", "provenance": "default" diff --git a/Reports/PfbApiDriftReport.json b/Reports/PfbApiDriftReport.json index 1965656..787c823 100644 --- a/Reports/PfbApiDriftReport.json +++ b/Reports/PfbApiDriftReport.json @@ -14058,7 +14058,13 @@ "missingQueryParameters": [ "context_names" ], - "missingBodyProperties": [], + "missingBodyProperties": [ + "copyable", + "key", + "namespace", + "resource", + "value" + ], "readOnlyFields": [], "confidence": { "level": "partial", diff --git a/Reports/PfbApiDriftReport.md b/Reports/PfbApiDriftReport.md index 5a6ecd6..1360edd 100644 --- a/Reports/PfbApiDriftReport.md +++ b/Reports/PfbApiDriftReport.md @@ -22,7 +22,7 @@ This report accepts **false positives in order to eliminate false negatives**. A - Uncovered endpoints: 96 - Endpoints with parameter gaps: 438 -- Missing body properties (addable): 422 +- Missing body properties (addable): 427 - Missing query parameters (addable): 953 - Read-only body fields (not addable -- see the Read-only fields section below): 382 - Phantom fields silently excluded (accumulated in the capability map, absent from the newest analysed spec): 40 @@ -512,7 +512,7 @@ Endpoints an existing cmdlet already calls, where the capability map knows of a | `POST /workloads/placement-recommendations` | New-PfbWorkloadPlacementRecommendation | context_names | additional_constraints, parameters, preset, projection_months, recommendation_engine, results_limit | `partial` -- /!\ 1 unresolved param (see Partial-confidence detail below) | | | `POST /worm-data-policies` | New-PfbWormPolicy | context_names, names | default_retention, enabled, location, max_retention, min_retention, mode, retention_lock | `partial` -- /!\ 1 unresolved param (see Partial-confidence detail below) | | | `PUT /presets/workload` | Set-PfbPresetWorkload | context_names | description, directory_configurations, export_configurations, name, parameters, periodic_replication_configurations, placement_configurations, platform_features, qos_configurations, quota_configurations, snapshot_configurations, volume_configurations, workload_tags, workload_type | `high` | | -| `PUT /workloads/tags/batch` | Set-PfbWorkloadTag | context_names | | `partial` -- /!\ 1 unresolved param (see Partial-confidence detail below) | | +| `PUT /workloads/tags/batch` | Set-PfbWorkloadTag | context_names | copyable, key, namespace, resource, value | `partial` -- /!\ 1 unresolved param (see Partial-confidence detail below) | | ### Partial-confidence detail