Skip to content

Add New-PfbDriftIssue.ps1: reconcile drift findings with GitHub issues (dry run by default) - #158

Merged
juemerson-at-purestorage merged 16 commits into
dmann000:mainfrom
juemerson-at-purestorage:feat/drift-issue-reconciler
Sep 25, 2026
Merged

juemerson-at-purestorage merged 16 commits into
dmann000:mainfrom
juemerson-at-purestorage:feat/drift-issue-reconciler

Conversation

@juemerson-at-purestorage

Copy link
Copy Markdown
Collaborator

Summary

Adds tools/New-PfbDriftIssue.ps1, which turns the findings in Reports/PfbApiDriftReport.json and Reports/PfbDeadKeyReport.json into GitHub issues and keeps them in step with the reports. It is a reconciler: issues are its only state (a machine block of HTML comments at the end of each drift issue), a re-run over unchanged inputs plans nothing, and it is a dry run unless given -Apply. It never closes an issue.

  • One finding per atomic gap, fingerprinted as sha256("<category>|<METHOD /path>|<field>") (16 hex, no version component). A report row missing any member of that tuple stops the run rather than producing a colliding fingerprint.
  • One issue per group: family:, systemic: (a parameter missing in 3+ families), envelope:, validateset:, deadkey:, and reopen:<N> for findings still reported after #N closed.
  • Skips findings in issues closed as not planned, and findings a docs/settled/ entry names in a new optional **Drift keys:** field.
  • A machine block counts only on an issue labelled source:drift, which only collaborators can apply; a block on anyone else's issue is ignored and named in a warning, so it can neither suppress nor capture findings, nor stop a run. Because an issue's author can always edit and close it, source:drift belongs only on collaborator-authored issues (documented in docs/TRIAGE-ROLES.md).
  • A finding that stops being reported is noted once; an issue left with none is labelled status:resolved-upstream in place of its status label, and loses it again when any finding is still reported (documented in docs/TRIAGE-ROLES.md).
  • Aborts before writing if more than 25% of recorded findings would vanish in one run.
  • New issues are capped per run (-MaxCreate, default 10), most severe first, labelled source:drift, status:triage, needs:live-test and one area:; every body and comment says automation wrote it.
  • .github/workflows/drift-issues.yml: workflow_dispatch only, apply defaults to false, built-in token with issues: write, no secrets.

Adoption -- pairing existing issues with their findings and provisioning the status:resolved-upstream label -- happens after merge and before the first -Apply.

Testing

  • Tests/PfbDriftIssueTools.Tests.ps1 (160) and Tests/New-PfbDriftIssue.Tests.ps1 (14), plus the approved-verb, import-guard and agent-brief guard files: pwsh 7 218 passed, 0 failed, 0 skipped; Windows PowerShell 5.1 218 passed, 0 failed, 0 skipped.
  • The script runs against a fake gh in tests: a dry run makes zero write calls, and -Apply makes them (the control).
  • Live read-only dry run against this repository: 1143 findings, 10 creates planned (all deadkey: groups), 138 queued, and exactly two gh calls (issue list, label list) on each edition. The 66 issue bodies containing non-ASCII text decoded cleanly.

Live verification

The diff leaves the module source and the manifest entirely untouched, so nothing here can alter a request the module sends or a response it parses. -- the diff is tools/, Tests/, a workflow and docs only; no line in Public/, Private/, the manifest or the root module changed, so the usual live-FlashBlade verification does not apply.

No version bump and no CHANGELOG change.

🤖 Generated with Claude Code

First piece of the drift -> issue reconciler (G2): endpoint normalisation,
endpoint family, ordinal sorting and sha256('<category>|<METHOD /path>|<field>')
fingerprints, pinned by golden values because they are stamped into issues.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One finding per gap, one fingerprint each. Reads both the string and the
object shape of missingQueryParameters and missingBodyProperties, refuses a
field that is a stringified object, leaves out readOnlyFields and the
systemicGaps aggregate, normalises the dead-key report's split method and
path, and stops on a missing category rather than reading it as zero findings.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
family:<segment> by default; systemic:<param> once a missing parameter spans
three or more families (query and body together), which removes it from the
family groups; envelope:, validateset: and deadkey: for the other kinds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@
The block names an issue's group (or marks it paired) and lists its active and
vanished fingerprints. It counts only on an issue labelled source:drift, which
only collaborators can apply; anywhere else it is ignored and never throws. On a
trusted issue parsing fails closed and names the issue, including a block hidden
by an unclosed code fence; a block quoted in a balanced fence is ignored. A
rewrite preserves every character outside the block, CRLF and non-ASCII text
included, and must parse back to the block it wrote.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fence tracking toggled on any backtick or tilde fence line, so a backtick line
inside a tilde fence closed it early and a later real block on a trusted issue
read as fenced, returning no marker and inviting a duplicate filing. Tracking
now follows CommonMark via Update-PfbDriftFenceState: a fence closes only on a
run of the same character at least as long as its opener.

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

Get-PfbDriftFenceState avoids the state-changing-verb analyzer rule without a
suppression. $script:PfbDriftFence has no remaining readers; Task 6 will use the
helper instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every body and comment opens with the automation disclaimer; bodies end with the
machine block and stay under GitHub's size limit by truncating the table, never
the block. Titles are limited to characters that survive the ghx .cmd shim.
One area: label per issue, from the group's most severe finding.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An optional **Drift keys:** field (fp:, param:, family:, category:) lets a
settled entry stop the reconciler filing a declined finding again. An
unrecognised key stops the run instead of silently matching nothing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tracked > reappeared > settled > declined > regression (reopen:<N>) > append >
create, first match wins. Open issues come first so a "still reported after #N"
issue is filed once; closed duplicates are ignored; a close with no reason
counts as completed; paired legacy issues are never appended to; an issue
without source:drift claims nothing.

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

One block rewrite and one comment per changed issue; a vanished fingerprint is
announced once; an issue with nothing left is labelled status:resolved-upstream
in place of its status label, and never closed. The status label is derived
from the resulting block on every run, so an append lifts resolved-upstream and
a label left wrong by a failed call is fixed next time. More than 25% of
recorded fingerprints vanishing in one run aborts the plan. Creates are capped,
most severe groups first.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dry run by default)

Reads both reports and docs/settled, lists every issue, prints the plan, and
writes only with -Apply. Bodies go through --body-file as BOM-less UTF-8, stdout
is decoded as UTF-8, the CLI runs from the repo root (ghx resolves identity from
there), and every label the plan adds is checked before the first write. Issues
whose pfb-drift block was ignored for lack of source:drift are named in a
warning. Tested against a fake CLI: a dry run makes no write call; -Apply does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
workflow_dispatch only, dry run unless the apply input is ticked, built-in
token with issues: write and nothing else writable, never cancelled mid-run --
each property pinned by a test. Documents status:resolved-upstream in
TRIAGE-ROLES.md, the reconciler in tools/README.md, and the reports' new
consumer in Reports/README.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A report row whose tuple member was absent or renamed read as '', so
distinct rows collapsed onto one garbage fingerprint and were merged
without a word (final review I-1, measured: two dead keys with wireKey
renamed became one finding 'deadKey|GET /alerts|').

Get-PfbDriftRowValue now reads every member the frozen tuple needs
(endpoint, method, wireKey, location, field, from, to, cmdlet,
parameter) and throws on an absent or empty one, naming the category,
the member and the row. The dead-key report also gains a schema gate:
it has never carried a schemaVersion, so one appearing now stops the
run instead of being read under the old shape.

No golden value moves: the committed reports give the same 1143
fingerprints before and after, byte for byte.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A fingerprint recorded only in a closed issue's vanished list must still
be declined (NOT_PLANNED) or filed as a regression (COMPLETED). The code
already did this, but the spec-named rule had no test (final review,
Task 7). Dropping the vanished term from the closed-issue lookup turns
exactly these two tests red on both editions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Invoke-TestRun runs New-PfbDriftIssue.ps1 in-process, so on every CI
leg the script appended its tables, 'APPLY' ones included, to the
Pester job's own GITHUB_STEP_SUMMARY (final review, Task 9). Each run
now points the variable at a file in its fixture directory and restores
the previous value, or its absence, in a finally block.

Measured with GITHUB_STEP_SUMMARY set to a scratch file around the whole
test file: 1976 bytes and 4 APPLY lines written before, 0 after, and the
variable holds its original value afterwards, on both editions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Only collaborators can apply the label, but an issue's author can always
edit its body and close it, so on an outsider-authored issue the label
would vouch for a block the outsider still controls (final review I-2).
Say to apply source:drift only to collaborator- or bot-authored issues,
and to check the author first, by hand or in the post-merge stamper.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@juemerson-at-purestorage
juemerson-at-purestorage merged commit c678bc8 into dmann000:main Sep 25, 2026
7 checks passed
@juemerson-at-purestorage
juemerson-at-purestorage deleted the feat/drift-issue-reconciler branch September 25, 2026 15:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant