diff --git a/CHANGELOG.md b/CHANGELOG.md index a96dbe5..e5c0b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`Test-DPLibraryConflict`** — a public cmdlet that reports known-incompatible module combinations loaded in the current session (with the reason, the separate-process workaround, and the issue link) and returns the active conflicts. - **`Import-DPLibrary` now warns** when a known-conflicting module pair is — or later becomes — co-loaded: immediately if both are already imported, otherwise via a best-effort, idempotent, self-unregistering `AssemblyLoad` handler that fires only once the whole pair is co-loaded (armed only when both modules are installed). Advisory: it never throws and is skipped under Constrained Language Mode. -- Data-driven **`knownConflicts`** in `build/dependency-policy.json`, shipped into the module as `KnownConflicts.json` (adding a future conflict is a data edit, not a code change). +- Data-driven conflict list in **`src/DLLPickle/KnownConflicts.json`** — a committed source file shipped verbatim with the module (adding a future conflict is a data edit, not a code change). Because it lives under `src/DLLPickle/`, a conflict-data edit correctly publishes a new release, while policy/CI-only edits do not. - Runtime ALC-ownership probe tooling under `tools/` (`Get-DLLPickleLoadedTrackedAssembly.ps1`; `Get-DLLPickleRuntimeAssemblySnapshot` now sources its filter from `trackedAssemblies`, so it captures the OData stack) used to adjudicate #174. ### Changed diff --git a/build/DLLPickle.Build.ps1 b/build/DLLPickle.Build.ps1 index dc9e848..3e5ab7e 100644 --- a/build/DLLPickle.Build.ps1 +++ b/build/DLLPickle.Build.ps1 @@ -836,16 +836,6 @@ Add-BuildTask CopyModuleFiles -After RestoreDependencies -Before Build { } #CopyModuleFiles -# Synopsis: Ship the policy's knownConflicts list into the module for the runtime conflict warning -Add-BuildTask ExportKnownConflicts -After CopyModuleFiles { - Write-Build Gray ' Exporting knownConflicts to the module output...' - $PolicyPath = Join-Path -Path $script:ProjectRoot -ChildPath 'build/dependency-policy.json' - $OutputPath = Join-Path -Path $script:ModuleOutputPath -ChildPath 'KnownConflicts.json' - & (Join-Path -Path $PSScriptRoot -ChildPath 'Export-DLLPickleKnownConflicts.ps1') -PolicyPath $PolicyPath -OutputPath $OutputPath - Write-Build Gray ' ...knownConflicts exported.' -} - - # Synopsis: Copies module assets to Artifacts folder Add-BuildTask AssetCopy -After CopyModuleFiles -Before Build { Write-Build Gray ' Copying assets to Artifacts...' diff --git a/build/Export-DLLPickleKnownConflicts.ps1 b/build/Export-DLLPickleKnownConflicts.ps1 deleted file mode 100644 index ccc6df4..0000000 --- a/build/Export-DLLPickleKnownConflicts.ps1 +++ /dev/null @@ -1,31 +0,0 @@ -<# -.SYNOPSIS - Extracts the knownConflicts array from the dependency policy into a standalone JSON file shipped - with the module, so the runtime conflict-warning can read it (the full policy is not shipped). -.PARAMETER PolicyPath - Path to dependency-policy.json. -.PARAMETER OutputPath - Path to write the extracted knownConflicts JSON (an array). -#> -[CmdletBinding()] -param( - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [string]$PolicyPath, - - [Parameter(Mandatory)] - [ValidateNotNullOrEmpty()] - [string]$OutputPath -) - -$ErrorActionPreference = 'Stop' - -$Policy = Get-Content -LiteralPath $PolicyPath -Raw | ConvertFrom-Json -$Conflicts = @($Policy.knownConflicts) - -$OutputDirectory = Split-Path -Path $OutputPath -Parent -if ($OutputDirectory -and -not (Test-Path -LiteralPath $OutputDirectory -PathType Container)) { - $null = New-Item -Path $OutputDirectory -ItemType Directory -Force -} - -ConvertTo-Json -InputObject $Conflicts -Depth 20 | Set-Content -LiteralPath $OutputPath -Encoding utf8NoBOM diff --git a/build/dependency-policy.json b/build/dependency-policy.json index 9b9a69c..2239f46 100644 --- a/build/dependency-policy.json +++ b/build/dependency-policy.json @@ -70,22 +70,6 @@ ] ] }, - "knownConflicts": [ - { - "id": "174-odata-azstorage-exo", - "modules": [ "Az.Storage", "ExchangeOnlineManagement" ], - "assembly": "Microsoft.OData.Core", - "issue": "174", - "reason": "Az.Storage force-loads Microsoft.OData.Core 7.6.4 at import; ExchangeOnlineManagement's Get-EXO* cmdlets require 7.22.0. Both target the default ALC and are strong-named, so the two versions cannot coexist in one process - both import orders fail.", - "workaround": "Use Az.Storage and ExchangeOnlineManagement (Get-EXO* cmdlets) in separate PowerShell processes - for example a background job (Start-Job) or a second pwsh. A separate runspace in the same process does NOT help: the conflict is process-wide (one default AssemblyLoadContext per process).", - "evidence": { - "versions": { "Az.Storage": "7.6.4", "ExchangeOnlineManagement": "7.22.0" }, - "alc": "Default", - "runtimeProbe": "2026-06-01 scenarios 1-4: both load OData into the Default ALC; Az.Storage-first -> EXO REF_DEF_MISMATCH (0x80131040); EXO-first -> Az.Storage 'same name already loaded'.", - "decidedOn": "2026-06-01" - } - } - ], "baseline": { "capturedOn": "2026-05-31", "moduleVersions": { @@ -386,7 +370,7 @@ "updateMode": "reportOnly", "reason": "Issue #174 showed that default OData preloading can break Az.Storage when ExchangeOnlineManagement and Az.Storage require incompatible OData identities.", "evidence": { - "basis": "Az.Storage 9.6.1 ships Microsoft.OData.Core 7.6.4; ExchangeOnlineManagement 3.9.2 requires 7.22.0. Both load into the default ALC; no preload version can satisfy both. See knownConflicts 174-odata-azstorage-exo for the runtime adjudication." + "basis": "Az.Storage 9.6.1 ships Microsoft.OData.Core 7.6.4; ExchangeOnlineManagement 3.9.2 requires 7.22.0. Both load into the default ALC; no preload version can satisfy both. See issue #174 and the shipped conflict-data entry '174-odata-azstorage-exo' (src/DLLPickle/KnownConflicts.json) for the runtime adjudication." } }, { @@ -400,7 +384,7 @@ "updateMode": "reportOnly", "reason": "Keep the OData family out of the default preload set unless a future isolation strategy is implemented.", "evidence": { - "basis": "OData family dependency paired with Microsoft.OData.Core; same default-ALC conflict as Core. See knownConflicts 174-odata-azstorage-exo for the runtime adjudication." + "basis": "OData family dependency paired with Microsoft.OData.Core; same default-ALC conflict as Core. See issue #174 and the shipped conflict-data entry '174-odata-azstorage-exo' (src/DLLPickle/KnownConflicts.json) for the runtime adjudication." } }, { @@ -414,7 +398,7 @@ "updateMode": "reportOnly", "reason": "Keep the OData family out of the default preload set unless a future isolation strategy is implemented.", "evidence": { - "basis": "OData family dependency paired with Microsoft.OData.Core; same default-ALC conflict as Core. See knownConflicts 174-odata-azstorage-exo for the runtime adjudication." + "basis": "OData family dependency paired with Microsoft.OData.Core; same default-ALC conflict as Core. See issue #174 and the shipped conflict-data entry '174-odata-azstorage-exo' (src/DLLPickle/KnownConflicts.json) for the runtime adjudication." } } ] diff --git a/docs/superpowers/plans/2026-06-01-issue174-conflict-warning.md b/docs/superpowers/plans/2026-06-01-issue174-conflict-warning.md index 8b0da28..0c9d6d1 100644 --- a/docs/superpowers/plans/2026-06-01-issue174-conflict-warning.md +++ b/docs/superpowers/plans/2026-06-01-issue174-conflict-warning.md @@ -2,6 +2,8 @@ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. +> **Superseded (2026-06-02):** The conflict data was relocated before the 2.2.0 release — it is now a committed source file at `src/DLLPickle/KnownConflicts.json` rather than extracted from `build/dependency-policy.json` at build time. The `Export-DLLPickleKnownConflicts.ps1` extractor, the `ExportKnownConflicts` build task, and the extractor sync test described below no longer exist. See the superseding note in the design doc §3 for the rationale (Codex review of #231). + **Goal:** Warn DLLPickle users about the Az.Storage ↔ ExchangeOnlineManagement OData incompatibility, driven by a data-defined `knownConflicts` list, and record the runtime evidence + workaround. **Architecture:** Conflicts are data in `build/dependency-policy.json`; the build extracts them into a shipped `module/DLLPickle/KnownConflicts.json`; a pure Private detector compares them against the session's loaded modules; a public `Test-DPLibraryConflict` and an `Import-DPLibrary`-armed one-shot `AssemblyLoad` handler surface a `Write-Warning`. The warning never throws. diff --git a/docs/superpowers/specs/2026-06-01-issue174-conflict-warning-design.md b/docs/superpowers/specs/2026-06-01-issue174-conflict-warning-design.md index 2d5ccc0..eb01406 100644 --- a/docs/superpowers/specs/2026-06-01-issue174-conflict-warning-design.md +++ b/docs/superpowers/specs/2026-06-01-issue174-conflict-warning-design.md @@ -41,6 +41,8 @@ This is the single source of truth. The matching `blockedPreloadAssemblies` ODat ## 3. Runtime availability (build ships an extracted subset) +> **Superseded (2026-06-02):** This extraction approach shipped in 2.2.0's PR (#231) but was revised before release in response to Codex review (the policy is a `build/**` file excluded from the release trigger, so a `knownConflicts`-only edit would not have auto-published). The conflict data is now a **committed source file at `src/DLLPickle/KnownConflicts.json`** (the single source of truth), copied into the module verbatim by `CopyModuleFiles` — no extraction step, no sync test, and edits under `src/DLLPickle/` correctly trigger a release. The runtime read path is unchanged (it still reads `KnownConflicts.json` at the module root). The rest of this section describes the original design. + `build/dependency-policy.json` is a build/CI artifact and is **not** shipped in the module. So a build step extracts just `knownConflicts` and writes it to the module output as `module/DLLPickle/KnownConflicts.json` (during `PrepareModuleOutput`, after `CopyModuleFiles`). The runtime reads that shipped file (resolved relative to the module root). A unit test asserts the shipped subset equals the policy's `knownConflicts` (sync guard). Rationale: one source (the policy), small shipped payload (only the conflict list, not the full policy). ## 4. Detection (Private `Test-DPModuleConflict`) diff --git a/src/DLLPickle/KnownConflicts.json b/src/DLLPickle/KnownConflicts.json new file mode 100644 index 0000000..f330b6f --- /dev/null +++ b/src/DLLPickle/KnownConflicts.json @@ -0,0 +1,22 @@ +[ + { + "id": "174-odata-azstorage-exo", + "modules": [ + "Az.Storage", + "ExchangeOnlineManagement" + ], + "assembly": "Microsoft.OData.Core", + "issue": "174", + "reason": "Az.Storage force-loads Microsoft.OData.Core 7.6.4 at import; ExchangeOnlineManagement's Get-EXO* cmdlets require 7.22.0. Both target the default ALC and are strong-named, so the two versions cannot coexist in one process - both import orders fail.", + "workaround": "Use Az.Storage and ExchangeOnlineManagement (Get-EXO* cmdlets) in separate PowerShell processes - for example a background job (Start-Job) or a second pwsh. A separate runspace in the same process does NOT help: the conflict is process-wide (one default AssemblyLoadContext per process).", + "evidence": { + "versions": { + "Az.Storage": "7.6.4", + "ExchangeOnlineManagement": "7.22.0" + }, + "alc": "Default", + "runtimeProbe": "2026-06-01 scenarios 1-4: both load OData into the Default ALC; Az.Storage-first -> EXO REF_DEF_MISMATCH (0x80131040); EXO-first -> Az.Storage 'same name already loaded'.", + "decidedOn": "2026-06-01" + } + } +] diff --git a/tests/Unit/KnownConflicts.Tests.ps1 b/tests/Unit/KnownConflicts.Tests.ps1 index 0114c31..891be1b 100644 --- a/tests/Unit/KnownConflicts.Tests.ps1 +++ b/tests/Unit/KnownConflicts.Tests.ps1 @@ -1,7 +1,5 @@ BeforeAll { $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path - $ExportScript = Join-Path $RepoRoot 'build\Export-DLLPickleKnownConflicts.ps1' - $PolicyPath = Join-Path $RepoRoot 'build\dependency-policy.json' . (Join-Path $RepoRoot 'src\DLLPickle\Private\Test-DPModuleConflict.ps1') . (Join-Path $RepoRoot 'src\DLLPickle\Private\Get-DPKnownConflict.ps1') . (Join-Path $RepoRoot 'src\DLLPickle\Private\Format-DPConflictWarning.ps1') @@ -63,15 +61,31 @@ Describe 'Get-DPKnownConflict' -Tag 'Unit' { } } -Describe 'Export-DLLPickleKnownConflicts' -Tag 'Unit' { - It 'writes the policy knownConflicts array to the output file verbatim' { - $Out = Join-Path $TestDrive 'KnownConflicts.json' - & $ExportScript -PolicyPath $PolicyPath -OutputPath $Out - Test-Path -LiteralPath $Out | Should -BeTrue - $Written = Get-Content -LiteralPath $Out -Raw | ConvertFrom-Json - $Policy = Get-Content -LiteralPath $PolicyPath -Raw | ConvertFrom-Json - @($Written).Count | Should -Be @($Policy.knownConflicts).Count - ($Written | Where-Object id -EQ '174-odata-azstorage-exo') | Should -Not -BeNullOrEmpty +Describe 'Shipped KnownConflicts.json source' -Tag 'Unit' { + # The conflict data is a committed source file under src/DLLPickle (the single source of truth), + # copied into the module verbatim by the build. No build-time extraction step to validate anymore. + BeforeAll { + $KnownConflictsPath = Join-Path $RepoRoot 'src\DLLPickle\KnownConflicts.json' + } + + It 'exists as a committed source file under src/DLLPickle' { + Test-Path -LiteralPath $KnownConflictsPath -PathType Leaf | Should -BeTrue + } + + It 'is a non-empty JSON array of conflict entries' { + $Parsed = Get-Content -LiteralPath $KnownConflictsPath -Raw | ConvertFrom-Json + @($Parsed).Count | Should -BeGreaterThan 0 + } + + It 'contains the #174 Az.Storage + ExchangeOnlineManagement entry with the required fields' { + $Conflicts = Get-DPKnownConflict -Path $KnownConflictsPath + $Odata = $Conflicts | Where-Object id -EQ '174-odata-azstorage-exo' + $Odata | Should -Not -BeNullOrEmpty + @($Odata.modules) | Should -Contain 'Az.Storage' + @($Odata.modules) | Should -Contain 'ExchangeOnlineManagement' + $Odata.issue | Should -Be '174' + $Odata.reason | Should -Not -BeNullOrEmpty + $Odata.workaround | Should -Not -BeNullOrEmpty } }