Skip to content

fix(deps): consolidate Dependabot and add a repair pass that keeps packages.config projects internally consistent - #920

Merged
drmoisan merged 34 commits into
mainfrom
bug/dependabot-fanout-and-ci-failing-nuget-upgrades-911
Sep 26, 2026
Merged

drmoisan merged 34 commits into
mainfrom
bug/dependabot-fanout-and-ci-failing-nuget-upgrades-911

Conversation

@drmoisan

Copy link
Copy Markdown
Owner

Suggested title

fix(deps): consolidate Dependabot and add a repair pass that keeps packages.config projects internally consistent

Summary

  • Adds .github/workflows/dependabot-repair.yml, a repair pass that runs on the Dependabot pull request and reconciles every project against its packages.config (<Import>, <Error>, <Reference>, <HintPath>, <Analyzer Include>), then verifies the result.
  • Consolidates .github/dependabot.yml to a single catch-all group with open-pull-requests-limit: 1, so one upgrade cycle produces one pull request. All existing ignore entries and semver-major exclusions are preserved and pinned by a test.
  • Moves packages.config and app.config out of the CSharpier gate via .csharpierignore and normalises all 18 manifests once. Dependabot writes these files inline, and the format check previously rejected all 20 files it touched on Bump the test-frameworks group with 11 updates #908.
  • Pins nuget-version to an exact version in _build-analyzers.yml, _build-nullable.yml and _mstest-coverage.yml, replacing the floating latest.
  • Folds in the prerequisite defects: Bug: Bug: Sync-PackageReferences ranks netstandard2.1 above netstandard2.0 and would reintroduce #895 #902 (Sync-PackageReferences.ps1 ranked netstandard2.1 above netstandard2.0; incompatible frameworks are now excluded rather than ranked) and Bug: Bug: ToDoModel.Test/packages.config omits FSharp.Core and Deedle despite carrying HintPaths for both #903 (ToDoModel.Test/packages.config omitted FSharp.Core and Deedle).
  • Adds six PowerShell modules/scripts under scripts/dependencies/ with Pester suites; PowerShell line coverage moves from 83.93% on origin/main to 94.49% at head.

Why

The CI failure on Dependabot pull request #908 had a verified three-version divergence inside single projects:

Location Meziantou.Analyzer version
packages.config 3.0.235
.csproj <Import> / <Error> 3.0.259
.csproj <Analyzer Include> 3.0.203

nuget restore honours packages.config, the build honours the .csproj, and EnsureNuGetPackageBuildImports fails closed. Dependabot already invokes nuget update, so replacing it would inherit the same defect. The design decision recorded in the feature folder is to keep Dependabot as the upgrade engine and add a repair pass over its own pull request.

The previously assumed "one pull request per group x directory" fan-out was falsified: #908 was one pull request spanning ten directories. The fan-out was one pull request per group, and four groups produced four pull requests, including #907, #908 and #909.

What Changed

Core feature: repair pass

  • scripts/dependencies/Repair-PackageManifestConsistency.ps1: composition root invoked by the workflow.
  • scripts/dependencies/ProjectConsistency.psm1: version reconciliation across the four dependent element kinds, including Reference assembly versions resolved from assembly evidence.
  • scripts/dependencies/AnalyzerItemRepair.psm1: rewrites <Analyzer Include> paths while preserving the existing analyzer folder segment and sibling items.
  • scripts/dependencies/ConsistencyVerifier.psm1: repairs freely and fails only on residual inconsistency; asserts reference completeness and detects orphaned hint paths.
  • scripts/dependencies/PackageCompatibility.psm1: asset-level compatibility gate; an incompatible package is skipped and the remaining upgrades proceed.
  • scripts/dependencies/PackageGraph.psm1: manifest discovery and the idempotent normaliser.
  • scripts/vscode/Sync-PackageReferences.ps1: rewritten framework selection (Bug: Bug: Sync-PackageReferences ranks netstandard2.1 above netstandard2.0 and would reintroduce #895 #902) behind an injectable seam.

CI / tooling

  • .github/workflows/dependabot-repair.yml (new): gated on the repair write set; commits under a GitHub App identity when the credential exists, otherwise documented fallback.
  • .github/dependabot.yml: single group, limit 1.
  • .github/workflows/_build-analyzers.yml, _build-nullable.yml, _mstest-coverage.yml: exact nuget-version pin.
  • .github/workflows/_pester.yml: scope extended to the new test directory.
  • .csharpierignore: packages.config and app.config.

Mechanical

Tests

  • Eight new Pester suites under tests/scripts/dependencies/ and tests/scripts/vscode/Sync-PackageReferences.Tests.ps1 (previously the only script in scripts/vscode/ with no tests). 320 tests pass.

Docs

  • .github/workflows/README.md: operator documentation for the repair workflow, including the stated limit that the app.config binding-redirect pass exists in the script but is not reachable from the workflow_run invocation.
  • Feature folder with issue, spec (26 acceptance criteria), research, plan, remediation plan, runbook, two review cycles and evidence.

Architecture / How It Fits Together

  1. Dependabot opens one grouped pull request and runs nuget update, which rewrites packages.config, <Import>, <Error> and <Reference> but not <Analyzer Include>, and may write out-of-scope versions.
  2. dependabot-repair.yml checks out the pull request branch and runs Repair-PackageManifestConsistency.ps1.
  3. The composition root discovers manifests (PackageGraph), gates compatibility (PackageCompatibility), reconciles project elements (ProjectConsistency, AnalyzerItemRepair), then runs ConsistencyVerifier, which fails the step on any residual inconsistency.
  4. If the write set is non-empty, the workflow commits onto the Dependabot branch and appends a disclosure block to the pull request body. The push is a pull_request synchronize event, so the required checks re-run.

Verification

Completed (recorded under evidence/qa-gates/)

  • PowerShell loop, final pass after the post-review corrections: PoshQC format 0 rewrites; PSScriptAnalyzer 13 findings, identical to the baseline, 0 in owned files; Pester 320 passed, 0 failed, aggregate line coverage 94.49%.
  • C# toolchain (AC25): CSharpier check, analyzer rebuild, nullable rebuild and MSTest with coverage passed; C# coverage 85.93% line / 80.10% branch on an unchanged denominator. Not re-run after the final correction commit, which changed no .cs, .csproj or config file.
  • AC6: cold-cache analyzer build failure observed before the fix and absent after.
  • AC21/AC22: the Bump the test-frameworks group with 11 updates #908 three-way divergence reproduced as a fixture, observed failing before the fix and passing after.
  • Independent re-audit at head: 1,498 package restore-path references across 18 project/manifest pairs, zero disagreeing with the sibling manifest.
  • A dispatched CI run (35513025198) passed all six jobs at de9a00106; the intervening commits are documentation and PowerShell-only. The PR-triggered run at the merge head is the authoritative gate.

Not verified in this PR

Recommended

  • pwsh -NoProfile -File scripts/dependencies/Repair-PackageManifestConsistency.ps1 against a clean checkout should produce an empty git diff.
  • After merge, observe the next Dependabot pull request and the dependabot-repair run it triggers.

Backward Compatibility / Migration Notes

Risks and Mitigations

Review Guide

  1. spec.md Acceptance Criteria, then feature-audit.2026-09-20T09-42.md for the latest review outcome.
  2. .github/dependabot.yml, .github/workflows/dependabot-repair.yml, .github/workflows/README.md.
  3. scripts/dependencies/Repair-PackageManifestConsistency.ps1, then the five modules, then scripts/vscode/Sync-PackageReferences.ps1.
  4. The Pester suites.
  5. Skim only: the 35 packages.config / app.config normalisations and the evidence tree.

Merge instructions

Follow-ups

GitHub Auto-close

Referenced, not closed: #911 (stays open until #914), #914, #898 (already closed by #913), #563, #668 (coverage-floor conflict, unresolved), #181 and #895 (already closed). #907, #908 and #909 are Dependabot pull requests, not issues. Issue states were checked with gh issue view in this session because the PR-context collector reported the GitHub CLI as unavailable.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Tphos3rkKSiEVLUCuSu8UK

drmoisan and others added 30 commits September 19, 2026 09:51
Records the diagnosis that Dependabot cannot maintain the .csproj state
coupled to packages.config in non-SDK projects, and the settled design in
which Dependabot detects while a NuGet-driven workflow performs the upgrade.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zmuPjoz21q75F4iMZ4BQ1
Execution falsified the original premise. Dependabot does maintain .csproj
and app.config state; its packages.config updater invokes the NuGet CLI.
The verified cause is that while updating one group it also rewrites the
Import and Error guards of out-of-scope packages to a version no manifest
declares, so restore and build disagree and the package-imports target
fails closed.

Adds spec.md with 26 acceptance criteria, the research artifact, and a
runbook for the one-time GitHub App token setup.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zmuPjoz21q75F4iMZ4BQ1
Adds a ten-phase plan covering the prerequisite corrections, the
Sync-PackageReferences ranking fix, the Dependabot consolidation and the
repair-pass components.

Records the pre-fix red run proving origin/main does not build from a cold
restore: CS0006 on Meziantou.Analyzer.3.0.203, a version no manifest
declares. Also records that the pester job is scoped to tests/scripts/vscode
and would not execute this change's tests until its paths are widened.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018zmuPjoz21q75F4iMZ4BQ1
Replaces a vacuous fixture-availability query that evaluated to zero for
every possible repository state and would have selected the deferred
verification branch even with open Dependabot pull requests.

Removes unreachable ok:true acceptances against sixteen pre-existing
analyzer findings, replaces the unreachable eighty-five percent coverage
floor at batch A close-out with a no-regression assertion against the
measured baseline of 83.93 percent, and replaces an actionlint file count
the command never prints with an independent enumeration.

Splits ProjectConsistency into reconciliation and verification modules to
stay under the five hundred line ceiling and to separate the mutator from
the inspector concern.

128 tasks across ten phases.

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

Revision 4 replaces the analyzer folder SELECTION rule with a PRESERVE rule
across five places, after measurement showed neither analyzer family names
the highest folder its package ships: Meziantou items sit at roslyn5.0 with
5.6 and 5.9 available, Roslynator items at roslyn4.7 with 5.0 available. A
selection rule would have rewritten all eighty analyzer items rather than
the fifteen that are stale.

Revision 5 adds gate rule 12 requiring byte-exact path substitution and a
positive residual assertion, after a sed pattern with doubled backslashes
silently matched nothing while sed -i still rewrote fifteen files' line
endings, producing a fifteen-file porcelain against an empty diff.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Criterion authorship belongs to the planning agents, not the executor: the
acceptance-criteria-tracking skill limits executors to checking boxes, and
P9-T11 asserts no criterion text was reworded during execution. Amending
here, before Phase 0 begins, removes that contradiction.

AC12 previously required that a listing offering two Roslyn-qualified
folders select the higher. Measured, neither analyzer family in this
repository names the highest folder its package ships, so a selection rule
would rewrite all eighty analyzer items rather than the fifteen that are
stale. AC12 now requires the existing folder segment be preserved and only
the version segment moved, with a reported non-fatal class when the
preserved segment is absent from the new version.

Also corrects the Risks bullet that named the superseded rule.

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

Adds a MEZIANTOU-898-STATE declaration so the plan survives either merge
order relative to the split branch bug/meziantou-analyzer-hintpath-skew-898.
Without it three tasks fail rather than trivially pass when that branch
lands first, two of them in phase zero. The already-landed branch requires
a positive base-commit count of sixteen files at 3.0.235, because a tree
from which the fifteen items had simply been deleted would otherwise
satisfy every zero-valued clause.

Counts the fifth verifier surface everywhere it is cited, gives the
missing-segment class one owner, and removes a test-name ban that forbade
the clearest name for the case it required.

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

The clause added to stop the already-landed branch being selected vacuously
was itself vacuous. Measured against both refs: the unanchored literal
returns sixteen files in BOTH states because it also matches the Import and
Error guards, which already name 3.0.235 everywhere - that asymmetry is
defect 898 itself. Read as a sum it returns thirty-three and forty-eight,
neither of which is the asserted sixteen. Anchoring to Analyzer Include=
returns one at an unfixed base and sixteen at a fixed one, and returns one
again for a tree whose analyzer items were merely deleted.

Replaces the aggregate footprint floor with five per-class counts. Neither
seventy nor fifty-five could fail for any reason connected to this change:
the deterministic classes total sixty paths before any evidence artifact
and a sibling task asserts at least eighty-five artifacts.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The per-batch budget assertions read hook state that can never populate:
the hook roots itself at the session worktree and discards out-of-root
candidates with allow and no state write, so every PowerShell file this
plan writes in the execution worktree is silently ignored. Replaced with a
direct count over each batch commit, which measures the property instead of
a proxy that cannot observe this tree.

Coverage floors corrected from 85 to 80 at five sites. The authoritative
CLAUDE.md is the execution worktree's copy, which records the maintainer
decision of 2026-09-11 under issue 563; the session copy is 243 commits
stale. The discrepancy against .claude/rules is tracked as issue 668. This
is material: the measured Pester baseline of 83.93 percent sits between the
two figures.

Twenty-two coverage collector documents moved out of the evidence tree to
gitignored coverage/, because the authoritative CLAUDE.md prohibits adding
a raw collector or test-platform document to git in any form, including
under a feature folder's evidence tree.

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

Revision 8 stopped committing raw coverage documents but committed nothing
in their place. The policy section requires the substitution, not the
prohibition alone: its rationale is that the projection and summary carry
every figure a reviewer needs. The coverage runner already emits all three
permitted forms into gitignored coverage, where the next run overwrites
them, so two tasks now copy the JaCoCo projection and the trx summary into
the evidence tree and commit them.

States plainly that the PowerShell route has no enumerated permitted form,
because all three are defined against the C# Cobertura path and Pester
emits JaCoCo directly, so its markdown figures stand in for a form the
policy does not define rather than satisfying one it does.

Replaces two at-most-three batch assertions with exact counts. An at-most
bound is satisfied by zero and zero, so it caught an overrun and missed an
omission - the seventh absence-shaped defect in this plan, introduced while
repairing the sixth.

Restores the P0-T1 through P0-T10 tick set, destroyed by the previous sync.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ining SHA, tick assertions

The trx-derived summary is emitted only when the document parses and
carries a result-summary node; the projection is unconditional. Revision 9
treated both as unconditional, and the warning branch is live in this
repository. Both copy tasks now branch, recording the warning verbatim and
copying the projection alone when the summary is absent.

Stops pinning the plan file's own commit hash, which the act of revising
the plan invalidates. Round 2 fixed this by substituting a fresh literal
and revisions 8 and 9 re-broke it. The assertion is now a property: the
recorded commit is the tip commit touching the plan file and is an ancestor
of HEAD. A sweep found and fixed one further literal of the same class.

Adds exact ticked-task assertions at P0-T24 and at all five commit tasks -
23, 46, 64, 93, 105 and 111 - so a destroyed tick set surfaces at the next
batch boundary rather than at task 127 of 128. Three of those five figures
were wrong on first derivation and were corrected from the phase totals.

Two sites still credited the batch-budget hook with denying a fourth write;
both now name the three measurements that actually enforce it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The round-7 replacement for the self-invalidating plan hash asserted that
the last-touching commit is an ancestor of HEAD. git log restricted to a
path walks HEAD's own history, so every value it returns is reachable from
HEAD by construction and the check cannot fail. A fix for the
literal-invalidation class reintroduced the constant-valued class the same
reviewer had removed at P0-T3. Now asserts a non-empty forty-character
value, whose reachable failure is the empty return for an untracked path.

The Pester artifact count of twenty was written without being counted and
matches none of the three real populations: twenty-three tasks invoke a
Pester command, twenty-two documents are produced, six record a coverage
figure. Left alone it would have driven the work rather than described it.
The obligation now falls on the six, enumerated by task id.

Records the rev-list figure as measured rather than asserting a literal; it
has already drifted from four to eight to eleven. Clarifies that a phase
restart re-runs tasks without reverting their check-offs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
…manifest entries, NuGet pin

Batch A of issue #911.

- .csharpierignore: append **/packages.config and **/app.config so the formatter no
  longer owns the manifests the NuGet CLI writes. Appended rather than inserted, so
  line 4 (**/evidence/**) keeps the line number three later sites cite.
- scripts/dependencies/PackageGraph.psm1 and its Pester suite: a pure-over-text parser
  and renderer for packages.config, project files and app.config, with discovery behind
  an injected delegate. 164 of 164 instrumented lines covered.
- One-time normalisation of all 18 packages.config and 17 app.config files to canonical
  inline form. 17 and 17 changed; SVGControl/packages.config was already canonical.
- Issue #898: realign the stranded Meziantou.Analyzer.3.0.203 analyzer item to 3.0.235
  in 15 project files, leaving the analyzers\dotnet\roslyn5.0\cs segment byte-identical.
  A cold restore plus solution rebuild is now green where it previously failed CS0006.
- Issue #903: add the Deedle 3.0.0 and FSharp.Core 11.0.100 entries that
  ToDoModel.Test.csproj already referenced by HintPath.
- Pin nuget-version to 7.9.0 in the three workflows that used a floating selector, and
  widen the Pester workflow to the two-member test and coverage paths.

Gates: csharpier check 0; msbuild analyzers 0 with 18 assemblies compiled; msbuild
nullable 0; Pester 206 passed 0 failed; PSScriptAnalyzer 16, the pre-existing baseline,
with 0 in files this change owns; MSTest 7343 passed, line 85.93 percent and branch
80.09 percent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
… dependabot consolidation

Adds scripts/dependencies/PackageCompatibility.psm1, an asset-level
compatibility gate for net481 that excludes netstandard2.1 outright by
non-membership rather than ranking it last, which is the #902 correction:
a ranking still selects the framework when nothing else is offered.

Rewrites scripts/vscode/Sync-PackageReferences.ps1 from 159 to 423 lines.
The $tfmPreference array is deleted and all framework selection is
delegated to the shared module; the script is restructured into eight
advanced functions behind a seven-member injectable filesystem seam, so
the repair logic is exercisable in memory with no temporary file. The
three PSAvoidUsingWriteHost findings the file carried are removed.

Consolidates .github/dependabot.yml to one catch-all group with
open-pull-requests-limit 1, removes the four inert partition keys, and
adds an unqualified Deedle ignore entry. The eight semver-major ignore
entries are retained unchanged and in order.

Delivers AC1, AC4, AC7 and AC9.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
…e verifier

Adds scripts/dependencies/AnalyzerItemRepair.psm1, the restore-path layer and the
#898 repair. The repair is governed by the preserve rule: it changes only the
<Id>.<Version> segment of an <Analyzer Include> path and leaves every following
segment byte-identical, including the Roslyn-qualified folder. The restored package
is enumerated through an injected listing delegate solely to confirm the preserved
segment still exists, never to select a folder.

That rule is measured rather than stylistic. Neither analyzer family in this
repository names the highest folder its package ships: Meziantou items sit at
roslyn5.0 while the package ships roslyn4.14, roslyn4.8, roslyn5.0, roslyn5.6 and
roslyn5.9, and Roslynator items sit at roslyn4.7 while the package ships roslyn3.8,
roslyn4.7 and roslyn5.0. A selection rule would rewrite all 80 analyzer items in
those two families rather than the 15 that were stale. Both preservation fixtures
are constructed so the highest offered folder is not the one the item names, so a
selection implementation fails them.

When the preserved segment is absent from the new version's listing the repair
emits no guessed path: it leaves the item unmodified and returns a record. When the
restored directory does not exist at all it throws.

Adds scripts/dependencies/ProjectConsistency.psm1 carrying version reconciliation
across <Import>, <Error>, <Reference> and <HintPath>, and binding-redirect
reconciliation into both the oldVersion upper bound and newVersion. A <Reference>
takes a resolved assembly version rather than the package version, because the two
are not required to track each other.

Adds scripts/dependencies/ConsistencyVerifier.psm1 carrying every finding class,
every examined count, the repairs report, the failure-result type and the repair
entry point. Two classes are non-fatal and are counted and named rather than
failing the run: a dependent element whose package is absent from the sibling
manifest, and an analyzer item whose preserved Roslyn segment is absent from the
listing. The live altcover instance in QuickFiler.Test is reported by the general
rule; no package identifier is special-cased. A failure result is reserved for a
divergence no repair can resolve.

The module is split from the reconciliation half unconditionally. The combined file
measured 644 lines against the repository's 500-line ceiling; the restore-path
vocabulary moved to the module whose subject is restore paths and the verifier now
stands at 493.

Adds the three matching Pester suites. The AC21 case reproducing the #908 three-way
divergence was captured failing against the declared pass-throughs and passing on
the delivered tree, with no edit to the test between the two runs.

Delivers AC8, AC11, AC12, AC13, AC14, AC16, AC21, AC22 and AC23.

Gates: PoshQC format 0 rewrites on the closing pass; PSScriptAnalyzer 13, the
pre-existing baseline, with 0 in files this change owns; Pester 268 passed 0 failed,
aggregate line 93.85 percent, with AnalyzerItemRepair and ProjectConsistency at 100
and ConsistencyVerifier at 98.74; csharpier check 0 over 1623 files. The batch
changed no C# compilation input.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Adds scripts/dependencies/Repair-PackageManifestConsistency.ps1, the composition
root and command-line entry point. It owns every filesystem dependency: the five
modules it wires are pure over text and reach the disk only through the delegates
it supplies, which is what lets the whole pipeline be driven over an in-memory
fixture with no temporary file. The passes run in the order each consumes the
previous one's output: asset-level compatibility gate, version reconciliation,
analyzer-item regeneration, binding-redirect reconciliation, normalisation,
verification. An incompatible package is skipped with a recorded reason and the
remaining upgrades proceed.

Two resolution rules confirm rather than select, because the script can observe
what a restored package ships but cannot reproduce the assembly MSBuild binds. A
Reference assembly version some assembly in the restored package already carries
is preserved; only a version the package carries nowhere is rewritten. Measured
over this tree, 796 of 796 declared Reference versions are confirmed, so the rule
is a no-op here and stays falsifiable. The alternative - resolving the compatible
folder's assembly outright - disagreed with 9 declared versions and would have
rewritten them. An analyzer item's folder segment is likewise preserved and the
restored listing is enumerated solely to confirm the item still resolves.

The missing-segment class is measured over every analyzer item rather than only
over items a repair touched: each of the 162 items is compared against the path
set the preserve rule derives for its own package, and all 162 are confirmed, so
the class reports 0 over an examined population of 162 rather than 0 over 0. The
absent-from-manifest class excludes the HintPath kind, an absent HintPath being
necessarily orphaned as well, so the two altcover imports in QuickFiler.Test are
counted once and the seven orphaned hint paths are reported in their own class.

Adds .github/workflows/dependabot-repair.yml. It triggers on workflow_run at CI
completion, restricted to head branches under the dependabot prefix, mints a
GitHub App installation token from DEPENDABOT_REPAIR_APP_ID and
DEPENDABOT_REPAIR_APP_PRIVATE_KEY, restores with NuGet pinned to 7.9.0, runs the
repair, pushes onto Dependabot's branch with the same token, appends the repairs
report to the pull-request body and applies deps:autofixed only when a repair
outside the analyzer-item and binding-redirect classes was applied.

Documents the workflow, its trigger, both secret names, the installation runbook
and the degraded mode in .github/workflows/README.md, with the NuGet pin recorded
once and asserted equal to every workflow literal.

Adds tests/scripts/dependencies/Repair-PackageManifestConsistency.Tests.ps1 with
30 cases, three of them the AC10 skip-and-proceed assertions, and extends
tests/scripts/dependencies/DependabotConfig.Tests.ps1 with the AC17 and AC26
assertions. The suite caught two defects in the entry point: -WhatIf did not reach
Invoke-ManifestNormalization, a preference variable set on a script not reaching a
module's own session state; and a disagreement on an item the repair deliberately
left alone produced a failure result despite the class being non-fatal.

Delivers AC5, AC10, AC15, AC17 and AC26.

Gates: the repair run over the working tree applies 0 repairs over 2559 examined
elements and writes nothing, twice, with the porcelain captures byte-identical and
the csproj-scoped porcelain empty after both runs; the merge-base csproj diff lists
exactly the 15 Write Set paths and no sixteenth; csharpier check 0 over 1623 files;
actionlint 0 with empty stdout over 9 workflow files; Pester 302 passed 0 failed,
with the entry point at 91.12 percent line coverage.

Reported, not repaired: ten binding redirects across six app.config files name an
older assembly version than the restored package and the project reference declare.
The drift predates this branch, is outside the upgrade this pass repairs, and is
recorded in the P7-T5 evidence for a follow-up issue.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
…riteria

Records the P8-T1 measurement that selected the deferred branch for AC18, AC19
and AC20: a repository secrets query exiting 0 with an empty name list, so
CREDENTIAL-PRESENT is false from a successful query rather than a forbidden one,
and an open-pull-request query exiting 0 with a Dependabot count of 0. Both
conditions for the live branch fail independently, so the three criteria are
unverifiable rather than failing and are left unchecked.

Records the follow-up issue 914, which carries the three deferred criteria with
their exact verification commands and the runbook path, the two scripts/vscode
files Scope Decision 8 leaves unformatted, the ten stale binding redirects
verified pre-existing at the merge base, and the latent no-AssemblyVersion
fallback in Invoke-ProjectConsistencyRepair that ships unreached. The issue was
created through the repository promotion lifecycle rather than the gh
issue-creation command, which a repository hook refused because the orchestrator
checkpoint's steps 6 to 8 are legitimately pending; the refusal output is
recorded verbatim in the mirror and no gate was suppressed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Corrects five PSScriptAnalyzer findings the Phase 9 analyzer step surfaced in
tests/scripts/dependencies/Repair-PackageManifestConsistency.Tests.ps1, the only
file this change owns that carried any. No analyzer step ran between that file
being authored in Batch D and this gate, which is why they surface here.

PSUseShouldProcessForStateChangingFunctions fired on the two fixture builders.
The New verb is in the analyzer state-changing set and neither function changes
state: each returns an in-memory hashtable. Declaring SupportsShouldProcess would
assert a capability they do not have, so both were renamed to Get-RepairFixture
and Get-StandardFixture. Both are defined and called only inside this file.

PSReviewUnusedParameter fired on three parameters referenced only inside nested
scriptblocks closed over by GetNewClosure, which the analyzer data-flow does not
follow. Each is now read into a local at body level and the closures capture the
local, which captures the same values. No assertion, expectation or fixture datum
was altered.

Records the full seven-step final QA loop. PowerShell: format rewrote 0 of 46
files with an empty revert set; analyze reports exactly 13 findings, every one a
member of the 16-tuple merge-base baseline and 0 in the fifteen owned files;
Pester 302 passed 0 failed with aggregate line coverage 93.89 percent, every new
module at or above 94.12 and Sync-PackageReferences moved from 0 covered lines to
95. C#: csharpier check 0 over 1623 files; both solution-wide rebuilds exit 0 with
0 CS0006 and 18 compiled projects each; MSTest 7343 passed 0 failed with
first-party coverage 85.91 percent line and 80.07 percent branch, each 0.02 points
below the P2-T7 baseline and well inside the half-point noise band.

Delivers AC24 and AC25, which brings the criteria to 23 of 26. AC18, AC19 and
AC20 remain unchecked as unverifiable rather than failing and are carried by
issue 914.

Commits the two permitted coverage-evidence forms the run produced, the JaCoCo
package projection and the test-result summary, rather than the raw collector
document that gate rule 12 prohibits under the evidence tree.

Reported, not repaired: the change-footprint audit finds one path outside the
four classes its acceptance enumerates, the feature-promotion lifecycle artifact
under docs/features/potential/promoted. It was introduced by the promotion commit
d46ae2d before Phase 0 and no plan task can avoid producing it; the clause
omits that inherited class. Recorded in the P9-T12 artifact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Records the review-handoff index over all 133 evidence artifacts, each with its
discharging task, its recorded exit code and, for the criteria-bearing ones, the
criterion it discharges. Every listed path was confirmed present on disk. The
index also pins the merge base, the four batch commits, the Phase 8 commit and
the Phase 9 commit.

Asserts the gate rule 12 honesty obligation where it can fail: exactly six
artifacts record a JaCoCo line figure and each was searched for the standing-in
statement. Expected six, found six. The other seventeen Pester tasks record no
coverage figure and carry no such obligation.

Records the plan check-off re-sync. The execution copy and the session copy are
byte-identical once every task line is normalised to its unchecked form, which is
the fixpoint exclusion that lets this comparison terminate at all. The execution
copy carried 126 ticks at that measurement and carries 128 here, the last two
being the re-sync task and this one, ticked before the commit so the terminal tree
is clean.

Closes the plan at 128 of 128 tasks and 23 of 26 acceptance criteria. AC18, AC19
and AC20 remain unverifiable for want of a credential and a fixture and are
carried by issue 914.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Carries the artifact recording the plan close-out: 128 of 128 task lines ticked,
0 unticked, an empty porcelain capture after the close-out commit, and that
commit's SHA. The artifact is written after the commit it records because the SHA
it records does not exist until that commit is made, so this one further commit
exists only to carry it and leave the terminal tree clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Review verdict PARTIAL, no-go until the four blocking findings are
discharged. 23 of 26 acceptance criteria pass.

The review found three defects in the repair workflow that no gate had
exercised, because the workflow has never run. The most serious is that the
push gate counts only project-file repairs, so a run whose writes are
normalisation or binding-redirect goes green, skips the push, discards the
repair and reports that none were applied.

It also overturned the orchestrator's judgment that the reference-sync
coverage shortfall was acceptable: nine of the uncovered lines are pure
logic and every one is a negative or error path, including the rejection
handler for the very defect this change fixes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Seven phases, eighty tasks, discharging four blocking and five major review
findings plus four minor.

Amends the AC14 note to record that the binding-redirect class is exercised
by unit assertion only and is not reachable from the configured trigger,
because the workflow invokes the repair entry point with no candidate
upgrade set. Criterion authorship belongs to the planning agents, so the
amendment is applied here rather than by the executor; the criterion stays
ticked and the count stays at twenty-six.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Appends the gate rule 20 clause pair to P3-T6, the one task preflight
found omitting it. That task removes a filter clause on the argument that
the binding-redirect class is unreachable, and the unreachability rests
entirely on a trigger path that has never executed - so a reader saw a
deletion with no statement that its premise is unverified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
…ageReferences

Discharges remediation finding R2 for issue 911. Adds eight Pester tests driving the
existing injected seam, covering lines 151, 180, 248, 290, 293, 330, 336, 337 and 345.
Per-file line coverage moves from 95 of 127 to 104 of 127. Line 248 is the issue 902
rejection handler. Also records the Phase 0 remediation baseline for this cycle.

Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
… incorrectly

Discharges remediation findings R5, R9b and R9c for issue 911.

R5: Resolve-ReferenceAssemblyVersion moves from the composition root into
ProjectConsistency.psm1 and Invoke-ProjectConsistencyRepair now resolves the assembly
version per package and passes it, so the entry point preserves a declared Reference
assembly version instead of rewriting it to the package version for every caller.

R9b: the unfiltered Get-AnalyzerAssemblyPath call site is commented as a verification
membership set that must never be written to a project file.

R9c: the default manifest lister emits the enumerated directory and returned file counts,
making a one-level-deep discovery shortfall observable in the run log.

Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
… identity

Discharges remediation findings R3, R6, R7 and R8 for issue 911, all four in
.github/workflows/dependabot-repair.yml.

R3 blocking: the repair step publishes written-count from the WrittenPath set and the
commit step gates on it. RepairCount counts per-project repair records only, so a run
whose only writes were a manifest normalisation or a binding-redirect reconciliation went
green, skipped the push and discarded the repair.

R6: the disclosure step is guarded on written-count or skip-count, and the body edit
strips any prior delimited block before appending a fresh one, so repeated runs leave one
block rather than accumulating.

R7: the binding-redirect clause is removed from the beyond-known-weak filter and the
reachability decision is recorded at the line it explains.

R8: the commit identity is derived from the token step slug and the resolved bot user id,
with a named guard on each, replacing a hand-written address that matched no account.

Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
…aceholders

Discharges remediation finding R4 for issue 911, and R9a in the untracked pr_context file.

R4 blocking: 103 occurrences of an absolute host path disclosing the account name were
committed across 33 markdown documents under the feature folder. Every occurrence is
replaced with a placeholder token, byte-exactly, through a substitution map built at run
time from the home directory and applied longest-first under IgnoreCase. Seven distinct
spellings were present, including a dash-mangled scratchpad form the review did not
enumerate. Residual is zero occurrences across zero files, and the placeholder count rose
from 121 to 224, which is 121 plus the 103 replaced.

The working tree is sanitised but the pre-sanitisation blobs remain reachable in this
branch history. The pull request must be squash-merged. That instruction and its reason
are recorded for whoever merges.

Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
…tprint

Phase 5 of remediation cycle 1. The full toolchain ran in order in a single pass with no
restart: PoshQC format 0 rewrites of 46, PoshQC analyze 13 findings equal to baseline with
0 in the seven owned files, Pester 318 passed at 94.43 percent line coverage, CSharpier
1623 files clean, MSBuild analyzer and nullable rebuilds each 0 warnings and 0 errors
across 18 compiled assemblies, and MSTest 7343 passed at 0.8593 line and 0.8010 branch.

Both C# deltas against the post-merge baseline are positive on identical denominators.
Sync-PackageReferences.ps1 moves from 74.80 to 81.89 percent with all nine target logic
lines covered. All 17 footprint files are at most 500 lines. The change footprint is
exactly the 8 enumerated non-documentation paths, every one already a spec Write Set
member.

Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
drmoisan and others added 4 commits September 20, 2026 09:29
…an close-out

Phase 6 of remediation cycle 1, and the terminal commit of the cycle.

The branch is pushed and the CI workflow was dispatched against it. Run 35513025198 at
head de9a001 concluded success with all six jobs green:
actionlint, format-check, build-analyzers, build-nullable, mstest-coverage and pester.
That run carries event workflow_dispatch, so the authoritative discharge of R1 remains the
pull-request context run at whatever commit is merged.

Three merge-time instructions are recorded with their reasons: squash-merge, because the
pre-sanitisation blobs remain reachable in branch history; re-strip the two issue-number
detector false positives if pr_context is regenerated; and do not represent issue 911 as
closed until 914 discharges AC18, AC19 and AC20 against a live fixture.

All 80 plan tasks are ticked.

Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Each of these three records a fact that does not exist until after the commit it describes,
so none could be inside that commit. Documentation only, under the feature folder, and no
input to any CI job, so the green run at head remains evidence about the code at head.

Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Applies the one Major and four Minor items the 2026-09-20T09-42 re-audit
recommended before the pull request. Not a remediation cycle: the re-audit
returned zero remediable blocking findings and a Go decision, neither completed
plan was reopened, and no acceptance criterion was amended.

R-C2-1, Major. The workflows README advertised an app.config binding-redirect
repair the workflow_run trigger cannot reach. The clause is replaced with a
named limit that states the pass exists in the repair script, that the step
supplies no -CandidateUpgrade so the applied-upgrade set is always empty, and
the condition under which the class becomes reachable again.

R-C2-2, Minor and latent. Resolve-ReferenceAssemblyVersion matched
case-sensitively while Get-RewrittenReferenceVersionLine compares with -ne,
which does not. The resolver now uses RegexOptions.IgnoreCase and tolerates
whitespace on both sides of the Include name, matching the rewriter's Trim.
One test added, observed failing before the fix on the predicted mechanism -
resolver returns the empty string - and passing after. Exposure was and remains
zero of 912 Reference elements.

R-C2-3, Minor. The R9c discovery record used Write-Verbose and its comment
claimed the shortfall was observable in the run log. The workflow step passes
no -Verbose and sets no preference, so it was not. Now Write-Information with
-InformationAction Continue stated on the call, so visibility does not depend
on the caller, and the comment says what the code does. The test that pinned
the old literal is updated.

R-C2-4, Minor. Line 410 of Sync-PackageReferences.ps1 is now covered, taking
the file from 104 of 127 to 105 of 127, 82.68 percent, the ceiling under the
existing seam. The finding described 410 as the non-zero-fix branch; it is the
zero-fix branch, and the non-zero branch at 407 was already covered. The test
drives the branch that is actually uncovered and the discrepancy is recorded.

R-C2-5, Minor. The P3-T10 deletion table attributed 2 deletions to P3-T4 where
the diff shows 1, making the column sum to 6 against a measured 5. Re-measured
and corrected to 1 with a dated corrigendum; the conclusion and the numstat are
untouched.

PowerShell toolchain loop, restarted once after format normalised line endings
in four files. Final pass: format 0 rewrites, verified by a byte-identical
aggregate hash over all 48 PowerShell files; analyze 13 findings, element for
element the P0-T17 baseline, 0 in owned files; Pester 320 passed, 0 failed, 0
skipped, exit 0, aggregate line coverage 94.49 against a previous 94.43 and
Sync-PackageReferences.ps1 at 82.68 against a previous 81.89. The C# loop was
not re-run: this cycle changed no .cs, .csproj or config file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QaUVgY37zfbsTvSTPd7wsr
Run 36199942860, event pull_request, head f56dabc, all six checks pass.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01Tphos3rkKSiEVLUCuSu8UK
@drmoisan
drmoisan merged commit fb36d9c into main Sep 26, 2026
6 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

1 participant