Skip to content

Backlog scorer: rank open issues into lanes (read-only) - #173

Merged
juemerson-at-purestorage merged 18 commits into
dmann000:mainfrom
juemerson-at-purestorage:feat/backlog-scorer
Sep 25, 2026
Merged

juemerson-at-purestorage merged 18 commits into
dmann000:mainfrom
juemerson-at-purestorage:feat/backlog-scorer

Conversation

@juemerson-at-purestorage

Copy link
Copy Markdown
Collaborator

Summary

Adds tools/Build-PfbBacklog.ps1, a read-only backlog scorer. It sorts every open issue into exactly one lane and ranks the lanes that are ready to work, so a script rather than a person picks the next unit of work.

Lanes:

  • Ranked: build (agent-ready, design-approved) and design (needs-design).
  • Proposals: triage, where each issue gets a proposed priority and size for batch confirmation.
  • Unranked: inFlight, parked (blocked, human-only), confirmClose (resolved-upstream).
  • labelErrors: issues whose labels break the rules in docs/TRIAGE-ROLES.md.

Output is PfbBacklog.json (schema v1) and PfbBacklog.md. The new Backlog workflow publishes the Markdown to the job summary and uploads the JSON as a 90-day artifact. Nothing is committed and Reports/ is untouched.

What it adds

  • tools/lib/PfbGitHubRead.ps1: shared read-only GitHub REST helpers (Invoke-PfbGitHubApi, Invoke-PfbGitHubList, and a new Invoke-PfbGitHubPagedList).
    • Paging uses -FollowRelLink, flattening both page shapes.
    • The helper throws rather than truncating when the last page still links to a next page.
    • A rate-limit 403 and a permission 403 produce different messages.
  • tools/lib/PfbBacklogTools.ps1: the pure rules.
    • Normalises REST rows and keeps the drift trust boundary: only source:drift issues contribute findings.
    • Places each issue in a lane and reports label problems in two tiers.
    • Holds the impact-class table, the triage proposal and the tiered sort with decidedBy.
    • Builds the schema-v1 object and the Markdown.
  • tools/Build-PfbBacklog.ps1: reads open issues through the paged helper and joins them to the committed drift and dead-key reports.
    • Parameters: -Repo, -Token (then GH_TOKEN, then GITHUB_TOKEN), -OutputPath, -Lane, -First.
  • .github/workflows/backlog.yml: runs on workflow_dispatch and after every Drift Issues run.
    • Permissions are contents: read and issues: read only.
    • Concurrency group backlog with cancel-in-progress.
    • Inputs reach run: blocks only through env:.
  • Tests: Tests/PfbGitHubRead.Tests.ps1, Tests/PfbBacklogTools.Tests.ps1 and Tests/Build-PfbBacklog.Tests.ps1.
  • tools/README.md: a new section documenting the scorer.

Refactor

scripts/Assert-PfbAgentReadyBrief.ps1 now dot-sources tools/lib/PfbGitHubRead.ps1 instead of defining its own REST helpers. Its messages, guards and User-Agent are unchanged. The script has no tests, so I checked it with an anonymous live run before and after the extraction: the output was identical (Compare-Object printed nothing). tools/New-PfbDriftIssue.ps1:158's comment pointer now points at the lib.

Design decisions

  • Priority first. The human priority: label is the primary sort key; the score only orders issues within a band.

  • Tiered keys, no weights. Issues sort by priority, readiness, impact class, size, live finding count (descending), then issue number. decidedBy names the first key that differs from the row above.

  • Proposals only. No labels are written. Triage issues get a proposed priority and size, with a differs flag when these disagree with the current labels.

  • Impact classes. The proposal comes from the issue's live drift findings. It reproduces the hand triage of Drift: dead or unreachable request keys in the 'arrays' family #163-Drift: dead or unreachable request keys in the 'support' family #172 10/10, pinned as a fixture test.

    Class Findings Proposed
    A dead key on a write (DESTRUCTIVE, CREATE) P0
    B dead key on a GET, no surviving selector P1
    C response or ValidateSet drift P1
    D net-new coverage P2
    E new ValidateSet candidate P3

    Size is S for 10 or fewer live findings and M above that; L is never proposed. An unknown category or severity throws.

  • Nothing committed. Output goes to the job summary and a run artifact.

  • Output shaping only. -Lane and -First shape the Markdown only; the JSON always carries every lane.

  • notes field. Each row carries a notes array. For example, a trusted issue with 0 live findings is flagged as possibly resolved.

  • Markdown escaping. Cells escape | and <, so titles and label problems that contain <name> or HTML comments are not dropped by GFM.

Verification

Scoped Pester, touched areas (8 files):

Edition Pester Passed Failed Skipped Container
pwsh 7 (7.6.6) 6.0.1 406 0 0 ok
WinPS 5.1 6.0.1 225 0 181 ok
File pwsh 7 passed/failed/skipped 5.1 passed/failed/skipped
Tests/PfbGitHubRead.Tests.ps1 18 / 0 / 0 0 / 0 / 18
Tests/PfbBacklogTools.Tests.ps1 147 / 0 / 0 0 / 0 / 147
Tests/Build-PfbBacklog.Tests.ps1 16 / 0 / 0 0 / 0 / 16
  • Coverage baseline. Tests/coverage-baseline.psd1 pins the winps51 ExpectedSkips at 18 / 147 / 16. The source is #Requires -Version 7.0, so every Describe skips on 5.1. The header was recomputed as 22 entries summing to 503, across 213 test files.
  • Analyzer. PSScriptAnalyzer 1.25.0 with the repo settings: 0 gated findings and 0 warnings on every touched file. Every added line is ASCII.
  • Live read-only smoke run. One anonymous GET scored all open issues, 56 at the time of the run. Drift: dead or unreachable request keys in the 'arrays' family #163-Drift: dead or unreachable request keys in the 'support' family #172 landed in build/design, the legacy triage set landed in triage with proposals, and labelErrors was empty. git status was clean afterwards.
  • Live multi-page probe. Run during the final review, it confirmed the paging path against the real API:
    • several pages flatten to the full record set;
    • the ceiling throws when a next page remains;
    • exactly-full pages at the ceiling do not throw.
  • Wire exemption. Test-PfbWireExemption.ps1 gives VERDICT: EXEMPT. 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.

No version bump or CHANGELOG edit.

After merge

workflow_run fires only from the workflow file on the default branch. The Drift Issues → Backlog chain starts working once this is merged; until then only workflow_dispatch can run it.

🤖 Generated with Claude Code

…GitHubRead.ps1

Adds Invoke-PfbGitHubPagedList: follows rel=next links, flattens both
-FollowRelLink output shapes, and throws when the last page still links
to a next one. -UserAgent is mandatory on every call.

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

No behaviour change: an anonymous read-only run against dmann000/fb-powershell
before and after the extraction produced identical output. The gate keeps its
own User-Agent. New-PfbDriftIssue.ps1's pointer now names the lib.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Drops pull requests, maps state_reason to the gh shape ConvertFrom-PfbDriftIssue
reads (StrictMode-safe), and parses each issue's block alone so a malformed
trusted block is reported instead of stopping the run.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Errors (unplaceable) send an issue to labelErrors; warnings leave it in its
lane. The paired legacy source:drift exception is never warned on.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A single named table drives the in-band sort and the proposed priority; an
unknown category or dead-key severity throws. The dmann000#163-dmann000#172 hand triage is a
fixture test and reproduces 10/10 with LF and CRLF bodies.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Priority, readiness, impact, size, live findings, number -- no weights. Each
row names the first key that differs from the row above it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Every open issue lands in exactly one of seven lanes; ranked lanes carry rank
and decidedBy, triage carries proposals, labelErrors carries its problems. A
trusted issue with no live finding is noted. LF and CRLF bodies give the
same result.

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

GFM strips a title such as 'Add <name> support', or a label problem that
echoes an HTML comment, as raw HTML. Format-PfbBacklogCell now
backslash-escapes < as well as |; the JSON keeps the text unescaped.

Also pins three render paths: a triage row with no proposal, a note on a
row hidden by -First, and the whole Markdown under Set-StrictMode Latest.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Fetches open issues (paged, anonymous unless a token is given), reads the two
committed drift reports and writes PfbBacklog.json/.md to -OutputPath, or the
Markdown to the console. Nothing is committed and no label is written.

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

The drift fixture's analysedVersions is now 2.27, so the specVersion 2.28
assertion can only pass if the value comes from the dead-key report. The
anonymous-mode hint names GH_TOKEN (the first fallback) as well as GITHUB_TOKEN.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
workflow_dispatch plus workflow_run on Drift Issues (any conclusion); issues:
read only; latest run wins. Markdown to the job summary, JSON as a 90-day
artifact. -First is passed only when the input is non-empty.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
PfbGitHubRead.Tests.ps1 18, PfbBacklogTools.Tests.ps1 147,
Build-PfbBacklog.Tests.ps1 16 -- new files, gated wholesale on 5.1 because
the code under test is PowerShell 7 only. Measured, not inferred. Header note
recomputed from the map: 22 entries, 503.

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

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@juemerson-at-purestorage
juemerson-at-purestorage marked this pull request as ready for review September 25, 2026 23:25
@juemerson-at-purestorage
juemerson-at-purestorage merged commit 233b7c8 into dmann000:main Sep 25, 2026
7 checks passed
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