Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 0 additions & 10 deletions build/DLLPickle.Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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...'
Expand Down
31 changes: 0 additions & 31 deletions build/Export-DLLPickleKnownConflicts.ps1

This file was deleted.

22 changes: 3 additions & 19 deletions build/dependency-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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."
}
},
{
Expand All @@ -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."
}
},
{
Expand All @@ -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."
}
}
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
22 changes: 22 additions & 0 deletions src/DLLPickle/KnownConflicts.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
]
36 changes: 25 additions & 11 deletions tests/Unit/KnownConflicts.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -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
}
}

Expand Down
Loading