[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-dotnet-multithreaded-task-migration":3,"mdc--1fj35e-key":39,"related-repo-dotnet-multithreaded-task-migration":4239,"related-org-dotnet-multithreaded-task-migration":4247},{"slug":4,"name":4,"fn":5,"description":6,"org":7,"tags":11,"stars":22,"repoUrl":23,"updatedAt":24,"license":25,"forks":26,"topics":27,"repo":34,"sourceUrl":37,"mdContent":38},"multithreaded-task-migration","migrate MSBuild tasks to multithreaded mode","Guide for migrating MSBuild tasks to multithreaded mode support, including compatibility red-team review. Use this when converting tasks to thread-safe versions, implementing IMultiThreadableTask, adding TaskEnvironment support, or auditing migrations for behavioral compatibility.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},"dotnet",".NET (Microsoft)","https:\u002F\u002Fpexgzepcugksgbtrxkhf.supabase.co\u002Fstorage\u002Fv1\u002Fobject\u002Fpublic\u002Forg-logos\u002Fdotnet.png",[12,16,19],{"name":13,"slug":14,"type":15},"Performance","performance","tag",{"name":17,"slug":18,"type":15},".NET","net",{"name":20,"slug":21,"type":15},"Engineering","engineering",5535,"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild","2026-07-22T05:37:33.965588",null,1436,[28,29,30,31,32,33],"build","hacktoberfest","help-wanted","microsoft","msbuild","visual-studio",{"repoUrl":23,"stars":22,"forks":26,"topics":35,"description":36},[28,29,30,31,32,33],"The Microsoft Build Engine (MSBuild) is the build platform for .NET and Visual Studio.","https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Ftree\u002FHEAD\u002Fplugins\u002Fmt-migration\u002Fskills\u002Fmultithreaded-task-migration","---\nname: multithreaded-task-migration\ndescription: Guide for migrating MSBuild tasks to multithreaded mode support, including compatibility red-team review. Use this when converting tasks to thread-safe versions, implementing IMultiThreadableTask, adding TaskEnvironment support, or auditing migrations for behavioral compatibility.\n---\n\n# Migrating MSBuild Tasks to Multithreaded API\n\nMSBuild's multithreaded execution model requires tasks to avoid global process state (working directory, environment variables). Thread-safe tasks declare this capability via `MSBuildMultiThreadableTask` and use `TaskEnvironment` from `IMultiThreadableTask` for safe alternatives.\n\n## Migration Steps\n\n### Step 1: Update Task Class Declaration\n\na. Ensure the task implementing class is decorated with the `MSBuildMultiThreadableTask` attribute.\nb. Implement `IMultiThreadableTask` **only if** the task needs `TaskEnvironment` APIs (path absolutization, env vars, process start). If the task has no file\u002Fenvironment operations (e.g., a stub class), the attribute alone is sufficient.\n\n```csharp\n[MSBuildMultiThreadableTask]\npublic class MyTask : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    ...\n}\n```\n\n**Note**: `[MSBuildMultiThreadableTask]` has `Inherited = false` — it must be on each concrete class, not just the base.\n\n### Step 2: Absolutize Paths Before File Operations\n\nAll path strings must be absolutized with `TaskEnvironment.GetAbsolutePath()` before use in file system APIs. This resolves paths relative to the project directory, not the process working directory.\n\n```csharp\nAbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath);\nif (File.Exists(absolutePath))\n{\n    string content = File.ReadAllText(absolutePath);\n}\n```\n\nThe [`AbsolutePath`](https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Fblob\u002Fmain\u002Fsrc\u002FFramework\u002FPathHelpers\u002FAbsolutePath.cs) struct:\n- `Value` — the absolute path string\n- `OriginalValue` — preserves the input path (use for error messages and `[Output]` properties)\n- Implicitly convertible to `string` for File\u002FDirectory API compatibility\n- `GetCanonicalForm()` — resolves `..` segments and normalizes separators (see [Sin 5](#sin-5-canonicalization-mismatch))\n\n**CAUTION**: `GetAbsolutePath()` throws `ArgumentException` for null\u002Fempty inputs. See [Sin 3](#sin-3-null-coalescing-that-changes-control-flow) and [Sin 6](#sin-6-exception-type-change) for compatibility implications.\n\n### Step 3: Replace Environment Variable APIs\n\n| BEFORE (UNSAFE)                                  | AFTER (SAFE)                                       |\n|--------------------------------------------------|----------------------------------------------------|\n| `Environment.GetEnvironmentVariable(\"VAR\");`       | `TaskEnvironment.GetEnvironmentVariable(\"VAR\");`     |\n| `Environment.SetEnvironmentVariable(\"VAR\", \"v\");`  | `TaskEnvironment.SetEnvironmentVariable(\"VAR\", \"v\");` |\n\n### Step 4: Replace Process Start APIs\n\n| BEFORE (UNSAFE - inherits process state)           | AFTER (SAFE - uses task's isolated environment)     |\n|----------------------------------------------------|-----------------------------------------------------|\n| `var psi = new ProcessStartInfo(\"tool.exe\");`      | `var psi = TaskEnvironment.GetProcessStartInfo();`  |\n|                                                    | `psi.FileName = GetFullPathToTool(); \u002F\u002F must be absolute` |\n\n## Updating Unit Tests\n\nBuilt-in MSBuild tasks now initialize `TaskEnvironment` with a `MultiProcessTaskEnvironmentDriver`-backed default. Tests creating instances of built-in tasks no longer need manual `TaskEnvironment` setup. For custom or third-party tasks that implement `IMultiThreadableTask` without a default initializer, set `TaskEnvironment = TaskEnvironment.Fallback` (or use `TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(path)` to point at a specific project directory).\n\n## APIs to Avoid\n\n| Category | APIs | Alternative |\n|---|---|---|\n| **Forbidden** | `Environment.Exit`, `FailFast`, `Process.Kill`, `ThreadPool.SetMin\u002FMaxThreads`, `Console.*` | Return false, throw, or use `Log` |\n| **Use TaskEnvironment** | `Environment.CurrentDirectory`, `Get\u002FSetEnvironmentVariable`, `Path.GetFullPath`, `ProcessStartInfo` | See Steps 2-4 |\n| **Need absolute paths** | `File.*`, `Directory.*`, `FileInfo`, `DirectoryInfo`, `FileStream`, `StreamReader\u002FWriter` | Absolutize first (File System APIs) |\n| **Review required** | `Assembly.Load*`, `Activator.CreateInstance*` | Check for version conflicts |\n\n## Practical Notes\n\n### CRITICAL: Trace All Path String Usage\n\nTrace every path string through all method calls and assignments to find all places it flows into file system operations — including helper methods that may internally use File System APIs.\n\n1. Find every path string (e.g., `item.ItemSpec`, function parameters)\n2. Trace downstream through all method calls\n3. Absolutize BEFORE any code path that touches the file system\n4. Use `OriginalValue` for user-facing output (logs, errors) — see [Sin 2](#sin-2-error-message-path-inflation)\n\n### Exception Handling in Batch Operations\n\nIn batch processing (iterating over files), `GetAbsolutePath()` throwing on one bad path aborts the entire batch. Match the original task's error semantics:\n\n```csharp\nbool success = true;\nforeach (ITaskItem item in SourceFiles)\n{\n    try\n    {\n        AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec);\n        ProcessFile(path);\n    }\n    catch (ArgumentException ex)\n    {\n        Log.LogError(\"Invalid path '{0}': {1}\", item.ItemSpec, ex.Message);\n        success = false;\n    }\n}\nreturn success;\n```\n\n### Prefer AbsolutePath Over String\n\nStay in the `AbsolutePath` world — it's implicitly convertible to `string` where needed. Avoid round-tripping through `string` and back.\n\n### TaskEnvironment is Not Thread-Safe\n\nIf your task spawns multiple threads internally, synchronize access to `TaskEnvironment`. Each task *instance* gets its own environment, so no synchronization between tasks is needed.\n\n### API Discipline When Plumbing Through Helpers\n\nWhen the migration ripples into shared helpers:\n\n- **Helper signatures: take `AbsolutePath`, not `(string path, string pathForMessages)`**. Two-string signatures drift apart; the caller passing one `AbsolutePath` keeps `.Value` and `.OriginalValue` in lockstep.\n- **Repo-wide helpers belong in an extensions class** (e.g., `TaskEnvironmentExtensions`), not on the task. If a per-task helper looks generic, extract it.\n- **Don't condition behavior on MT-mode**. `TaskEnvironment.Fallback` already gives single-process semantics; `if (mtMode) { … } else { … }` doubles the maintenance surface and skips test coverage of one branch.\n- **Don't silently swallow `ArgumentException` from `GetAbsolutePath`** in a helper — log a diagnostic so customers can debug bad inputs.\n\n## References\n\n- [Thread-Safe Tasks Spec](https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Fblob\u002Fmain\u002Fdocumentation\u002Fspecs\u002Fmultithreading\u002Fthread-safe-tasks.md)\n- [`AbsolutePath`](https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Fblob\u002Fmain\u002Fsrc\u002FFramework\u002FPathHelpers\u002FAbsolutePath.cs)\n- [`TaskEnvironment`](https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Fblob\u002Fmain\u002Fsrc\u002FFramework\u002FTaskEnvironment.cs)\n- [`IMultiThreadableTask`](https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Fblob\u002Fmain\u002Fsrc\u002FFramework\u002FIMultiThreadableTask.cs)\n\n---\n\n# Compatibility Red-Team Playbook\n\nAfter migration, review for behavioral compatibility. **Every observable difference is a bug until proven otherwise.**\n\nObservable behavior = `Execute()` return value, `[Output]` property values, error\u002Fwarning message content, exception types, files written, and which code path runs.\n\n## The 7 Deadly Compatibility Sins\n\nReal bugs found during MSBuild task migrations. Every one shipped in initial \"passing\" code with green tests.\n\n**Edge-case discipline:** For every migrated code path, verify behavior when inputs are `null`, empty string (`\"\"`), or whitespace-only. `GetAbsolutePath` throws `ArgumentException` on null\u002Fempty — if the pre-migration code handled these differently (e.g., returned early, used a default, or threw a different exception type), the migration must preserve that behavior. Even if a scenario seems unlikely, treat it as a relevant finding if it is theoretically possible.\n\n### Sin 1: Output Property Contamination\n\nAbsolutized values leak into `[Output]` properties that users\u002Fother tasks consume.\n\n```csharp\n\u002F\u002F BROKEN: ManifestPath was \"bin\\Release\\app.manifest\", now \"C:\\repo\\bin\\Release\\app.manifest\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, name));\nManifestPath = abs; \u002F\u002F implicit string conversion!\n\n\u002F\u002F CORRECT: separate original form from absolutized path\nstring originalPath = Path.Combine(OutputDirectory, name);\nAbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(originalPath);\nManifestPath = originalPath;          \u002F\u002F [Output]: original form\ndocument.Save((string)outputPath);    \u002F\u002F file I\u002FO: absolute path\n```\n\n**Detect**: For every `[Output]` property, trace backward — is it ever assigned from an `AbsolutePath`?\n\n### Sin 2: Error\u002FLog Message Path Inflation\n\nError messages and exception messages show absolutized paths instead of the user's original input.\n\n**Direct leakage** — passing an `AbsolutePath` to logging APIs:\n\n```csharp\n\u002F\u002F BROKEN: \"Cannot find 'C:\\repo\\app.manifest'\" instead of \"Cannot find 'app.manifest'\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(path);\nLog.LogError(\"Cannot find '{0}'\", abs); \u002F\u002F implicit conversion!\n\n\u002F\u002F CORRECT: use OriginalValue\nLog.LogError(\"Cannot find '{0}'\", abs.OriginalValue);\n```\n\n**Indirect leakage** — exception messages from helpers that received the absolutized path:\n\n```csharp\n\u002F\u002F BROKEN: ex.FileName \u002F ex.Message embed the absolutized path\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", ex.FileName); }\ncatch (Exception ex)              { Log.LogError(ex.Message); }\n\n\u002F\u002F CORRECT: prefer the original input; if you must use the exception, sanitize\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", abs.OriginalValue); }\ncatch (Exception ex)              { Log.LogError(ex.Message.Replace(abs.Value, abs.OriginalValue)); }\n```\n\n**Detect**: Search every `Log.LogError`\u002F`LogWarning`\u002F`LogMessage` — is any argument an `AbsolutePath`? Also check every `Log.LogError(ex.Message …)` \u002F `ex.FileName` downstream of a `GetAbsolutePath` — did the exception originate from a helper that received the absolutized path?\n\n### Sin 3: Null Coalescing That Changes Control Flow\n\nAdding `?? \"\"` silently swallows an exception the old code relied on for error handling.\n\n```csharp\n\u002F\u002F BEFORE: Path.GetDirectoryName(\"C:\\\") → null → Path.Combine(null, x) → ArgumentNullException\n\u002F\u002F   → task fails with an exception \u002F error logged → Execute() returns false\n\n\u002F\u002F BROKEN: ?? \"\" added \"for safety\"\nstring dir = Path.GetDirectoryName(fileName) ?? string.Empty;\n\u002F\u002F Path.Combine(\"\", x) succeeds silently → no error → Execute() returns TRUE!\n```\n\n**Detect**: For every `??` you added, ask: \"What happened when this was null before?\" If it threw and was caught → your `??` is a bug.\n\n### Sin 4: Try-Catch Scope Mismatch\n\n`GetAbsolutePath()` inside a try block leaves the absolutized value out of scope in the catch block. Helper methods in the catch (like `LockCheck`) then use the original non-absolute path.\n\n```csharp\n\u002F\u002F CORRECT: hoist above try so catch can use it too\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(OutputManifest.ItemSpec);\ntry {\n    WriteFile(abs);\n} catch (Exception ex) {\n    string lockMsg = LockCheck.GetLockedFileMessage(abs);        \u002F\u002F absolute → correct file\n    Log.LogError(\"Failed: {0}\", OutputManifest.ItemSpec, ...);   \u002F\u002F original → user-friendly\n}\n```\n\n**Detect**: For every `GetAbsolutePath` inside a try, check if the catch block needs the absolutized value.\n\n### Sin 5: Canonicalization Mismatch\n\n`GetAbsolutePath` does NOT canonicalize. `Path.GetFullPath` does TWO things: absolutize AND canonicalize (`..` resolution, separator normalization). If the old code used `Path.GetFullPath` for dictionary keys, comparisons, or display, you must add `.GetCanonicalForm()`:\n\n```csharp\n\u002F\u002F GetAbsolutePath(\"foo\u002F..\u002Fbar\")  → \"C:\\repo\\foo\u002F..\u002Fbar\"  (NOT canonical)\n\u002F\u002F Path.GetFullPath(\"foo\u002F..\u002Fbar\") → \"C:\\repo\\bar\"         (canonical)\n\n\u002F\u002F BROKEN for dictionary keys — \"C:\\repo\\foo\\..\\bar\" ≠ \"C:\\repo\\bar\"\nvar map = items.ToDictionary(p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec), ...);\n\n\u002F\u002F CORRECT\nvar map = items.ToDictionary(\n    p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec).GetCanonicalForm(),\n    StringComparer.OrdinalIgnoreCase);\n```\n\n**Detect**: Find every `Dictionary`\u002F`HashSet`\u002F`ToDictionary` using path keys, and every place the old code called `Path.GetFullPath`. If canonicalization mattered, add `.GetCanonicalForm()`.\n\n### Sin 6: Exception Type Change\n\nOld code threw `FileNotFoundException` for missing files; new code throws `ArgumentException` from `GetAbsolutePath(\"\")` before reaching the file check. Custom catch blocks filtering by exception type may be bypassed. (`ExceptionHandling.IsIoRelatedException` catches `ArgumentException`, but task-specific handlers might not.)\n\n**Detect**: For every `GetAbsolutePath`, check what the old code threw for null\u002Fempty and whether the calling code has type-specific catch blocks.\n\n### Sin 7: `Path.IsPathRooted` as an Absolutize Gate\n\n`Path.IsPathRooted` returns `true` for **drive-relative** (`C:foo\\bar`) and **root-relative** (`\\foo\\bar`) Windows paths — both still depend on process current-directory \u002F current-drive state. \"Absolutize only if not rooted\" silently leaves those paths process-dependent.\n\n`GetAbsolutePath` correctly handles all path forms — including the Windows edge cases (`C:foo`, `\\foo`) that `Path.IsPathRooted` considers \"rooted\" but that are still CWD\u002Fdrive-dependent. Call it unconditionally; remove any `IsPathRooted` short-circuit.\n\n---\n\n## Call-Chain Hazards Beyond the Task\n\nThe 7 sins above are *local* to the task body. The other half of MT migration is auditing the **transitive call chain** — helpers and shared utility classes the task reaches into.\n\n### Helpers That Capture Process State\n\nHelpers reached from `Execute()` can quietly depend on process state in any of these ways:\n\n- A relative path falls through to `Directory.GetCurrentDirectory()` (often as a \"fallback\") or to `Path.GetFullPath(x)` without a base.\n- An env var is read directly via `Environment.GetEnvironmentVariable` (not `TaskEnvironment`).\n- A `static` field is seeded from process state on first use (`static string s_x = Directory.GetCurrentDirectory()`), permanently capturing the first caller's environment for every later caller.\n- A BCL API echoes its input path back through `ex.Message` \u002F `ex.FileName` (Sin 2).\n- The helper takes a `string path` and returns a `string` — losing the `.OriginalValue` distinction, so callers either lie in messages or absolutize twice.\n\n**Resolution patterns:**\n\n1. Add an MT-aware overload that takes `TaskEnvironment` (or an `AbsolutePath` \u002F explicit fallback path). Legacy overload delegates with `TaskEnvironment.Fallback`. Don't branch on \"MT mode on\u002Foff\".\n2. Promote the parameter type to `AbsolutePath` so `.Value` and `.OriginalValue` travel together.\n3. Replace process-state-seeded static caches with `ConcurrentDictionary\u003CTKey, TValue>` keyed on the inputs that determine uniqueness — never on process state.\n4. For cross-repo helpers (foundation types used by many tasks), migrate the foundation in a dedicated PR before the tasks that consume it; the hydration step is usually where `Path.GetFullPath` is hiding.\n\n### Tasks Instantiated Directly by Other Tasks\n\n`[MSBuildMultiThreadableTask]` is only honored when the TaskFactory creates the task (i.e., a target invokes it as `\u003CMyTask … \u002F>`). A task created via `new MyTask()` inside another task's body bypasses the factory and **never gets `TaskEnvironment` injected** — it silently defaults to `TaskEnvironment.Fallback` (= process CWD).\n\nMigrating a nested task in isolation is therefore meaningless. Either migrate the parent and have it explicitly propagate its `TaskEnvironment` to the nested instance before calling `Execute()`, or restructure so the nested task is a regular TaskFactory-created task.\n\n**Detect:** grep for `new …Task()` followed by `.Execute()` — every direct instantiation is a hole.\n\n### ToolTask Override Hot Spots\n\n`ToolTask` subclasses inherit virtual methods that run *before or after* `Execute()` and frequently touch the file system. Audit every override:\n\n| Override | Hazard | Migration |\n|---|---|---|\n| `GenerateFullPathToTool()` | Builds `ProcessStartInfo.FileName` — relative tool path → wrong tool launched | Absolutize the tool path before returning |\n| `SkipTaskExecution()` | Up-to-date check on input\u002Foutput timestamps using relative paths | Absolutize both sides of the comparison |\n| `ValidateParameters()` | `File.Exists` on input parameters | Absolutize before probing |\n| `GenerateResponseFileCommands()` \u002F `GenerateCommandLineCommands()` | Tempting to absolutize args | **Don't** — the child's `WorkingDirectory` is the project dir, so relative args resolve correctly; absolutizing inflates user-visible output and can leak into tool diagnostics or generated artifacts |\n| `GetWorkingDirectory()` | Default `null` → child inherits host CWD | Leave alone if you use `TaskEnvironment.GetProcessStartInfo()`; it already sets `WorkingDirectory` |\n\n**Key contract:** `ProcessStartInfo.FileName` is resolved by the OS **before** `WorkingDirectory` takes effect, so the executable path must be absolute. Tool arguments are interpreted by the child process in its working directory, so they should stay relative.\n\n### Engine-Owned Environment Variables Are Immutable in MT Mode\n\nEnv vars consumed by the engine *before* tasks run (e.g., `MSBUILD*`-prefixed flags and the framework\u002FSDK discovery variables) are snapshotted into engine caches at startup. Tasks that mutate them later have no observable effect on the engine — but the mutation appears to succeed locally, masking bugs.\n\nIf a migrated task previously mutated such a variable to influence a downstream tool, re-architect to pass the value via `ProcessStartInfo.EnvironmentVariables` on the child process instead of mutating the parent's environment.\n\n---\n\n## Test Patterns for MT Migrations\n\nA migration test must **fail when the migration is undone**. Tests that pass identically against pre- and post-migration code are theater. The two patterns below are the ones that reliably exercise MT-specific behavior.\n\n### Pattern A: Decoy-CWD Test (for tasks that touch the file system)\n\nSet the process working directory to a **decoy** dir with no relevant files; set `TaskEnvironment.ProjectDirectory` to a different dir with the inputs\u002Fexpected outputs. The task must read\u002Fwrite against the project directory, not the decoy.\n\n```csharp\nusing TestEnvironment env = TestEnvironment.Create();\nTransientTestFolder projectDir = env.CreateFolder();\nTransientTestFolder decoyCwd = env.CreateFolder();\nFile.WriteAllText(Path.Combine(projectDir.Path, \"input.txt\"), \"expected\");\n\nenv.SetCurrentDirectory(decoyCwd.Path); \u002F\u002F auto-restored when env is disposed\n\nvar task = new MyTask\n{\n    TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path),\n    Input = \"input.txt\", \u002F\u002F relative\n};\ntask.Execute().ShouldBeTrue();\n```\n\n**CRITICAL:** `Directory.SetCurrentDirectory` is **process-global**. xUnit parallelizes tests within a class by default — a CWD-mutating test will flake unless pinned to a non-parallel collection (`[Collection]` + `[CollectionDefinition(DisableParallelization = true)]`). The `IDisposable` restore protects sequential safety but not concurrent siblings.\n\n### Pattern B: Cross-Instance Independence (for tasks doing relative-path resolution)\n\nTwo task instances, two `TaskEnvironment.ProjectDirectory` values, same relative input. Assert each task's output is rooted to its own project directory — proving resolution is per-instance, not shared.\n\n### When NOT to Write a \"Migration Test\"\n\nAttribute-only migrations (just `[MSBuildMultiThreadableTask]`, no `IMultiThreadableTask`) on tasks whose `Execute()` body contains no file\u002Fenv\u002Fprocess\u002Fstatic-state interactions have nothing to test — a decoy-CWD test would pass whether the attribute is present or not. **Don't write the test.** Document the call-chain audit conclusion in the PR description instead.\n\n### Test Hygiene\n\n- Use `TestEnvironment` \u002F `TransientTestFolder` for temp dirs; never `Path.GetTempPath() + Guid.NewGuid()` with manual `try\u002Ffinally`.\n- Hard-code expected assertion values; don't recompute them from `Path.Combine(...)` in the assertion — when the test fails you want to know what was actually wrong.\n\n---\n\n## Red-Team Audit Protocol\n\n### Phase 1: Trace Every Changed Line\n\nFor each modified line: What was the exact runtime value before? After? Where does it flow (outputs, logs, file paths, dictionary keys)? Does each destination produce identical observable behavior?\n\n### Phase 2: Null\u002FEmpty\u002FEdge Input Analysis\n\n| Input | `GetAbsolutePath` | Old behavior | Match? |\n|---|---|---|---|\n| `null` | `ArgumentException` | Varies | ❓ |\n| `\"\"` | `ArgumentException` | Varies | ❓ |\n| `\"C:\\\"` (root) | Valid | Valid | ✅ usually |\n| `\".\"` | `\"C:\\repo\\.\"` (not canonical) | `\"C:\\repo\"` if GetFullPath | ❌ maybe |\n| `\"foo\\..\\bar\"` | `\"C:\\repo\\foo\\..\\bar\"` | `\"C:\\repo\\bar\"` if GetFullPath | ❌ maybe |\n| Already absolute | Pass-through | Pass-through | ✅ |\n\n`Path.GetDirectoryName` → `Path.Combine` chains:\n| Input | `GetDirectoryName` returns | `Path.Combine(result, x)` |\n|---|---|---|\n| `\"C:\\\"` | `null` | Throws `ArgumentNullException` |\n| `\"\"` | `\"\"` (.NET Fx) \u002F `null` (.NET Core+) | Works \u002F Throws ⚠️ |\n| `\"file.resx\"` (no dir) | `\"\"` | Works |\n\nVerify behavior on **both** .NET Framework and .NET TFMs.\n\n### Phase 3: Downstream Impact\n\n1. **Output properties**: What consumes this task's `[Output]`? Does it compare, display, or use as a path?\n2. **Written files**: Does file content change? (e.g., XML with embedded paths)\n3. **Helper methods**: Do `LockCheck`, `ManifestWriter`, etc. internally resolve relative paths?\n4. **Error codes**: MSBuild error codes (MSBxxxx) must be identical.\n\n### Phase 4: Concurrency\n\n1. Two tasks with different `ProjectDirectory` values don't interfere\n2. No writes to static fields (shared across threads)\n3. All file operations use absolutized paths\n\n## Compatibility Test Matrix\n\n```\n[Task] × [Input Type] × [Assertion]\n\nInputs: relative path, absolute path, null, empty, \"..\" segments, root \"C:\\\",\n        forward slashes, trailing separator, UNC path, 260+ char path\n\nAssertions: Execute() return value, [Output] exact string, error message content,\n            exception type, file location, file content\n```\n\n## Sign-Off Checklist\n\n- [ ] `[MSBuildMultiThreadableTask]` on every concrete class (not just base — `Inherited=false`)\n- [ ] `IMultiThreadableTask` on classes that use `TaskEnvironment` APIs, with default initializer `= TaskEnvironment.Fallback`\n- [ ] Every `[Output]` property: exact string value matches pre-migration (Sin 1)\n- [ ] Every `Log.LogError`\u002F`LogWarning`: path in message matches pre-migration (use `OriginalValue`) (Sin 2)\n- [ ] Every `Log.LogError(ex.Message …)` \u002F `ex.FileName`: exception path sanitized to original input (Sin 2)\n- [ ] Every `GetAbsolutePath` call: null\u002Fempty exception behavior matches old code path (Sin 3, 6)\n- [ ] Every dictionary\u002Fset with path keys: canonicalization preserved (`GetCanonicalForm()`) (Sin 5)\n- [ ] Every try-catch: absolutized value available in catch block where needed (Sin 4)\n- [ ] Every `??` or `?.` added: verified it doesn't swallow a previously-thrown exception (Sin 3)\n- [ ] No `Path.IsPathRooted` short-circuits around `GetAbsolutePath` — call unconditionally (Sin 7)\n- [ ] No `AbsolutePath` leaks into user-visible strings unintentionally\n- [ ] No behavior conditioned on \"MT mode on\u002Foff\" — `TaskEnvironment.Fallback` handles single-process case\n- [ ] **Call chain traced end-to-end** for every helper invoked from `Execute()`:\n    - No `Environment.CurrentDirectory` \u002F `Directory.GetCurrentDirectory()` \u002F `Path.GetFullPath(x)` without a base anywhere in the transitive call graph\n    - No direct `Environment.Get\u002FSetEnvironmentVariable` (route through `TaskEnvironment`)\n    - No `static` mutable fields seeded from process state; replace with `ConcurrentDictionary` keyed on inputs\n    - No `Console.*`, `Environment.Exit`, `Process.Kill`, `FailFast`\n- [ ] ToolTask overrides audited (`GenerateFullPathToTool`, `SkipTaskExecution`, `ValidateParameters`); `ProcessStartInfo.FileName` is absolute, tool *arguments* stay relative\n- [ ] No nested tasks created via `new …Task()` without explicit `TaskEnvironment` propagation\n- [ ] No mutations of engine-owned env vars (e.g. `MSBUILD*` and the framework\u002FSDK discovery vars) in MT mode\n- [ ] Tests for custom tasks set `TaskEnvironment = TaskEnvironmentHelper.CreateForTest()` (built-in tasks have a default)\n- [ ] Migration test follows Pattern A (decoy-CWD) **or** Pattern B (cross-instance independence), or PR explains why no test is meaningful\n- [ ] CWD-mutating tests pinned to a non-parallel xUnit collection\n- [ ] Cross-framework: tested on .NET Framework and .NET TFMs.\n",{"data":40,"body":41},{"name":4,"description":6},{"type":42,"children":43},"root",[44,53,84,91,98,132,199,225,231,244,289,309,384,425,431,501,507,565,571,621,627,852,858,864,869,914,920,932,1064,1070,1096,1102,1122,1128,1133,1247,1253,1304,1308,1314,1324,1344,1350,1355,1394,1400,1412,1492,1516,1522,1527,1544,1598,1608,1670,1732,1738,1751,1805,1828,1834,1852,1922,1937,1943,1982,2067,2112,2118,2160,2175,2189,2236,2276,2279,2285,2304,2310,2322,2433,2441,2520,2526,2573,2592,2618,2624,2648,2839,2868,2874,2893,2906,2909,2915,2927,2933,2953,3061,3109,3115,3127,3133,3166,3172,3222,3225,3231,3237,3242,3248,3463,3482,3608,3620,3626,3690,3696,3722,3728,3738,3744,4233],{"type":45,"tag":46,"props":47,"children":49},"element","h1",{"id":48},"migrating-msbuild-tasks-to-multithreaded-api",[50],{"type":51,"value":52},"text","Migrating MSBuild Tasks to Multithreaded API",{"type":45,"tag":54,"props":55,"children":56},"p",{},[57,59,66,68,74,76,82],{"type":51,"value":58},"MSBuild's multithreaded execution model requires tasks to avoid global process state (working directory, environment variables). Thread-safe tasks declare this capability via ",{"type":45,"tag":60,"props":61,"children":63},"code",{"className":62},[],[64],{"type":51,"value":65},"MSBuildMultiThreadableTask",{"type":51,"value":67}," and use ",{"type":45,"tag":60,"props":69,"children":71},{"className":70},[],[72],{"type":51,"value":73},"TaskEnvironment",{"type":51,"value":75}," from ",{"type":45,"tag":60,"props":77,"children":79},{"className":78},[],[80],{"type":51,"value":81},"IMultiThreadableTask",{"type":51,"value":83}," for safe alternatives.",{"type":45,"tag":85,"props":86,"children":88},"h2",{"id":87},"migration-steps",[89],{"type":51,"value":90},"Migration Steps",{"type":45,"tag":92,"props":93,"children":95},"h3",{"id":94},"step-1-update-task-class-declaration",[96],{"type":51,"value":97},"Step 1: Update Task Class Declaration",{"type":45,"tag":54,"props":99,"children":100},{},[101,103,108,110,115,117,123,125,130],{"type":51,"value":102},"a. Ensure the task implementing class is decorated with the ",{"type":45,"tag":60,"props":104,"children":106},{"className":105},[],[107],{"type":51,"value":65},{"type":51,"value":109}," attribute.\nb. Implement ",{"type":45,"tag":60,"props":111,"children":113},{"className":112},[],[114],{"type":51,"value":81},{"type":51,"value":116}," ",{"type":45,"tag":118,"props":119,"children":120},"strong",{},[121],{"type":51,"value":122},"only if",{"type":51,"value":124}," the task needs ",{"type":45,"tag":60,"props":126,"children":128},{"className":127},[],[129],{"type":51,"value":73},{"type":51,"value":131}," APIs (path absolutization, env vars, process start). If the task has no file\u002Fenvironment operations (e.g., a stub class), the attribute alone is sufficient.",{"type":45,"tag":133,"props":134,"children":139},"pre",{"className":135,"code":136,"language":137,"meta":138,"style":138},"language-csharp shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","[MSBuildMultiThreadableTask]\npublic class MyTask : Task, IMultiThreadableTask\n{\n    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n    ...\n}\n","csharp","",[140],{"type":45,"tag":60,"props":141,"children":142},{"__ignoreMap":138},[143,154,163,172,181,190],{"type":45,"tag":144,"props":145,"children":148},"span",{"class":146,"line":147},"line",1,[149],{"type":45,"tag":144,"props":150,"children":151},{},[152],{"type":51,"value":153},"[MSBuildMultiThreadableTask]\n",{"type":45,"tag":144,"props":155,"children":157},{"class":146,"line":156},2,[158],{"type":45,"tag":144,"props":159,"children":160},{},[161],{"type":51,"value":162},"public class MyTask : Task, IMultiThreadableTask\n",{"type":45,"tag":144,"props":164,"children":166},{"class":146,"line":165},3,[167],{"type":45,"tag":144,"props":168,"children":169},{},[170],{"type":51,"value":171},"{\n",{"type":45,"tag":144,"props":173,"children":175},{"class":146,"line":174},4,[176],{"type":45,"tag":144,"props":177,"children":178},{},[179],{"type":51,"value":180},"    public TaskEnvironment TaskEnvironment { get; set; } = TaskEnvironment.Fallback;\n",{"type":45,"tag":144,"props":182,"children":184},{"class":146,"line":183},5,[185],{"type":45,"tag":144,"props":186,"children":187},{},[188],{"type":51,"value":189},"    ...\n",{"type":45,"tag":144,"props":191,"children":193},{"class":146,"line":192},6,[194],{"type":45,"tag":144,"props":195,"children":196},{},[197],{"type":51,"value":198},"}\n",{"type":45,"tag":54,"props":200,"children":201},{},[202,207,209,215,217,223],{"type":45,"tag":118,"props":203,"children":204},{},[205],{"type":51,"value":206},"Note",{"type":51,"value":208},": ",{"type":45,"tag":60,"props":210,"children":212},{"className":211},[],[213],{"type":51,"value":214},"[MSBuildMultiThreadableTask]",{"type":51,"value":216}," has ",{"type":45,"tag":60,"props":218,"children":220},{"className":219},[],[221],{"type":51,"value":222},"Inherited = false",{"type":51,"value":224}," — it must be on each concrete class, not just the base.",{"type":45,"tag":92,"props":226,"children":228},{"id":227},"step-2-absolutize-paths-before-file-operations",[229],{"type":51,"value":230},"Step 2: Absolutize Paths Before File Operations",{"type":45,"tag":54,"props":232,"children":233},{},[234,236,242],{"type":51,"value":235},"All path strings must be absolutized with ",{"type":45,"tag":60,"props":237,"children":239},{"className":238},[],[240],{"type":51,"value":241},"TaskEnvironment.GetAbsolutePath()",{"type":51,"value":243}," before use in file system APIs. This resolves paths relative to the project directory, not the process working directory.",{"type":45,"tag":133,"props":245,"children":247},{"className":135,"code":246,"language":137,"meta":138,"style":138},"AbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath);\nif (File.Exists(absolutePath))\n{\n    string content = File.ReadAllText(absolutePath);\n}\n",[248],{"type":45,"tag":60,"props":249,"children":250},{"__ignoreMap":138},[251,259,267,274,282],{"type":45,"tag":144,"props":252,"children":253},{"class":146,"line":147},[254],{"type":45,"tag":144,"props":255,"children":256},{},[257],{"type":51,"value":258},"AbsolutePath absolutePath = TaskEnvironment.GetAbsolutePath(inputPath);\n",{"type":45,"tag":144,"props":260,"children":261},{"class":146,"line":156},[262],{"type":45,"tag":144,"props":263,"children":264},{},[265],{"type":51,"value":266},"if (File.Exists(absolutePath))\n",{"type":45,"tag":144,"props":268,"children":269},{"class":146,"line":165},[270],{"type":45,"tag":144,"props":271,"children":272},{},[273],{"type":51,"value":171},{"type":45,"tag":144,"props":275,"children":276},{"class":146,"line":174},[277],{"type":45,"tag":144,"props":278,"children":279},{},[280],{"type":51,"value":281},"    string content = File.ReadAllText(absolutePath);\n",{"type":45,"tag":144,"props":283,"children":284},{"class":146,"line":183},[285],{"type":45,"tag":144,"props":286,"children":287},{},[288],{"type":51,"value":198},{"type":45,"tag":54,"props":290,"children":291},{},[292,294,307],{"type":51,"value":293},"The ",{"type":45,"tag":295,"props":296,"children":300},"a",{"href":297,"rel":298},"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Fblob\u002Fmain\u002Fsrc\u002FFramework\u002FPathHelpers\u002FAbsolutePath.cs",[299],"nofollow",[301],{"type":45,"tag":60,"props":302,"children":304},{"className":303},[],[305],{"type":51,"value":306},"AbsolutePath",{"type":51,"value":308}," struct:",{"type":45,"tag":310,"props":311,"children":312},"ul",{},[313,325,344,357],{"type":45,"tag":314,"props":315,"children":316},"li",{},[317,323],{"type":45,"tag":60,"props":318,"children":320},{"className":319},[],[321],{"type":51,"value":322},"Value",{"type":51,"value":324}," — the absolute path string",{"type":45,"tag":314,"props":326,"children":327},{},[328,334,336,342],{"type":45,"tag":60,"props":329,"children":331},{"className":330},[],[332],{"type":51,"value":333},"OriginalValue",{"type":51,"value":335}," — preserves the input path (use for error messages and ",{"type":45,"tag":60,"props":337,"children":339},{"className":338},[],[340],{"type":51,"value":341},"[Output]",{"type":51,"value":343}," properties)",{"type":45,"tag":314,"props":345,"children":346},{},[347,349,355],{"type":51,"value":348},"Implicitly convertible to ",{"type":45,"tag":60,"props":350,"children":352},{"className":351},[],[353],{"type":51,"value":354},"string",{"type":51,"value":356}," for File\u002FDirectory API compatibility",{"type":45,"tag":314,"props":358,"children":359},{},[360,366,368,374,376,382],{"type":45,"tag":60,"props":361,"children":363},{"className":362},[],[364],{"type":51,"value":365},"GetCanonicalForm()",{"type":51,"value":367}," — resolves ",{"type":45,"tag":60,"props":369,"children":371},{"className":370},[],[372],{"type":51,"value":373},"..",{"type":51,"value":375}," segments and normalizes separators (see ",{"type":45,"tag":295,"props":377,"children":379},{"href":378},"#sin-5-canonicalization-mismatch",[380],{"type":51,"value":381},"Sin 5",{"type":51,"value":383},")",{"type":45,"tag":54,"props":385,"children":386},{},[387,392,393,399,401,407,409,415,417,423],{"type":45,"tag":118,"props":388,"children":389},{},[390],{"type":51,"value":391},"CAUTION",{"type":51,"value":208},{"type":45,"tag":60,"props":394,"children":396},{"className":395},[],[397],{"type":51,"value":398},"GetAbsolutePath()",{"type":51,"value":400}," throws ",{"type":45,"tag":60,"props":402,"children":404},{"className":403},[],[405],{"type":51,"value":406},"ArgumentException",{"type":51,"value":408}," for null\u002Fempty inputs. See ",{"type":45,"tag":295,"props":410,"children":412},{"href":411},"#sin-3-null-coalescing-that-changes-control-flow",[413],{"type":51,"value":414},"Sin 3",{"type":51,"value":416}," and ",{"type":45,"tag":295,"props":418,"children":420},{"href":419},"#sin-6-exception-type-change",[421],{"type":51,"value":422},"Sin 6",{"type":51,"value":424}," for compatibility implications.",{"type":45,"tag":92,"props":426,"children":428},{"id":427},"step-3-replace-environment-variable-apis",[429],{"type":51,"value":430},"Step 3: Replace Environment Variable APIs",{"type":45,"tag":432,"props":433,"children":434},"table",{},[435,454],{"type":45,"tag":436,"props":437,"children":438},"thead",{},[439],{"type":45,"tag":440,"props":441,"children":442},"tr",{},[443,449],{"type":45,"tag":444,"props":445,"children":446},"th",{},[447],{"type":51,"value":448},"BEFORE (UNSAFE)",{"type":45,"tag":444,"props":450,"children":451},{},[452],{"type":51,"value":453},"AFTER (SAFE)",{"type":45,"tag":455,"props":456,"children":457},"tbody",{},[458,480],{"type":45,"tag":440,"props":459,"children":460},{},[461,471],{"type":45,"tag":462,"props":463,"children":464},"td",{},[465],{"type":45,"tag":60,"props":466,"children":468},{"className":467},[],[469],{"type":51,"value":470},"Environment.GetEnvironmentVariable(\"VAR\");",{"type":45,"tag":462,"props":472,"children":473},{},[474],{"type":45,"tag":60,"props":475,"children":477},{"className":476},[],[478],{"type":51,"value":479},"TaskEnvironment.GetEnvironmentVariable(\"VAR\");",{"type":45,"tag":440,"props":481,"children":482},{},[483,492],{"type":45,"tag":462,"props":484,"children":485},{},[486],{"type":45,"tag":60,"props":487,"children":489},{"className":488},[],[490],{"type":51,"value":491},"Environment.SetEnvironmentVariable(\"VAR\", \"v\");",{"type":45,"tag":462,"props":493,"children":494},{},[495],{"type":45,"tag":60,"props":496,"children":498},{"className":497},[],[499],{"type":51,"value":500},"TaskEnvironment.SetEnvironmentVariable(\"VAR\", \"v\");",{"type":45,"tag":92,"props":502,"children":504},{"id":503},"step-4-replace-process-start-apis",[505],{"type":51,"value":506},"Step 4: Replace Process Start APIs",{"type":45,"tag":432,"props":508,"children":509},{},[510,526],{"type":45,"tag":436,"props":511,"children":512},{},[513],{"type":45,"tag":440,"props":514,"children":515},{},[516,521],{"type":45,"tag":444,"props":517,"children":518},{},[519],{"type":51,"value":520},"BEFORE (UNSAFE - inherits process state)",{"type":45,"tag":444,"props":522,"children":523},{},[524],{"type":51,"value":525},"AFTER (SAFE - uses task's isolated environment)",{"type":45,"tag":455,"props":527,"children":528},{},[529,550],{"type":45,"tag":440,"props":530,"children":531},{},[532,541],{"type":45,"tag":462,"props":533,"children":534},{},[535],{"type":45,"tag":60,"props":536,"children":538},{"className":537},[],[539],{"type":51,"value":540},"var psi = new ProcessStartInfo(\"tool.exe\");",{"type":45,"tag":462,"props":542,"children":543},{},[544],{"type":45,"tag":60,"props":545,"children":547},{"className":546},[],[548],{"type":51,"value":549},"var psi = TaskEnvironment.GetProcessStartInfo();",{"type":45,"tag":440,"props":551,"children":552},{},[553,556],{"type":45,"tag":462,"props":554,"children":555},{},[],{"type":45,"tag":462,"props":557,"children":558},{},[559],{"type":45,"tag":60,"props":560,"children":562},{"className":561},[],[563],{"type":51,"value":564},"psi.FileName = GetFullPathToTool(); \u002F\u002F must be absolute",{"type":45,"tag":85,"props":566,"children":568},{"id":567},"updating-unit-tests",[569],{"type":51,"value":570},"Updating Unit Tests",{"type":45,"tag":54,"props":572,"children":573},{},[574,576,581,583,589,591,596,598,603,605,611,613,619],{"type":51,"value":575},"Built-in MSBuild tasks now initialize ",{"type":45,"tag":60,"props":577,"children":579},{"className":578},[],[580],{"type":51,"value":73},{"type":51,"value":582}," with a ",{"type":45,"tag":60,"props":584,"children":586},{"className":585},[],[587],{"type":51,"value":588},"MultiProcessTaskEnvironmentDriver",{"type":51,"value":590},"-backed default. Tests creating instances of built-in tasks no longer need manual ",{"type":45,"tag":60,"props":592,"children":594},{"className":593},[],[595],{"type":51,"value":73},{"type":51,"value":597}," setup. For custom or third-party tasks that implement ",{"type":45,"tag":60,"props":599,"children":601},{"className":600},[],[602],{"type":51,"value":81},{"type":51,"value":604}," without a default initializer, set ",{"type":45,"tag":60,"props":606,"children":608},{"className":607},[],[609],{"type":51,"value":610},"TaskEnvironment = TaskEnvironment.Fallback",{"type":51,"value":612}," (or use ",{"type":45,"tag":60,"props":614,"children":616},{"className":615},[],[617],{"type":51,"value":618},"TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(path)",{"type":51,"value":620}," to point at a specific project directory).",{"type":45,"tag":85,"props":622,"children":624},{"id":623},"apis-to-avoid",[625],{"type":51,"value":626},"APIs to Avoid",{"type":45,"tag":432,"props":628,"children":629},{},[630,651],{"type":45,"tag":436,"props":631,"children":632},{},[633],{"type":45,"tag":440,"props":634,"children":635},{},[636,641,646],{"type":45,"tag":444,"props":637,"children":638},{},[639],{"type":51,"value":640},"Category",{"type":45,"tag":444,"props":642,"children":643},{},[644],{"type":51,"value":645},"APIs",{"type":45,"tag":444,"props":647,"children":648},{},[649],{"type":51,"value":650},"Alternative",{"type":45,"tag":455,"props":652,"children":653},{},[654,714,760,820],{"type":45,"tag":440,"props":655,"children":656},{},[657,665,703],{"type":45,"tag":462,"props":658,"children":659},{},[660],{"type":45,"tag":118,"props":661,"children":662},{},[663],{"type":51,"value":664},"Forbidden",{"type":45,"tag":462,"props":666,"children":667},{},[668,674,676,682,683,689,690,696,697],{"type":45,"tag":60,"props":669,"children":671},{"className":670},[],[672],{"type":51,"value":673},"Environment.Exit",{"type":51,"value":675},", ",{"type":45,"tag":60,"props":677,"children":679},{"className":678},[],[680],{"type":51,"value":681},"FailFast",{"type":51,"value":675},{"type":45,"tag":60,"props":684,"children":686},{"className":685},[],[687],{"type":51,"value":688},"Process.Kill",{"type":51,"value":675},{"type":45,"tag":60,"props":691,"children":693},{"className":692},[],[694],{"type":51,"value":695},"ThreadPool.SetMin\u002FMaxThreads",{"type":51,"value":675},{"type":45,"tag":60,"props":698,"children":700},{"className":699},[],[701],{"type":51,"value":702},"Console.*",{"type":45,"tag":462,"props":704,"children":705},{},[706,708],{"type":51,"value":707},"Return false, throw, or use ",{"type":45,"tag":60,"props":709,"children":711},{"className":710},[],[712],{"type":51,"value":713},"Log",{"type":45,"tag":440,"props":715,"children":716},{},[717,725,755],{"type":45,"tag":462,"props":718,"children":719},{},[720],{"type":45,"tag":118,"props":721,"children":722},{},[723],{"type":51,"value":724},"Use TaskEnvironment",{"type":45,"tag":462,"props":726,"children":727},{},[728,734,735,741,742,748,749],{"type":45,"tag":60,"props":729,"children":731},{"className":730},[],[732],{"type":51,"value":733},"Environment.CurrentDirectory",{"type":51,"value":675},{"type":45,"tag":60,"props":736,"children":738},{"className":737},[],[739],{"type":51,"value":740},"Get\u002FSetEnvironmentVariable",{"type":51,"value":675},{"type":45,"tag":60,"props":743,"children":745},{"className":744},[],[746],{"type":51,"value":747},"Path.GetFullPath",{"type":51,"value":675},{"type":45,"tag":60,"props":750,"children":752},{"className":751},[],[753],{"type":51,"value":754},"ProcessStartInfo",{"type":45,"tag":462,"props":756,"children":757},{},[758],{"type":51,"value":759},"See Steps 2-4",{"type":45,"tag":440,"props":761,"children":762},{},[763,771,815],{"type":45,"tag":462,"props":764,"children":765},{},[766],{"type":45,"tag":118,"props":767,"children":768},{},[769],{"type":51,"value":770},"Need absolute paths",{"type":45,"tag":462,"props":772,"children":773},{},[774,780,781,787,788,794,795,801,802,808,809],{"type":45,"tag":60,"props":775,"children":777},{"className":776},[],[778],{"type":51,"value":779},"File.*",{"type":51,"value":675},{"type":45,"tag":60,"props":782,"children":784},{"className":783},[],[785],{"type":51,"value":786},"Directory.*",{"type":51,"value":675},{"type":45,"tag":60,"props":789,"children":791},{"className":790},[],[792],{"type":51,"value":793},"FileInfo",{"type":51,"value":675},{"type":45,"tag":60,"props":796,"children":798},{"className":797},[],[799],{"type":51,"value":800},"DirectoryInfo",{"type":51,"value":675},{"type":45,"tag":60,"props":803,"children":805},{"className":804},[],[806],{"type":51,"value":807},"FileStream",{"type":51,"value":675},{"type":45,"tag":60,"props":810,"children":812},{"className":811},[],[813],{"type":51,"value":814},"StreamReader\u002FWriter",{"type":45,"tag":462,"props":816,"children":817},{},[818],{"type":51,"value":819},"Absolutize first (File System APIs)",{"type":45,"tag":440,"props":821,"children":822},{},[823,831,847],{"type":45,"tag":462,"props":824,"children":825},{},[826],{"type":45,"tag":118,"props":827,"children":828},{},[829],{"type":51,"value":830},"Review required",{"type":45,"tag":462,"props":832,"children":833},{},[834,840,841],{"type":45,"tag":60,"props":835,"children":837},{"className":836},[],[838],{"type":51,"value":839},"Assembly.Load*",{"type":51,"value":675},{"type":45,"tag":60,"props":842,"children":844},{"className":843},[],[845],{"type":51,"value":846},"Activator.CreateInstance*",{"type":45,"tag":462,"props":848,"children":849},{},[850],{"type":51,"value":851},"Check for version conflicts",{"type":45,"tag":85,"props":853,"children":855},{"id":854},"practical-notes",[856],{"type":51,"value":857},"Practical Notes",{"type":45,"tag":92,"props":859,"children":861},{"id":860},"critical-trace-all-path-string-usage",[862],{"type":51,"value":863},"CRITICAL: Trace All Path String Usage",{"type":45,"tag":54,"props":865,"children":866},{},[867],{"type":51,"value":868},"Trace every path string through all method calls and assignments to find all places it flows into file system operations — including helper methods that may internally use File System APIs.",{"type":45,"tag":870,"props":871,"children":872},"ol",{},[873,886,891,896],{"type":45,"tag":314,"props":874,"children":875},{},[876,878,884],{"type":51,"value":877},"Find every path string (e.g., ",{"type":45,"tag":60,"props":879,"children":881},{"className":880},[],[882],{"type":51,"value":883},"item.ItemSpec",{"type":51,"value":885},", function parameters)",{"type":45,"tag":314,"props":887,"children":888},{},[889],{"type":51,"value":890},"Trace downstream through all method calls",{"type":45,"tag":314,"props":892,"children":893},{},[894],{"type":51,"value":895},"Absolutize BEFORE any code path that touches the file system",{"type":45,"tag":314,"props":897,"children":898},{},[899,901,906,908],{"type":51,"value":900},"Use ",{"type":45,"tag":60,"props":902,"children":904},{"className":903},[],[905],{"type":51,"value":333},{"type":51,"value":907}," for user-facing output (logs, errors) — see ",{"type":45,"tag":295,"props":909,"children":911},{"href":910},"#sin-2-error-message-path-inflation",[912],{"type":51,"value":913},"Sin 2",{"type":45,"tag":92,"props":915,"children":917},{"id":916},"exception-handling-in-batch-operations",[918],{"type":51,"value":919},"Exception Handling in Batch Operations",{"type":45,"tag":54,"props":921,"children":922},{},[923,925,930],{"type":51,"value":924},"In batch processing (iterating over files), ",{"type":45,"tag":60,"props":926,"children":928},{"className":927},[],[929],{"type":51,"value":398},{"type":51,"value":931}," throwing on one bad path aborts the entire batch. Match the original task's error semantics:",{"type":45,"tag":133,"props":933,"children":935},{"className":135,"code":934,"language":137,"meta":138,"style":138},"bool success = true;\nforeach (ITaskItem item in SourceFiles)\n{\n    try\n    {\n        AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec);\n        ProcessFile(path);\n    }\n    catch (ArgumentException ex)\n    {\n        Log.LogError(\"Invalid path '{0}': {1}\", item.ItemSpec, ex.Message);\n        success = false;\n    }\n}\nreturn success;\n",[936],{"type":45,"tag":60,"props":937,"children":938},{"__ignoreMap":138},[939,947,955,962,970,978,986,995,1004,1013,1021,1030,1039,1047,1055],{"type":45,"tag":144,"props":940,"children":941},{"class":146,"line":147},[942],{"type":45,"tag":144,"props":943,"children":944},{},[945],{"type":51,"value":946},"bool success = true;\n",{"type":45,"tag":144,"props":948,"children":949},{"class":146,"line":156},[950],{"type":45,"tag":144,"props":951,"children":952},{},[953],{"type":51,"value":954},"foreach (ITaskItem item in SourceFiles)\n",{"type":45,"tag":144,"props":956,"children":957},{"class":146,"line":165},[958],{"type":45,"tag":144,"props":959,"children":960},{},[961],{"type":51,"value":171},{"type":45,"tag":144,"props":963,"children":964},{"class":146,"line":174},[965],{"type":45,"tag":144,"props":966,"children":967},{},[968],{"type":51,"value":969},"    try\n",{"type":45,"tag":144,"props":971,"children":972},{"class":146,"line":183},[973],{"type":45,"tag":144,"props":974,"children":975},{},[976],{"type":51,"value":977},"    {\n",{"type":45,"tag":144,"props":979,"children":980},{"class":146,"line":192},[981],{"type":45,"tag":144,"props":982,"children":983},{},[984],{"type":51,"value":985},"        AbsolutePath path = TaskEnvironment.GetAbsolutePath(item.ItemSpec);\n",{"type":45,"tag":144,"props":987,"children":989},{"class":146,"line":988},7,[990],{"type":45,"tag":144,"props":991,"children":992},{},[993],{"type":51,"value":994},"        ProcessFile(path);\n",{"type":45,"tag":144,"props":996,"children":998},{"class":146,"line":997},8,[999],{"type":45,"tag":144,"props":1000,"children":1001},{},[1002],{"type":51,"value":1003},"    }\n",{"type":45,"tag":144,"props":1005,"children":1007},{"class":146,"line":1006},9,[1008],{"type":45,"tag":144,"props":1009,"children":1010},{},[1011],{"type":51,"value":1012},"    catch (ArgumentException ex)\n",{"type":45,"tag":144,"props":1014,"children":1016},{"class":146,"line":1015},10,[1017],{"type":45,"tag":144,"props":1018,"children":1019},{},[1020],{"type":51,"value":977},{"type":45,"tag":144,"props":1022,"children":1024},{"class":146,"line":1023},11,[1025],{"type":45,"tag":144,"props":1026,"children":1027},{},[1028],{"type":51,"value":1029},"        Log.LogError(\"Invalid path '{0}': {1}\", item.ItemSpec, ex.Message);\n",{"type":45,"tag":144,"props":1031,"children":1033},{"class":146,"line":1032},12,[1034],{"type":45,"tag":144,"props":1035,"children":1036},{},[1037],{"type":51,"value":1038},"        success = false;\n",{"type":45,"tag":144,"props":1040,"children":1042},{"class":146,"line":1041},13,[1043],{"type":45,"tag":144,"props":1044,"children":1045},{},[1046],{"type":51,"value":1003},{"type":45,"tag":144,"props":1048,"children":1050},{"class":146,"line":1049},14,[1051],{"type":45,"tag":144,"props":1052,"children":1053},{},[1054],{"type":51,"value":198},{"type":45,"tag":144,"props":1056,"children":1058},{"class":146,"line":1057},15,[1059],{"type":45,"tag":144,"props":1060,"children":1061},{},[1062],{"type":51,"value":1063},"return success;\n",{"type":45,"tag":92,"props":1065,"children":1067},{"id":1066},"prefer-absolutepath-over-string",[1068],{"type":51,"value":1069},"Prefer AbsolutePath Over String",{"type":45,"tag":54,"props":1071,"children":1072},{},[1073,1075,1080,1082,1087,1089,1094],{"type":51,"value":1074},"Stay in the ",{"type":45,"tag":60,"props":1076,"children":1078},{"className":1077},[],[1079],{"type":51,"value":306},{"type":51,"value":1081}," world — it's implicitly convertible to ",{"type":45,"tag":60,"props":1083,"children":1085},{"className":1084},[],[1086],{"type":51,"value":354},{"type":51,"value":1088}," where needed. Avoid round-tripping through ",{"type":45,"tag":60,"props":1090,"children":1092},{"className":1091},[],[1093],{"type":51,"value":354},{"type":51,"value":1095}," and back.",{"type":45,"tag":92,"props":1097,"children":1099},{"id":1098},"taskenvironment-is-not-thread-safe",[1100],{"type":51,"value":1101},"TaskEnvironment is Not Thread-Safe",{"type":45,"tag":54,"props":1103,"children":1104},{},[1105,1107,1112,1114,1120],{"type":51,"value":1106},"If your task spawns multiple threads internally, synchronize access to ",{"type":45,"tag":60,"props":1108,"children":1110},{"className":1109},[],[1111],{"type":51,"value":73},{"type":51,"value":1113},". Each task ",{"type":45,"tag":1115,"props":1116,"children":1117},"em",{},[1118],{"type":51,"value":1119},"instance",{"type":51,"value":1121}," gets its own environment, so no synchronization between tasks is needed.",{"type":45,"tag":92,"props":1123,"children":1125},{"id":1124},"api-discipline-when-plumbing-through-helpers",[1126],{"type":51,"value":1127},"API Discipline When Plumbing Through Helpers",{"type":45,"tag":54,"props":1129,"children":1130},{},[1131],{"type":51,"value":1132},"When the migration ripples into shared helpers:",{"type":45,"tag":310,"props":1134,"children":1135},{},[1136,1181,1199,1225],{"type":45,"tag":314,"props":1137,"children":1138},{},[1139,1157,1159,1164,1166,1172,1173,1179],{"type":45,"tag":118,"props":1140,"children":1141},{},[1142,1144,1149,1151],{"type":51,"value":1143},"Helper signatures: take ",{"type":45,"tag":60,"props":1145,"children":1147},{"className":1146},[],[1148],{"type":51,"value":306},{"type":51,"value":1150},", not ",{"type":45,"tag":60,"props":1152,"children":1154},{"className":1153},[],[1155],{"type":51,"value":1156},"(string path, string pathForMessages)",{"type":51,"value":1158},". Two-string signatures drift apart; the caller passing one ",{"type":45,"tag":60,"props":1160,"children":1162},{"className":1161},[],[1163],{"type":51,"value":306},{"type":51,"value":1165}," keeps ",{"type":45,"tag":60,"props":1167,"children":1169},{"className":1168},[],[1170],{"type":51,"value":1171},".Value",{"type":51,"value":416},{"type":45,"tag":60,"props":1174,"children":1176},{"className":1175},[],[1177],{"type":51,"value":1178},".OriginalValue",{"type":51,"value":1180}," in lockstep.",{"type":45,"tag":314,"props":1182,"children":1183},{},[1184,1189,1191,1197],{"type":45,"tag":118,"props":1185,"children":1186},{},[1187],{"type":51,"value":1188},"Repo-wide helpers belong in an extensions class",{"type":51,"value":1190}," (e.g., ",{"type":45,"tag":60,"props":1192,"children":1194},{"className":1193},[],[1195],{"type":51,"value":1196},"TaskEnvironmentExtensions",{"type":51,"value":1198},"), not on the task. If a per-task helper looks generic, extract it.",{"type":45,"tag":314,"props":1200,"children":1201},{},[1202,1207,1209,1215,1217,1223],{"type":45,"tag":118,"props":1203,"children":1204},{},[1205],{"type":51,"value":1206},"Don't condition behavior on MT-mode",{"type":51,"value":1208},". ",{"type":45,"tag":60,"props":1210,"children":1212},{"className":1211},[],[1213],{"type":51,"value":1214},"TaskEnvironment.Fallback",{"type":51,"value":1216}," already gives single-process semantics; ",{"type":45,"tag":60,"props":1218,"children":1220},{"className":1219},[],[1221],{"type":51,"value":1222},"if (mtMode) { … } else { … }",{"type":51,"value":1224}," doubles the maintenance surface and skips test coverage of one branch.",{"type":45,"tag":314,"props":1226,"children":1227},{},[1228,1245],{"type":45,"tag":118,"props":1229,"children":1230},{},[1231,1233,1238,1239],{"type":51,"value":1232},"Don't silently swallow ",{"type":45,"tag":60,"props":1234,"children":1236},{"className":1235},[],[1237],{"type":51,"value":406},{"type":51,"value":75},{"type":45,"tag":60,"props":1240,"children":1242},{"className":1241},[],[1243],{"type":51,"value":1244},"GetAbsolutePath",{"type":51,"value":1246}," in a helper — log a diagnostic so customers can debug bad inputs.",{"type":45,"tag":85,"props":1248,"children":1250},{"id":1249},"references",[1251],{"type":51,"value":1252},"References",{"type":45,"tag":310,"props":1254,"children":1255},{},[1256,1266,1278,1291],{"type":45,"tag":314,"props":1257,"children":1258},{},[1259],{"type":45,"tag":295,"props":1260,"children":1263},{"href":1261,"rel":1262},"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Fblob\u002Fmain\u002Fdocumentation\u002Fspecs\u002Fmultithreading\u002Fthread-safe-tasks.md",[299],[1264],{"type":51,"value":1265},"Thread-Safe Tasks Spec",{"type":45,"tag":314,"props":1267,"children":1268},{},[1269],{"type":45,"tag":295,"props":1270,"children":1272},{"href":297,"rel":1271},[299],[1273],{"type":45,"tag":60,"props":1274,"children":1276},{"className":1275},[],[1277],{"type":51,"value":306},{"type":45,"tag":314,"props":1279,"children":1280},{},[1281],{"type":45,"tag":295,"props":1282,"children":1285},{"href":1283,"rel":1284},"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Fblob\u002Fmain\u002Fsrc\u002FFramework\u002FTaskEnvironment.cs",[299],[1286],{"type":45,"tag":60,"props":1287,"children":1289},{"className":1288},[],[1290],{"type":51,"value":73},{"type":45,"tag":314,"props":1292,"children":1293},{},[1294],{"type":45,"tag":295,"props":1295,"children":1298},{"href":1296,"rel":1297},"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fmsbuild\u002Fblob\u002Fmain\u002Fsrc\u002FFramework\u002FIMultiThreadableTask.cs",[299],[1299],{"type":45,"tag":60,"props":1300,"children":1302},{"className":1301},[],[1303],{"type":51,"value":81},{"type":45,"tag":1305,"props":1306,"children":1307},"hr",{},[],{"type":45,"tag":46,"props":1309,"children":1311},{"id":1310},"compatibility-red-team-playbook",[1312],{"type":51,"value":1313},"Compatibility Red-Team Playbook",{"type":45,"tag":54,"props":1315,"children":1316},{},[1317,1319],{"type":51,"value":1318},"After migration, review for behavioral compatibility. ",{"type":45,"tag":118,"props":1320,"children":1321},{},[1322],{"type":51,"value":1323},"Every observable difference is a bug until proven otherwise.",{"type":45,"tag":54,"props":1325,"children":1326},{},[1327,1329,1335,1337,1342],{"type":51,"value":1328},"Observable behavior = ",{"type":45,"tag":60,"props":1330,"children":1332},{"className":1331},[],[1333],{"type":51,"value":1334},"Execute()",{"type":51,"value":1336}," return value, ",{"type":45,"tag":60,"props":1338,"children":1340},{"className":1339},[],[1341],{"type":51,"value":341},{"type":51,"value":1343}," property values, error\u002Fwarning message content, exception types, files written, and which code path runs.",{"type":45,"tag":85,"props":1345,"children":1347},{"id":1346},"the-7-deadly-compatibility-sins",[1348],{"type":51,"value":1349},"The 7 Deadly Compatibility Sins",{"type":45,"tag":54,"props":1351,"children":1352},{},[1353],{"type":51,"value":1354},"Real bugs found during MSBuild task migrations. Every one shipped in initial \"passing\" code with green tests.",{"type":45,"tag":54,"props":1356,"children":1357},{},[1358,1363,1365,1371,1373,1379,1381,1386,1387,1392],{"type":45,"tag":118,"props":1359,"children":1360},{},[1361],{"type":51,"value":1362},"Edge-case discipline:",{"type":51,"value":1364}," For every migrated code path, verify behavior when inputs are ",{"type":45,"tag":60,"props":1366,"children":1368},{"className":1367},[],[1369],{"type":51,"value":1370},"null",{"type":51,"value":1372},", empty string (",{"type":45,"tag":60,"props":1374,"children":1376},{"className":1375},[],[1377],{"type":51,"value":1378},"\"\"",{"type":51,"value":1380},"), or whitespace-only. ",{"type":45,"tag":60,"props":1382,"children":1384},{"className":1383},[],[1385],{"type":51,"value":1244},{"type":51,"value":400},{"type":45,"tag":60,"props":1388,"children":1390},{"className":1389},[],[1391],{"type":51,"value":406},{"type":51,"value":1393}," on null\u002Fempty — if the pre-migration code handled these differently (e.g., returned early, used a default, or threw a different exception type), the migration must preserve that behavior. Even if a scenario seems unlikely, treat it as a relevant finding if it is theoretically possible.",{"type":45,"tag":92,"props":1395,"children":1397},{"id":1396},"sin-1-output-property-contamination",[1398],{"type":51,"value":1399},"Sin 1: Output Property Contamination",{"type":45,"tag":54,"props":1401,"children":1402},{},[1403,1405,1410],{"type":51,"value":1404},"Absolutized values leak into ",{"type":45,"tag":60,"props":1406,"children":1408},{"className":1407},[],[1409],{"type":51,"value":341},{"type":51,"value":1411}," properties that users\u002Fother tasks consume.",{"type":45,"tag":133,"props":1413,"children":1415},{"className":135,"code":1414,"language":137,"meta":138,"style":138},"\u002F\u002F BROKEN: ManifestPath was \"bin\\Release\\app.manifest\", now \"C:\\repo\\bin\\Release\\app.manifest\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, name));\nManifestPath = abs; \u002F\u002F implicit string conversion!\n\n\u002F\u002F CORRECT: separate original form from absolutized path\nstring originalPath = Path.Combine(OutputDirectory, name);\nAbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(originalPath);\nManifestPath = originalPath;          \u002F\u002F [Output]: original form\ndocument.Save((string)outputPath);    \u002F\u002F file I\u002FO: absolute path\n",[1416],{"type":45,"tag":60,"props":1417,"children":1418},{"__ignoreMap":138},[1419,1427,1435,1443,1452,1460,1468,1476,1484],{"type":45,"tag":144,"props":1420,"children":1421},{"class":146,"line":147},[1422],{"type":45,"tag":144,"props":1423,"children":1424},{},[1425],{"type":51,"value":1426},"\u002F\u002F BROKEN: ManifestPath was \"bin\\Release\\app.manifest\", now \"C:\\repo\\bin\\Release\\app.manifest\"\n",{"type":45,"tag":144,"props":1428,"children":1429},{"class":146,"line":156},[1430],{"type":45,"tag":144,"props":1431,"children":1432},{},[1433],{"type":51,"value":1434},"AbsolutePath abs = TaskEnvironment.GetAbsolutePath(Path.Combine(OutputDirectory, name));\n",{"type":45,"tag":144,"props":1436,"children":1437},{"class":146,"line":165},[1438],{"type":45,"tag":144,"props":1439,"children":1440},{},[1441],{"type":51,"value":1442},"ManifestPath = abs; \u002F\u002F implicit string conversion!\n",{"type":45,"tag":144,"props":1444,"children":1445},{"class":146,"line":174},[1446],{"type":45,"tag":144,"props":1447,"children":1449},{"emptyLinePlaceholder":1448},true,[1450],{"type":51,"value":1451},"\n",{"type":45,"tag":144,"props":1453,"children":1454},{"class":146,"line":183},[1455],{"type":45,"tag":144,"props":1456,"children":1457},{},[1458],{"type":51,"value":1459},"\u002F\u002F CORRECT: separate original form from absolutized path\n",{"type":45,"tag":144,"props":1461,"children":1462},{"class":146,"line":192},[1463],{"type":45,"tag":144,"props":1464,"children":1465},{},[1466],{"type":51,"value":1467},"string originalPath = Path.Combine(OutputDirectory, name);\n",{"type":45,"tag":144,"props":1469,"children":1470},{"class":146,"line":988},[1471],{"type":45,"tag":144,"props":1472,"children":1473},{},[1474],{"type":51,"value":1475},"AbsolutePath outputPath = TaskEnvironment.GetAbsolutePath(originalPath);\n",{"type":45,"tag":144,"props":1477,"children":1478},{"class":146,"line":997},[1479],{"type":45,"tag":144,"props":1480,"children":1481},{},[1482],{"type":51,"value":1483},"ManifestPath = originalPath;          \u002F\u002F [Output]: original form\n",{"type":45,"tag":144,"props":1485,"children":1486},{"class":146,"line":1006},[1487],{"type":45,"tag":144,"props":1488,"children":1489},{},[1490],{"type":51,"value":1491},"document.Save((string)outputPath);    \u002F\u002F file I\u002FO: absolute path\n",{"type":45,"tag":54,"props":1493,"children":1494},{},[1495,1500,1502,1507,1509,1514],{"type":45,"tag":118,"props":1496,"children":1497},{},[1498],{"type":51,"value":1499},"Detect",{"type":51,"value":1501},": For every ",{"type":45,"tag":60,"props":1503,"children":1505},{"className":1504},[],[1506],{"type":51,"value":341},{"type":51,"value":1508}," property, trace backward — is it ever assigned from an ",{"type":45,"tag":60,"props":1510,"children":1512},{"className":1511},[],[1513],{"type":51,"value":306},{"type":51,"value":1515},"?",{"type":45,"tag":92,"props":1517,"children":1519},{"id":1518},"sin-2-errorlog-message-path-inflation",[1520],{"type":51,"value":1521},"Sin 2: Error\u002FLog Message Path Inflation",{"type":45,"tag":54,"props":1523,"children":1524},{},[1525],{"type":51,"value":1526},"Error messages and exception messages show absolutized paths instead of the user's original input.",{"type":45,"tag":54,"props":1528,"children":1529},{},[1530,1535,1537,1542],{"type":45,"tag":118,"props":1531,"children":1532},{},[1533],{"type":51,"value":1534},"Direct leakage",{"type":51,"value":1536}," — passing an ",{"type":45,"tag":60,"props":1538,"children":1540},{"className":1539},[],[1541],{"type":51,"value":306},{"type":51,"value":1543}," to logging APIs:",{"type":45,"tag":133,"props":1545,"children":1547},{"className":135,"code":1546,"language":137,"meta":138,"style":138},"\u002F\u002F BROKEN: \"Cannot find 'C:\\repo\\app.manifest'\" instead of \"Cannot find 'app.manifest'\"\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(path);\nLog.LogError(\"Cannot find '{0}'\", abs); \u002F\u002F implicit conversion!\n\n\u002F\u002F CORRECT: use OriginalValue\nLog.LogError(\"Cannot find '{0}'\", abs.OriginalValue);\n",[1548],{"type":45,"tag":60,"props":1549,"children":1550},{"__ignoreMap":138},[1551,1559,1567,1575,1582,1590],{"type":45,"tag":144,"props":1552,"children":1553},{"class":146,"line":147},[1554],{"type":45,"tag":144,"props":1555,"children":1556},{},[1557],{"type":51,"value":1558},"\u002F\u002F BROKEN: \"Cannot find 'C:\\repo\\app.manifest'\" instead of \"Cannot find 'app.manifest'\"\n",{"type":45,"tag":144,"props":1560,"children":1561},{"class":146,"line":156},[1562],{"type":45,"tag":144,"props":1563,"children":1564},{},[1565],{"type":51,"value":1566},"AbsolutePath abs = TaskEnvironment.GetAbsolutePath(path);\n",{"type":45,"tag":144,"props":1568,"children":1569},{"class":146,"line":165},[1570],{"type":45,"tag":144,"props":1571,"children":1572},{},[1573],{"type":51,"value":1574},"Log.LogError(\"Cannot find '{0}'\", abs); \u002F\u002F implicit conversion!\n",{"type":45,"tag":144,"props":1576,"children":1577},{"class":146,"line":174},[1578],{"type":45,"tag":144,"props":1579,"children":1580},{"emptyLinePlaceholder":1448},[1581],{"type":51,"value":1451},{"type":45,"tag":144,"props":1583,"children":1584},{"class":146,"line":183},[1585],{"type":45,"tag":144,"props":1586,"children":1587},{},[1588],{"type":51,"value":1589},"\u002F\u002F CORRECT: use OriginalValue\n",{"type":45,"tag":144,"props":1591,"children":1592},{"class":146,"line":192},[1593],{"type":45,"tag":144,"props":1594,"children":1595},{},[1596],{"type":51,"value":1597},"Log.LogError(\"Cannot find '{0}'\", abs.OriginalValue);\n",{"type":45,"tag":54,"props":1599,"children":1600},{},[1601,1606],{"type":45,"tag":118,"props":1602,"children":1603},{},[1604],{"type":51,"value":1605},"Indirect leakage",{"type":51,"value":1607}," — exception messages from helpers that received the absolutized path:",{"type":45,"tag":133,"props":1609,"children":1611},{"className":135,"code":1610,"language":137,"meta":138,"style":138},"\u002F\u002F BROKEN: ex.FileName \u002F ex.Message embed the absolutized path\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", ex.FileName); }\ncatch (Exception ex)              { Log.LogError(ex.Message); }\n\n\u002F\u002F CORRECT: prefer the original input; if you must use the exception, sanitize\ncatch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", abs.OriginalValue); }\ncatch (Exception ex)              { Log.LogError(ex.Message.Replace(abs.Value, abs.OriginalValue)); }\n",[1612],{"type":45,"tag":60,"props":1613,"children":1614},{"__ignoreMap":138},[1615,1623,1631,1639,1646,1654,1662],{"type":45,"tag":144,"props":1616,"children":1617},{"class":146,"line":147},[1618],{"type":45,"tag":144,"props":1619,"children":1620},{},[1621],{"type":51,"value":1622},"\u002F\u002F BROKEN: ex.FileName \u002F ex.Message embed the absolutized path\n",{"type":45,"tag":144,"props":1624,"children":1625},{"class":146,"line":156},[1626],{"type":45,"tag":144,"props":1627,"children":1628},{},[1629],{"type":51,"value":1630},"catch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", ex.FileName); }\n",{"type":45,"tag":144,"props":1632,"children":1633},{"class":146,"line":165},[1634],{"type":45,"tag":144,"props":1635,"children":1636},{},[1637],{"type":51,"value":1638},"catch (Exception ex)              { Log.LogError(ex.Message); }\n",{"type":45,"tag":144,"props":1640,"children":1641},{"class":146,"line":174},[1642],{"type":45,"tag":144,"props":1643,"children":1644},{"emptyLinePlaceholder":1448},[1645],{"type":51,"value":1451},{"type":45,"tag":144,"props":1647,"children":1648},{"class":146,"line":183},[1649],{"type":45,"tag":144,"props":1650,"children":1651},{},[1652],{"type":51,"value":1653},"\u002F\u002F CORRECT: prefer the original input; if you must use the exception, sanitize\n",{"type":45,"tag":144,"props":1655,"children":1656},{"class":146,"line":192},[1657],{"type":45,"tag":144,"props":1658,"children":1659},{},[1660],{"type":51,"value":1661},"catch (FileNotFoundException ex) { Log.LogError(\"Not found: {0}\", abs.OriginalValue); }\n",{"type":45,"tag":144,"props":1663,"children":1664},{"class":146,"line":988},[1665],{"type":45,"tag":144,"props":1666,"children":1667},{},[1668],{"type":51,"value":1669},"catch (Exception ex)              { Log.LogError(ex.Message.Replace(abs.Value, abs.OriginalValue)); }\n",{"type":45,"tag":54,"props":1671,"children":1672},{},[1673,1677,1679,1685,1687,1693,1694,1700,1702,1707,1709,1715,1717,1723,1725,1730],{"type":45,"tag":118,"props":1674,"children":1675},{},[1676],{"type":51,"value":1499},{"type":51,"value":1678},": Search every ",{"type":45,"tag":60,"props":1680,"children":1682},{"className":1681},[],[1683],{"type":51,"value":1684},"Log.LogError",{"type":51,"value":1686},"\u002F",{"type":45,"tag":60,"props":1688,"children":1690},{"className":1689},[],[1691],{"type":51,"value":1692},"LogWarning",{"type":51,"value":1686},{"type":45,"tag":60,"props":1695,"children":1697},{"className":1696},[],[1698],{"type":51,"value":1699},"LogMessage",{"type":51,"value":1701}," — is any argument an ",{"type":45,"tag":60,"props":1703,"children":1705},{"className":1704},[],[1706],{"type":51,"value":306},{"type":51,"value":1708},"? Also check every ",{"type":45,"tag":60,"props":1710,"children":1712},{"className":1711},[],[1713],{"type":51,"value":1714},"Log.LogError(ex.Message …)",{"type":51,"value":1716}," \u002F ",{"type":45,"tag":60,"props":1718,"children":1720},{"className":1719},[],[1721],{"type":51,"value":1722},"ex.FileName",{"type":51,"value":1724}," downstream of a ",{"type":45,"tag":60,"props":1726,"children":1728},{"className":1727},[],[1729],{"type":51,"value":1244},{"type":51,"value":1731}," — did the exception originate from a helper that received the absolutized path?",{"type":45,"tag":92,"props":1733,"children":1735},{"id":1734},"sin-3-null-coalescing-that-changes-control-flow",[1736],{"type":51,"value":1737},"Sin 3: Null Coalescing That Changes Control Flow",{"type":45,"tag":54,"props":1739,"children":1740},{},[1741,1743,1749],{"type":51,"value":1742},"Adding ",{"type":45,"tag":60,"props":1744,"children":1746},{"className":1745},[],[1747],{"type":51,"value":1748},"?? \"\"",{"type":51,"value":1750}," silently swallows an exception the old code relied on for error handling.",{"type":45,"tag":133,"props":1752,"children":1754},{"className":135,"code":1753,"language":137,"meta":138,"style":138},"\u002F\u002F BEFORE: Path.GetDirectoryName(\"C:\\\") → null → Path.Combine(null, x) → ArgumentNullException\n\u002F\u002F   → task fails with an exception \u002F error logged → Execute() returns false\n\n\u002F\u002F BROKEN: ?? \"\" added \"for safety\"\nstring dir = Path.GetDirectoryName(fileName) ?? string.Empty;\n\u002F\u002F Path.Combine(\"\", x) succeeds silently → no error → Execute() returns TRUE!\n",[1755],{"type":45,"tag":60,"props":1756,"children":1757},{"__ignoreMap":138},[1758,1766,1774,1781,1789,1797],{"type":45,"tag":144,"props":1759,"children":1760},{"class":146,"line":147},[1761],{"type":45,"tag":144,"props":1762,"children":1763},{},[1764],{"type":51,"value":1765},"\u002F\u002F BEFORE: Path.GetDirectoryName(\"C:\\\") → null → Path.Combine(null, x) → ArgumentNullException\n",{"type":45,"tag":144,"props":1767,"children":1768},{"class":146,"line":156},[1769],{"type":45,"tag":144,"props":1770,"children":1771},{},[1772],{"type":51,"value":1773},"\u002F\u002F   → task fails with an exception \u002F error logged → Execute() returns false\n",{"type":45,"tag":144,"props":1775,"children":1776},{"class":146,"line":165},[1777],{"type":45,"tag":144,"props":1778,"children":1779},{"emptyLinePlaceholder":1448},[1780],{"type":51,"value":1451},{"type":45,"tag":144,"props":1782,"children":1783},{"class":146,"line":174},[1784],{"type":45,"tag":144,"props":1785,"children":1786},{},[1787],{"type":51,"value":1788},"\u002F\u002F BROKEN: ?? \"\" added \"for safety\"\n",{"type":45,"tag":144,"props":1790,"children":1791},{"class":146,"line":183},[1792],{"type":45,"tag":144,"props":1793,"children":1794},{},[1795],{"type":51,"value":1796},"string dir = Path.GetDirectoryName(fileName) ?? string.Empty;\n",{"type":45,"tag":144,"props":1798,"children":1799},{"class":146,"line":192},[1800],{"type":45,"tag":144,"props":1801,"children":1802},{},[1803],{"type":51,"value":1804},"\u002F\u002F Path.Combine(\"\", x) succeeds silently → no error → Execute() returns TRUE!\n",{"type":45,"tag":54,"props":1806,"children":1807},{},[1808,1812,1813,1819,1821,1826],{"type":45,"tag":118,"props":1809,"children":1810},{},[1811],{"type":51,"value":1499},{"type":51,"value":1501},{"type":45,"tag":60,"props":1814,"children":1816},{"className":1815},[],[1817],{"type":51,"value":1818},"??",{"type":51,"value":1820}," you added, ask: \"What happened when this was null before?\" If it threw and was caught → your ",{"type":45,"tag":60,"props":1822,"children":1824},{"className":1823},[],[1825],{"type":51,"value":1818},{"type":51,"value":1827}," is a bug.",{"type":45,"tag":92,"props":1829,"children":1831},{"id":1830},"sin-4-try-catch-scope-mismatch",[1832],{"type":51,"value":1833},"Sin 4: Try-Catch Scope Mismatch",{"type":45,"tag":54,"props":1835,"children":1836},{},[1837,1842,1844,1850],{"type":45,"tag":60,"props":1838,"children":1840},{"className":1839},[],[1841],{"type":51,"value":398},{"type":51,"value":1843}," inside a try block leaves the absolutized value out of scope in the catch block. Helper methods in the catch (like ",{"type":45,"tag":60,"props":1845,"children":1847},{"className":1846},[],[1848],{"type":51,"value":1849},"LockCheck",{"type":51,"value":1851},") then use the original non-absolute path.",{"type":45,"tag":133,"props":1853,"children":1855},{"className":135,"code":1854,"language":137,"meta":138,"style":138},"\u002F\u002F CORRECT: hoist above try so catch can use it too\nAbsolutePath abs = TaskEnvironment.GetAbsolutePath(OutputManifest.ItemSpec);\ntry {\n    WriteFile(abs);\n} catch (Exception ex) {\n    string lockMsg = LockCheck.GetLockedFileMessage(abs);        \u002F\u002F absolute → correct file\n    Log.LogError(\"Failed: {0}\", OutputManifest.ItemSpec, ...);   \u002F\u002F original → user-friendly\n}\n",[1856],{"type":45,"tag":60,"props":1857,"children":1858},{"__ignoreMap":138},[1859,1867,1875,1883,1891,1899,1907,1915],{"type":45,"tag":144,"props":1860,"children":1861},{"class":146,"line":147},[1862],{"type":45,"tag":144,"props":1863,"children":1864},{},[1865],{"type":51,"value":1866},"\u002F\u002F CORRECT: hoist above try so catch can use it too\n",{"type":45,"tag":144,"props":1868,"children":1869},{"class":146,"line":156},[1870],{"type":45,"tag":144,"props":1871,"children":1872},{},[1873],{"type":51,"value":1874},"AbsolutePath abs = TaskEnvironment.GetAbsolutePath(OutputManifest.ItemSpec);\n",{"type":45,"tag":144,"props":1876,"children":1877},{"class":146,"line":165},[1878],{"type":45,"tag":144,"props":1879,"children":1880},{},[1881],{"type":51,"value":1882},"try {\n",{"type":45,"tag":144,"props":1884,"children":1885},{"class":146,"line":174},[1886],{"type":45,"tag":144,"props":1887,"children":1888},{},[1889],{"type":51,"value":1890},"    WriteFile(abs);\n",{"type":45,"tag":144,"props":1892,"children":1893},{"class":146,"line":183},[1894],{"type":45,"tag":144,"props":1895,"children":1896},{},[1897],{"type":51,"value":1898},"} catch (Exception ex) {\n",{"type":45,"tag":144,"props":1900,"children":1901},{"class":146,"line":192},[1902],{"type":45,"tag":144,"props":1903,"children":1904},{},[1905],{"type":51,"value":1906},"    string lockMsg = LockCheck.GetLockedFileMessage(abs);        \u002F\u002F absolute → correct file\n",{"type":45,"tag":144,"props":1908,"children":1909},{"class":146,"line":988},[1910],{"type":45,"tag":144,"props":1911,"children":1912},{},[1913],{"type":51,"value":1914},"    Log.LogError(\"Failed: {0}\", OutputManifest.ItemSpec, ...);   \u002F\u002F original → user-friendly\n",{"type":45,"tag":144,"props":1916,"children":1917},{"class":146,"line":997},[1918],{"type":45,"tag":144,"props":1919,"children":1920},{},[1921],{"type":51,"value":198},{"type":45,"tag":54,"props":1923,"children":1924},{},[1925,1929,1930,1935],{"type":45,"tag":118,"props":1926,"children":1927},{},[1928],{"type":51,"value":1499},{"type":51,"value":1501},{"type":45,"tag":60,"props":1931,"children":1933},{"className":1932},[],[1934],{"type":51,"value":1244},{"type":51,"value":1936}," inside a try, check if the catch block needs the absolutized value.",{"type":45,"tag":92,"props":1938,"children":1940},{"id":1939},"sin-5-canonicalization-mismatch",[1941],{"type":51,"value":1942},"Sin 5: Canonicalization Mismatch",{"type":45,"tag":54,"props":1944,"children":1945},{},[1946,1951,1953,1958,1960,1965,1967,1972,1974,1980],{"type":45,"tag":60,"props":1947,"children":1949},{"className":1948},[],[1950],{"type":51,"value":1244},{"type":51,"value":1952}," does NOT canonicalize. ",{"type":45,"tag":60,"props":1954,"children":1956},{"className":1955},[],[1957],{"type":51,"value":747},{"type":51,"value":1959}," does TWO things: absolutize AND canonicalize (",{"type":45,"tag":60,"props":1961,"children":1963},{"className":1962},[],[1964],{"type":51,"value":373},{"type":51,"value":1966}," resolution, separator normalization). If the old code used ",{"type":45,"tag":60,"props":1968,"children":1970},{"className":1969},[],[1971],{"type":51,"value":747},{"type":51,"value":1973}," for dictionary keys, comparisons, or display, you must add ",{"type":45,"tag":60,"props":1975,"children":1977},{"className":1976},[],[1978],{"type":51,"value":1979},".GetCanonicalForm()",{"type":51,"value":1981},":",{"type":45,"tag":133,"props":1983,"children":1985},{"className":135,"code":1984,"language":137,"meta":138,"style":138},"\u002F\u002F GetAbsolutePath(\"foo\u002F..\u002Fbar\")  → \"C:\\repo\\foo\u002F..\u002Fbar\"  (NOT canonical)\n\u002F\u002F Path.GetFullPath(\"foo\u002F..\u002Fbar\") → \"C:\\repo\\bar\"         (canonical)\n\n\u002F\u002F BROKEN for dictionary keys — \"C:\\repo\\foo\\..\\bar\" ≠ \"C:\\repo\\bar\"\nvar map = items.ToDictionary(p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec), ...);\n\n\u002F\u002F CORRECT\nvar map = items.ToDictionary(\n    p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec).GetCanonicalForm(),\n    StringComparer.OrdinalIgnoreCase);\n",[1986],{"type":45,"tag":60,"props":1987,"children":1988},{"__ignoreMap":138},[1989,1997,2005,2012,2020,2028,2035,2043,2051,2059],{"type":45,"tag":144,"props":1990,"children":1991},{"class":146,"line":147},[1992],{"type":45,"tag":144,"props":1993,"children":1994},{},[1995],{"type":51,"value":1996},"\u002F\u002F GetAbsolutePath(\"foo\u002F..\u002Fbar\")  → \"C:\\repo\\foo\u002F..\u002Fbar\"  (NOT canonical)\n",{"type":45,"tag":144,"props":1998,"children":1999},{"class":146,"line":156},[2000],{"type":45,"tag":144,"props":2001,"children":2002},{},[2003],{"type":51,"value":2004},"\u002F\u002F Path.GetFullPath(\"foo\u002F..\u002Fbar\") → \"C:\\repo\\bar\"         (canonical)\n",{"type":45,"tag":144,"props":2006,"children":2007},{"class":146,"line":165},[2008],{"type":45,"tag":144,"props":2009,"children":2010},{"emptyLinePlaceholder":1448},[2011],{"type":51,"value":1451},{"type":45,"tag":144,"props":2013,"children":2014},{"class":146,"line":174},[2015],{"type":45,"tag":144,"props":2016,"children":2017},{},[2018],{"type":51,"value":2019},"\u002F\u002F BROKEN for dictionary keys — \"C:\\repo\\foo\\..\\bar\" ≠ \"C:\\repo\\bar\"\n",{"type":45,"tag":144,"props":2021,"children":2022},{"class":146,"line":183},[2023],{"type":45,"tag":144,"props":2024,"children":2025},{},[2026],{"type":51,"value":2027},"var map = items.ToDictionary(p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec), ...);\n",{"type":45,"tag":144,"props":2029,"children":2030},{"class":146,"line":192},[2031],{"type":45,"tag":144,"props":2032,"children":2033},{"emptyLinePlaceholder":1448},[2034],{"type":51,"value":1451},{"type":45,"tag":144,"props":2036,"children":2037},{"class":146,"line":988},[2038],{"type":45,"tag":144,"props":2039,"children":2040},{},[2041],{"type":51,"value":2042},"\u002F\u002F CORRECT\n",{"type":45,"tag":144,"props":2044,"children":2045},{"class":146,"line":997},[2046],{"type":45,"tag":144,"props":2047,"children":2048},{},[2049],{"type":51,"value":2050},"var map = items.ToDictionary(\n",{"type":45,"tag":144,"props":2052,"children":2053},{"class":146,"line":1006},[2054],{"type":45,"tag":144,"props":2055,"children":2056},{},[2057],{"type":51,"value":2058},"    p => (string)TaskEnvironment.GetAbsolutePath(p.ItemSpec).GetCanonicalForm(),\n",{"type":45,"tag":144,"props":2060,"children":2061},{"class":146,"line":1015},[2062],{"type":45,"tag":144,"props":2063,"children":2064},{},[2065],{"type":51,"value":2066},"    StringComparer.OrdinalIgnoreCase);\n",{"type":45,"tag":54,"props":2068,"children":2069},{},[2070,2074,2076,2082,2083,2089,2090,2096,2098,2103,2105,2110],{"type":45,"tag":118,"props":2071,"children":2072},{},[2073],{"type":51,"value":1499},{"type":51,"value":2075},": Find every ",{"type":45,"tag":60,"props":2077,"children":2079},{"className":2078},[],[2080],{"type":51,"value":2081},"Dictionary",{"type":51,"value":1686},{"type":45,"tag":60,"props":2084,"children":2086},{"className":2085},[],[2087],{"type":51,"value":2088},"HashSet",{"type":51,"value":1686},{"type":45,"tag":60,"props":2091,"children":2093},{"className":2092},[],[2094],{"type":51,"value":2095},"ToDictionary",{"type":51,"value":2097}," using path keys, and every place the old code called ",{"type":45,"tag":60,"props":2099,"children":2101},{"className":2100},[],[2102],{"type":51,"value":747},{"type":51,"value":2104},". If canonicalization mattered, add ",{"type":45,"tag":60,"props":2106,"children":2108},{"className":2107},[],[2109],{"type":51,"value":1979},{"type":51,"value":2111},".",{"type":45,"tag":92,"props":2113,"children":2115},{"id":2114},"sin-6-exception-type-change",[2116],{"type":51,"value":2117},"Sin 6: Exception Type Change",{"type":45,"tag":54,"props":2119,"children":2120},{},[2121,2123,2129,2131,2136,2137,2143,2145,2151,2153,2158],{"type":51,"value":2122},"Old code threw ",{"type":45,"tag":60,"props":2124,"children":2126},{"className":2125},[],[2127],{"type":51,"value":2128},"FileNotFoundException",{"type":51,"value":2130}," for missing files; new code throws ",{"type":45,"tag":60,"props":2132,"children":2134},{"className":2133},[],[2135],{"type":51,"value":406},{"type":51,"value":75},{"type":45,"tag":60,"props":2138,"children":2140},{"className":2139},[],[2141],{"type":51,"value":2142},"GetAbsolutePath(\"\")",{"type":51,"value":2144}," before reaching the file check. Custom catch blocks filtering by exception type may be bypassed. (",{"type":45,"tag":60,"props":2146,"children":2148},{"className":2147},[],[2149],{"type":51,"value":2150},"ExceptionHandling.IsIoRelatedException",{"type":51,"value":2152}," catches ",{"type":45,"tag":60,"props":2154,"children":2156},{"className":2155},[],[2157],{"type":51,"value":406},{"type":51,"value":2159},", but task-specific handlers might not.)",{"type":45,"tag":54,"props":2161,"children":2162},{},[2163,2167,2168,2173],{"type":45,"tag":118,"props":2164,"children":2165},{},[2166],{"type":51,"value":1499},{"type":51,"value":1501},{"type":45,"tag":60,"props":2169,"children":2171},{"className":2170},[],[2172],{"type":51,"value":1244},{"type":51,"value":2174},", check what the old code threw for null\u002Fempty and whether the calling code has type-specific catch blocks.",{"type":45,"tag":92,"props":2176,"children":2178},{"id":2177},"sin-7-pathispathrooted-as-an-absolutize-gate",[2179,2181,2187],{"type":51,"value":2180},"Sin 7: ",{"type":45,"tag":60,"props":2182,"children":2184},{"className":2183},[],[2185],{"type":51,"value":2186},"Path.IsPathRooted",{"type":51,"value":2188}," as an Absolutize Gate",{"type":45,"tag":54,"props":2190,"children":2191},{},[2192,2197,2199,2205,2207,2212,2214,2220,2222,2227,2228,2234],{"type":45,"tag":60,"props":2193,"children":2195},{"className":2194},[],[2196],{"type":51,"value":2186},{"type":51,"value":2198}," returns ",{"type":45,"tag":60,"props":2200,"children":2202},{"className":2201},[],[2203],{"type":51,"value":2204},"true",{"type":51,"value":2206}," for ",{"type":45,"tag":118,"props":2208,"children":2209},{},[2210],{"type":51,"value":2211},"drive-relative",{"type":51,"value":2213}," (",{"type":45,"tag":60,"props":2215,"children":2217},{"className":2216},[],[2218],{"type":51,"value":2219},"C:foo\\bar",{"type":51,"value":2221},") and ",{"type":45,"tag":118,"props":2223,"children":2224},{},[2225],{"type":51,"value":2226},"root-relative",{"type":51,"value":2213},{"type":45,"tag":60,"props":2229,"children":2231},{"className":2230},[],[2232],{"type":51,"value":2233},"\\foo\\bar",{"type":51,"value":2235},") Windows paths — both still depend on process current-directory \u002F current-drive state. \"Absolutize only if not rooted\" silently leaves those paths process-dependent.",{"type":45,"tag":54,"props":2237,"children":2238},{},[2239,2244,2246,2252,2253,2259,2261,2266,2268,2274],{"type":45,"tag":60,"props":2240,"children":2242},{"className":2241},[],[2243],{"type":51,"value":1244},{"type":51,"value":2245}," correctly handles all path forms — including the Windows edge cases (",{"type":45,"tag":60,"props":2247,"children":2249},{"className":2248},[],[2250],{"type":51,"value":2251},"C:foo",{"type":51,"value":675},{"type":45,"tag":60,"props":2254,"children":2256},{"className":2255},[],[2257],{"type":51,"value":2258},"\\foo",{"type":51,"value":2260},") that ",{"type":45,"tag":60,"props":2262,"children":2264},{"className":2263},[],[2265],{"type":51,"value":2186},{"type":51,"value":2267}," considers \"rooted\" but that are still CWD\u002Fdrive-dependent. Call it unconditionally; remove any ",{"type":45,"tag":60,"props":2269,"children":2271},{"className":2270},[],[2272],{"type":51,"value":2273},"IsPathRooted",{"type":51,"value":2275}," short-circuit.",{"type":45,"tag":1305,"props":2277,"children":2278},{},[],{"type":45,"tag":85,"props":2280,"children":2282},{"id":2281},"call-chain-hazards-beyond-the-task",[2283],{"type":51,"value":2284},"Call-Chain Hazards Beyond the Task",{"type":45,"tag":54,"props":2286,"children":2287},{},[2288,2290,2295,2297,2302],{"type":51,"value":2289},"The 7 sins above are ",{"type":45,"tag":1115,"props":2291,"children":2292},{},[2293],{"type":51,"value":2294},"local",{"type":51,"value":2296}," to the task body. The other half of MT migration is auditing the ",{"type":45,"tag":118,"props":2298,"children":2299},{},[2300],{"type":51,"value":2301},"transitive call chain",{"type":51,"value":2303}," — helpers and shared utility classes the task reaches into.",{"type":45,"tag":92,"props":2305,"children":2307},{"id":2306},"helpers-that-capture-process-state",[2308],{"type":51,"value":2309},"Helpers That Capture Process State",{"type":45,"tag":54,"props":2311,"children":2312},{},[2313,2315,2320],{"type":51,"value":2314},"Helpers reached from ",{"type":45,"tag":60,"props":2316,"children":2318},{"className":2317},[],[2319],{"type":51,"value":1334},{"type":51,"value":2321}," can quietly depend on process state in any of these ways:",{"type":45,"tag":310,"props":2323,"children":2324},{},[2325,2346,2366,2387,2406],{"type":45,"tag":314,"props":2326,"children":2327},{},[2328,2330,2336,2338,2344],{"type":51,"value":2329},"A relative path falls through to ",{"type":45,"tag":60,"props":2331,"children":2333},{"className":2332},[],[2334],{"type":51,"value":2335},"Directory.GetCurrentDirectory()",{"type":51,"value":2337}," (often as a \"fallback\") or to ",{"type":45,"tag":60,"props":2339,"children":2341},{"className":2340},[],[2342],{"type":51,"value":2343},"Path.GetFullPath(x)",{"type":51,"value":2345}," without a base.",{"type":45,"tag":314,"props":2347,"children":2348},{},[2349,2351,2357,2359,2364],{"type":51,"value":2350},"An env var is read directly via ",{"type":45,"tag":60,"props":2352,"children":2354},{"className":2353},[],[2355],{"type":51,"value":2356},"Environment.GetEnvironmentVariable",{"type":51,"value":2358}," (not ",{"type":45,"tag":60,"props":2360,"children":2362},{"className":2361},[],[2363],{"type":51,"value":73},{"type":51,"value":2365},").",{"type":45,"tag":314,"props":2367,"children":2368},{},[2369,2371,2377,2379,2385],{"type":51,"value":2370},"A ",{"type":45,"tag":60,"props":2372,"children":2374},{"className":2373},[],[2375],{"type":51,"value":2376},"static",{"type":51,"value":2378}," field is seeded from process state on first use (",{"type":45,"tag":60,"props":2380,"children":2382},{"className":2381},[],[2383],{"type":51,"value":2384},"static string s_x = Directory.GetCurrentDirectory()",{"type":51,"value":2386},"), permanently capturing the first caller's environment for every later caller.",{"type":45,"tag":314,"props":2388,"children":2389},{},[2390,2392,2398,2399,2404],{"type":51,"value":2391},"A BCL API echoes its input path back through ",{"type":45,"tag":60,"props":2393,"children":2395},{"className":2394},[],[2396],{"type":51,"value":2397},"ex.Message",{"type":51,"value":1716},{"type":45,"tag":60,"props":2400,"children":2402},{"className":2401},[],[2403],{"type":51,"value":1722},{"type":51,"value":2405}," (Sin 2).",{"type":45,"tag":314,"props":2407,"children":2408},{},[2409,2411,2417,2419,2424,2426,2431],{"type":51,"value":2410},"The helper takes a ",{"type":45,"tag":60,"props":2412,"children":2414},{"className":2413},[],[2415],{"type":51,"value":2416},"string path",{"type":51,"value":2418}," and returns a ",{"type":45,"tag":60,"props":2420,"children":2422},{"className":2421},[],[2423],{"type":51,"value":354},{"type":51,"value":2425}," — losing the ",{"type":45,"tag":60,"props":2427,"children":2429},{"className":2428},[],[2430],{"type":51,"value":1178},{"type":51,"value":2432}," distinction, so callers either lie in messages or absolutize twice.",{"type":45,"tag":54,"props":2434,"children":2435},{},[2436],{"type":45,"tag":118,"props":2437,"children":2438},{},[2439],{"type":51,"value":2440},"Resolution patterns:",{"type":45,"tag":870,"props":2442,"children":2443},{},[2444,2470,2495,2508],{"type":45,"tag":314,"props":2445,"children":2446},{},[2447,2449,2454,2456,2461,2463,2468],{"type":51,"value":2448},"Add an MT-aware overload that takes ",{"type":45,"tag":60,"props":2450,"children":2452},{"className":2451},[],[2453],{"type":51,"value":73},{"type":51,"value":2455}," (or an ",{"type":45,"tag":60,"props":2457,"children":2459},{"className":2458},[],[2460],{"type":51,"value":306},{"type":51,"value":2462}," \u002F explicit fallback path). Legacy overload delegates with ",{"type":45,"tag":60,"props":2464,"children":2466},{"className":2465},[],[2467],{"type":51,"value":1214},{"type":51,"value":2469},". Don't branch on \"MT mode on\u002Foff\".",{"type":45,"tag":314,"props":2471,"children":2472},{},[2473,2475,2480,2482,2487,2488,2493],{"type":51,"value":2474},"Promote the parameter type to ",{"type":45,"tag":60,"props":2476,"children":2478},{"className":2477},[],[2479],{"type":51,"value":306},{"type":51,"value":2481}," so ",{"type":45,"tag":60,"props":2483,"children":2485},{"className":2484},[],[2486],{"type":51,"value":1171},{"type":51,"value":416},{"type":45,"tag":60,"props":2489,"children":2491},{"className":2490},[],[2492],{"type":51,"value":1178},{"type":51,"value":2494}," travel together.",{"type":45,"tag":314,"props":2496,"children":2497},{},[2498,2500,2506],{"type":51,"value":2499},"Replace process-state-seeded static caches with ",{"type":45,"tag":60,"props":2501,"children":2503},{"className":2502},[],[2504],{"type":51,"value":2505},"ConcurrentDictionary\u003CTKey, TValue>",{"type":51,"value":2507}," keyed on the inputs that determine uniqueness — never on process state.",{"type":45,"tag":314,"props":2509,"children":2510},{},[2511,2513,2518],{"type":51,"value":2512},"For cross-repo helpers (foundation types used by many tasks), migrate the foundation in a dedicated PR before the tasks that consume it; the hydration step is usually where ",{"type":45,"tag":60,"props":2514,"children":2516},{"className":2515},[],[2517],{"type":51,"value":747},{"type":51,"value":2519}," is hiding.",{"type":45,"tag":92,"props":2521,"children":2523},{"id":2522},"tasks-instantiated-directly-by-other-tasks",[2524],{"type":51,"value":2525},"Tasks Instantiated Directly by Other Tasks",{"type":45,"tag":54,"props":2527,"children":2528},{},[2529,2534,2536,2542,2544,2550,2552,2564,2566,2571],{"type":45,"tag":60,"props":2530,"children":2532},{"className":2531},[],[2533],{"type":51,"value":214},{"type":51,"value":2535}," is only honored when the TaskFactory creates the task (i.e., a target invokes it as ",{"type":45,"tag":60,"props":2537,"children":2539},{"className":2538},[],[2540],{"type":51,"value":2541},"\u003CMyTask … \u002F>",{"type":51,"value":2543},"). A task created via ",{"type":45,"tag":60,"props":2545,"children":2547},{"className":2546},[],[2548],{"type":51,"value":2549},"new MyTask()",{"type":51,"value":2551}," inside another task's body bypasses the factory and ",{"type":45,"tag":118,"props":2553,"children":2554},{},[2555,2557,2562],{"type":51,"value":2556},"never gets ",{"type":45,"tag":60,"props":2558,"children":2560},{"className":2559},[],[2561],{"type":51,"value":73},{"type":51,"value":2563}," injected",{"type":51,"value":2565}," — it silently defaults to ",{"type":45,"tag":60,"props":2567,"children":2569},{"className":2568},[],[2570],{"type":51,"value":1214},{"type":51,"value":2572}," (= process CWD).",{"type":45,"tag":54,"props":2574,"children":2575},{},[2576,2578,2583,2585,2590],{"type":51,"value":2577},"Migrating a nested task in isolation is therefore meaningless. Either migrate the parent and have it explicitly propagate its ",{"type":45,"tag":60,"props":2579,"children":2581},{"className":2580},[],[2582],{"type":51,"value":73},{"type":51,"value":2584}," to the nested instance before calling ",{"type":45,"tag":60,"props":2586,"children":2588},{"className":2587},[],[2589],{"type":51,"value":1334},{"type":51,"value":2591},", or restructure so the nested task is a regular TaskFactory-created task.",{"type":45,"tag":54,"props":2593,"children":2594},{},[2595,2600,2602,2608,2610,2616],{"type":45,"tag":118,"props":2596,"children":2597},{},[2598],{"type":51,"value":2599},"Detect:",{"type":51,"value":2601}," grep for ",{"type":45,"tag":60,"props":2603,"children":2605},{"className":2604},[],[2606],{"type":51,"value":2607},"new …Task()",{"type":51,"value":2609}," followed by ",{"type":45,"tag":60,"props":2611,"children":2613},{"className":2612},[],[2614],{"type":51,"value":2615},".Execute()",{"type":51,"value":2617}," — every direct instantiation is a hole.",{"type":45,"tag":92,"props":2619,"children":2621},{"id":2620},"tooltask-override-hot-spots",[2622],{"type":51,"value":2623},"ToolTask Override Hot Spots",{"type":45,"tag":54,"props":2625,"children":2626},{},[2627,2633,2635,2640,2641,2646],{"type":45,"tag":60,"props":2628,"children":2630},{"className":2629},[],[2631],{"type":51,"value":2632},"ToolTask",{"type":51,"value":2634}," subclasses inherit virtual methods that run ",{"type":45,"tag":1115,"props":2636,"children":2637},{},[2638],{"type":51,"value":2639},"before or after",{"type":51,"value":116},{"type":45,"tag":60,"props":2642,"children":2644},{"className":2643},[],[2645],{"type":51,"value":1334},{"type":51,"value":2647}," and frequently touch the file system. Audit every override:",{"type":45,"tag":432,"props":2649,"children":2650},{},[2651,2672],{"type":45,"tag":436,"props":2652,"children":2653},{},[2654],{"type":45,"tag":440,"props":2655,"children":2656},{},[2657,2662,2667],{"type":45,"tag":444,"props":2658,"children":2659},{},[2660],{"type":51,"value":2661},"Override",{"type":45,"tag":444,"props":2663,"children":2664},{},[2665],{"type":51,"value":2666},"Hazard",{"type":45,"tag":444,"props":2668,"children":2669},{},[2670],{"type":51,"value":2671},"Migration",{"type":45,"tag":455,"props":2673,"children":2674},{},[2675,2705,2727,2755,2797],{"type":45,"tag":440,"props":2676,"children":2677},{},[2678,2687,2700],{"type":45,"tag":462,"props":2679,"children":2680},{},[2681],{"type":45,"tag":60,"props":2682,"children":2684},{"className":2683},[],[2685],{"type":51,"value":2686},"GenerateFullPathToTool()",{"type":45,"tag":462,"props":2688,"children":2689},{},[2690,2692,2698],{"type":51,"value":2691},"Builds ",{"type":45,"tag":60,"props":2693,"children":2695},{"className":2694},[],[2696],{"type":51,"value":2697},"ProcessStartInfo.FileName",{"type":51,"value":2699}," — relative tool path → wrong tool launched",{"type":45,"tag":462,"props":2701,"children":2702},{},[2703],{"type":51,"value":2704},"Absolutize the tool path before returning",{"type":45,"tag":440,"props":2706,"children":2707},{},[2708,2717,2722],{"type":45,"tag":462,"props":2709,"children":2710},{},[2711],{"type":45,"tag":60,"props":2712,"children":2714},{"className":2713},[],[2715],{"type":51,"value":2716},"SkipTaskExecution()",{"type":45,"tag":462,"props":2718,"children":2719},{},[2720],{"type":51,"value":2721},"Up-to-date check on input\u002Foutput timestamps using relative paths",{"type":45,"tag":462,"props":2723,"children":2724},{},[2725],{"type":51,"value":2726},"Absolutize both sides of the comparison",{"type":45,"tag":440,"props":2728,"children":2729},{},[2730,2739,2750],{"type":45,"tag":462,"props":2731,"children":2732},{},[2733],{"type":45,"tag":60,"props":2734,"children":2736},{"className":2735},[],[2737],{"type":51,"value":2738},"ValidateParameters()",{"type":45,"tag":462,"props":2740,"children":2741},{},[2742,2748],{"type":45,"tag":60,"props":2743,"children":2745},{"className":2744},[],[2746],{"type":51,"value":2747},"File.Exists",{"type":51,"value":2749}," on input parameters",{"type":45,"tag":462,"props":2751,"children":2752},{},[2753],{"type":51,"value":2754},"Absolutize before probing",{"type":45,"tag":440,"props":2756,"children":2757},{},[2758,2774,2779],{"type":45,"tag":462,"props":2759,"children":2760},{},[2761,2767,2768],{"type":45,"tag":60,"props":2762,"children":2764},{"className":2763},[],[2765],{"type":51,"value":2766},"GenerateResponseFileCommands()",{"type":51,"value":1716},{"type":45,"tag":60,"props":2769,"children":2771},{"className":2770},[],[2772],{"type":51,"value":2773},"GenerateCommandLineCommands()",{"type":45,"tag":462,"props":2775,"children":2776},{},[2777],{"type":51,"value":2778},"Tempting to absolutize args",{"type":45,"tag":462,"props":2780,"children":2781},{},[2782,2787,2789,2795],{"type":45,"tag":118,"props":2783,"children":2784},{},[2785],{"type":51,"value":2786},"Don't",{"type":51,"value":2788}," — the child's ",{"type":45,"tag":60,"props":2790,"children":2792},{"className":2791},[],[2793],{"type":51,"value":2794},"WorkingDirectory",{"type":51,"value":2796}," is the project dir, so relative args resolve correctly; absolutizing inflates user-visible output and can leak into tool diagnostics or generated artifacts",{"type":45,"tag":440,"props":2798,"children":2799},{},[2800,2809,2821],{"type":45,"tag":462,"props":2801,"children":2802},{},[2803],{"type":45,"tag":60,"props":2804,"children":2806},{"className":2805},[],[2807],{"type":51,"value":2808},"GetWorkingDirectory()",{"type":45,"tag":462,"props":2810,"children":2811},{},[2812,2814,2819],{"type":51,"value":2813},"Default ",{"type":45,"tag":60,"props":2815,"children":2817},{"className":2816},[],[2818],{"type":51,"value":1370},{"type":51,"value":2820}," → child inherits host CWD",{"type":45,"tag":462,"props":2822,"children":2823},{},[2824,2826,2832,2834],{"type":51,"value":2825},"Leave alone if you use ",{"type":45,"tag":60,"props":2827,"children":2829},{"className":2828},[],[2830],{"type":51,"value":2831},"TaskEnvironment.GetProcessStartInfo()",{"type":51,"value":2833},"; it already sets ",{"type":45,"tag":60,"props":2835,"children":2837},{"className":2836},[],[2838],{"type":51,"value":2794},{"type":45,"tag":54,"props":2840,"children":2841},{},[2842,2847,2848,2853,2855,2860,2861,2866],{"type":45,"tag":118,"props":2843,"children":2844},{},[2845],{"type":51,"value":2846},"Key contract:",{"type":51,"value":116},{"type":45,"tag":60,"props":2849,"children":2851},{"className":2850},[],[2852],{"type":51,"value":2697},{"type":51,"value":2854}," is resolved by the OS ",{"type":45,"tag":118,"props":2856,"children":2857},{},[2858],{"type":51,"value":2859},"before",{"type":51,"value":116},{"type":45,"tag":60,"props":2862,"children":2864},{"className":2863},[],[2865],{"type":51,"value":2794},{"type":51,"value":2867}," takes effect, so the executable path must be absolute. Tool arguments are interpreted by the child process in its working directory, so they should stay relative.",{"type":45,"tag":92,"props":2869,"children":2871},{"id":2870},"engine-owned-environment-variables-are-immutable-in-mt-mode",[2872],{"type":51,"value":2873},"Engine-Owned Environment Variables Are Immutable in MT Mode",{"type":45,"tag":54,"props":2875,"children":2876},{},[2877,2879,2883,2885,2891],{"type":51,"value":2878},"Env vars consumed by the engine ",{"type":45,"tag":1115,"props":2880,"children":2881},{},[2882],{"type":51,"value":2859},{"type":51,"value":2884}," tasks run (e.g., ",{"type":45,"tag":60,"props":2886,"children":2888},{"className":2887},[],[2889],{"type":51,"value":2890},"MSBUILD*",{"type":51,"value":2892},"-prefixed flags and the framework\u002FSDK discovery variables) are snapshotted into engine caches at startup. Tasks that mutate them later have no observable effect on the engine — but the mutation appears to succeed locally, masking bugs.",{"type":45,"tag":54,"props":2894,"children":2895},{},[2896,2898,2904],{"type":51,"value":2897},"If a migrated task previously mutated such a variable to influence a downstream tool, re-architect to pass the value via ",{"type":45,"tag":60,"props":2899,"children":2901},{"className":2900},[],[2902],{"type":51,"value":2903},"ProcessStartInfo.EnvironmentVariables",{"type":51,"value":2905}," on the child process instead of mutating the parent's environment.",{"type":45,"tag":1305,"props":2907,"children":2908},{},[],{"type":45,"tag":85,"props":2910,"children":2912},{"id":2911},"test-patterns-for-mt-migrations",[2913],{"type":51,"value":2914},"Test Patterns for MT Migrations",{"type":45,"tag":54,"props":2916,"children":2917},{},[2918,2920,2925],{"type":51,"value":2919},"A migration test must ",{"type":45,"tag":118,"props":2921,"children":2922},{},[2923],{"type":51,"value":2924},"fail when the migration is undone",{"type":51,"value":2926},". Tests that pass identically against pre- and post-migration code are theater. The two patterns below are the ones that reliably exercise MT-specific behavior.",{"type":45,"tag":92,"props":2928,"children":2930},{"id":2929},"pattern-a-decoy-cwd-test-for-tasks-that-touch-the-file-system",[2931],{"type":51,"value":2932},"Pattern A: Decoy-CWD Test (for tasks that touch the file system)",{"type":45,"tag":54,"props":2934,"children":2935},{},[2936,2938,2943,2945,2951],{"type":51,"value":2937},"Set the process working directory to a ",{"type":45,"tag":118,"props":2939,"children":2940},{},[2941],{"type":51,"value":2942},"decoy",{"type":51,"value":2944}," dir with no relevant files; set ",{"type":45,"tag":60,"props":2946,"children":2948},{"className":2947},[],[2949],{"type":51,"value":2950},"TaskEnvironment.ProjectDirectory",{"type":51,"value":2952}," to a different dir with the inputs\u002Fexpected outputs. The task must read\u002Fwrite against the project directory, not the decoy.",{"type":45,"tag":133,"props":2954,"children":2956},{"className":135,"code":2955,"language":137,"meta":138,"style":138},"using TestEnvironment env = TestEnvironment.Create();\nTransientTestFolder projectDir = env.CreateFolder();\nTransientTestFolder decoyCwd = env.CreateFolder();\nFile.WriteAllText(Path.Combine(projectDir.Path, \"input.txt\"), \"expected\");\n\nenv.SetCurrentDirectory(decoyCwd.Path); \u002F\u002F auto-restored when env is disposed\n\nvar task = new MyTask\n{\n    TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path),\n    Input = \"input.txt\", \u002F\u002F relative\n};\ntask.Execute().ShouldBeTrue();\n",[2957],{"type":45,"tag":60,"props":2958,"children":2959},{"__ignoreMap":138},[2960,2968,2976,2984,2992,2999,3007,3014,3022,3029,3037,3045,3053],{"type":45,"tag":144,"props":2961,"children":2962},{"class":146,"line":147},[2963],{"type":45,"tag":144,"props":2964,"children":2965},{},[2966],{"type":51,"value":2967},"using TestEnvironment env = TestEnvironment.Create();\n",{"type":45,"tag":144,"props":2969,"children":2970},{"class":146,"line":156},[2971],{"type":45,"tag":144,"props":2972,"children":2973},{},[2974],{"type":51,"value":2975},"TransientTestFolder projectDir = env.CreateFolder();\n",{"type":45,"tag":144,"props":2977,"children":2978},{"class":146,"line":165},[2979],{"type":45,"tag":144,"props":2980,"children":2981},{},[2982],{"type":51,"value":2983},"TransientTestFolder decoyCwd = env.CreateFolder();\n",{"type":45,"tag":144,"props":2985,"children":2986},{"class":146,"line":174},[2987],{"type":45,"tag":144,"props":2988,"children":2989},{},[2990],{"type":51,"value":2991},"File.WriteAllText(Path.Combine(projectDir.Path, \"input.txt\"), \"expected\");\n",{"type":45,"tag":144,"props":2993,"children":2994},{"class":146,"line":183},[2995],{"type":45,"tag":144,"props":2996,"children":2997},{"emptyLinePlaceholder":1448},[2998],{"type":51,"value":1451},{"type":45,"tag":144,"props":3000,"children":3001},{"class":146,"line":192},[3002],{"type":45,"tag":144,"props":3003,"children":3004},{},[3005],{"type":51,"value":3006},"env.SetCurrentDirectory(decoyCwd.Path); \u002F\u002F auto-restored when env is disposed\n",{"type":45,"tag":144,"props":3008,"children":3009},{"class":146,"line":988},[3010],{"type":45,"tag":144,"props":3011,"children":3012},{"emptyLinePlaceholder":1448},[3013],{"type":51,"value":1451},{"type":45,"tag":144,"props":3015,"children":3016},{"class":146,"line":997},[3017],{"type":45,"tag":144,"props":3018,"children":3019},{},[3020],{"type":51,"value":3021},"var task = new MyTask\n",{"type":45,"tag":144,"props":3023,"children":3024},{"class":146,"line":1006},[3025],{"type":45,"tag":144,"props":3026,"children":3027},{},[3028],{"type":51,"value":171},{"type":45,"tag":144,"props":3030,"children":3031},{"class":146,"line":1015},[3032],{"type":45,"tag":144,"props":3033,"children":3034},{},[3035],{"type":51,"value":3036},"    TaskEnvironment = TaskEnvironment.CreateWithProjectDirectoryAndEnvironment(projectDir.Path),\n",{"type":45,"tag":144,"props":3038,"children":3039},{"class":146,"line":1023},[3040],{"type":45,"tag":144,"props":3041,"children":3042},{},[3043],{"type":51,"value":3044},"    Input = \"input.txt\", \u002F\u002F relative\n",{"type":45,"tag":144,"props":3046,"children":3047},{"class":146,"line":1032},[3048],{"type":45,"tag":144,"props":3049,"children":3050},{},[3051],{"type":51,"value":3052},"};\n",{"type":45,"tag":144,"props":3054,"children":3055},{"class":146,"line":1041},[3056],{"type":45,"tag":144,"props":3057,"children":3058},{},[3059],{"type":51,"value":3060},"task.Execute().ShouldBeTrue();\n",{"type":45,"tag":54,"props":3062,"children":3063},{},[3064,3069,3070,3076,3078,3083,3085,3091,3093,3099,3101,3107],{"type":45,"tag":118,"props":3065,"children":3066},{},[3067],{"type":51,"value":3068},"CRITICAL:",{"type":51,"value":116},{"type":45,"tag":60,"props":3071,"children":3073},{"className":3072},[],[3074],{"type":51,"value":3075},"Directory.SetCurrentDirectory",{"type":51,"value":3077}," is ",{"type":45,"tag":118,"props":3079,"children":3080},{},[3081],{"type":51,"value":3082},"process-global",{"type":51,"value":3084},". xUnit parallelizes tests within a class by default — a CWD-mutating test will flake unless pinned to a non-parallel collection (",{"type":45,"tag":60,"props":3086,"children":3088},{"className":3087},[],[3089],{"type":51,"value":3090},"[Collection]",{"type":51,"value":3092}," + ",{"type":45,"tag":60,"props":3094,"children":3096},{"className":3095},[],[3097],{"type":51,"value":3098},"[CollectionDefinition(DisableParallelization = true)]",{"type":51,"value":3100},"). The ",{"type":45,"tag":60,"props":3102,"children":3104},{"className":3103},[],[3105],{"type":51,"value":3106},"IDisposable",{"type":51,"value":3108}," restore protects sequential safety but not concurrent siblings.",{"type":45,"tag":92,"props":3110,"children":3112},{"id":3111},"pattern-b-cross-instance-independence-for-tasks-doing-relative-path-resolution",[3113],{"type":51,"value":3114},"Pattern B: Cross-Instance Independence (for tasks doing relative-path resolution)",{"type":45,"tag":54,"props":3116,"children":3117},{},[3118,3120,3125],{"type":51,"value":3119},"Two task instances, two ",{"type":45,"tag":60,"props":3121,"children":3123},{"className":3122},[],[3124],{"type":51,"value":2950},{"type":51,"value":3126}," values, same relative input. Assert each task's output is rooted to its own project directory — proving resolution is per-instance, not shared.",{"type":45,"tag":92,"props":3128,"children":3130},{"id":3129},"when-not-to-write-a-migration-test",[3131],{"type":51,"value":3132},"When NOT to Write a \"Migration Test\"",{"type":45,"tag":54,"props":3134,"children":3135},{},[3136,3138,3143,3145,3150,3152,3157,3159,3164],{"type":51,"value":3137},"Attribute-only migrations (just ",{"type":45,"tag":60,"props":3139,"children":3141},{"className":3140},[],[3142],{"type":51,"value":214},{"type":51,"value":3144},", no ",{"type":45,"tag":60,"props":3146,"children":3148},{"className":3147},[],[3149],{"type":51,"value":81},{"type":51,"value":3151},") on tasks whose ",{"type":45,"tag":60,"props":3153,"children":3155},{"className":3154},[],[3156],{"type":51,"value":1334},{"type":51,"value":3158}," body contains no file\u002Fenv\u002Fprocess\u002Fstatic-state interactions have nothing to test — a decoy-CWD test would pass whether the attribute is present or not. ",{"type":45,"tag":118,"props":3160,"children":3161},{},[3162],{"type":51,"value":3163},"Don't write the test.",{"type":51,"value":3165}," Document the call-chain audit conclusion in the PR description instead.",{"type":45,"tag":92,"props":3167,"children":3169},{"id":3168},"test-hygiene",[3170],{"type":51,"value":3171},"Test Hygiene",{"type":45,"tag":310,"props":3173,"children":3174},{},[3175,3209],{"type":45,"tag":314,"props":3176,"children":3177},{},[3178,3179,3185,3186,3192,3194,3200,3202,3208],{"type":51,"value":900},{"type":45,"tag":60,"props":3180,"children":3182},{"className":3181},[],[3183],{"type":51,"value":3184},"TestEnvironment",{"type":51,"value":1716},{"type":45,"tag":60,"props":3187,"children":3189},{"className":3188},[],[3190],{"type":51,"value":3191},"TransientTestFolder",{"type":51,"value":3193}," for temp dirs; never ",{"type":45,"tag":60,"props":3195,"children":3197},{"className":3196},[],[3198],{"type":51,"value":3199},"Path.GetTempPath() + Guid.NewGuid()",{"type":51,"value":3201}," with manual ",{"type":45,"tag":60,"props":3203,"children":3205},{"className":3204},[],[3206],{"type":51,"value":3207},"try\u002Ffinally",{"type":51,"value":2111},{"type":45,"tag":314,"props":3210,"children":3211},{},[3212,3214,3220],{"type":51,"value":3213},"Hard-code expected assertion values; don't recompute them from ",{"type":45,"tag":60,"props":3215,"children":3217},{"className":3216},[],[3218],{"type":51,"value":3219},"Path.Combine(...)",{"type":51,"value":3221}," in the assertion — when the test fails you want to know what was actually wrong.",{"type":45,"tag":1305,"props":3223,"children":3224},{},[],{"type":45,"tag":85,"props":3226,"children":3228},{"id":3227},"red-team-audit-protocol",[3229],{"type":51,"value":3230},"Red-Team Audit Protocol",{"type":45,"tag":92,"props":3232,"children":3234},{"id":3233},"phase-1-trace-every-changed-line",[3235],{"type":51,"value":3236},"Phase 1: Trace Every Changed Line",{"type":45,"tag":54,"props":3238,"children":3239},{},[3240],{"type":51,"value":3241},"For each modified line: What was the exact runtime value before? After? Where does it flow (outputs, logs, file paths, dictionary keys)? Does each destination produce identical observable behavior?",{"type":45,"tag":92,"props":3243,"children":3245},{"id":3244},"phase-2-nullemptyedge-input-analysis",[3246],{"type":51,"value":3247},"Phase 2: Null\u002FEmpty\u002FEdge Input Analysis",{"type":45,"tag":432,"props":3249,"children":3250},{},[3251,3280],{"type":45,"tag":436,"props":3252,"children":3253},{},[3254],{"type":45,"tag":440,"props":3255,"children":3256},{},[3257,3262,3270,3275],{"type":45,"tag":444,"props":3258,"children":3259},{},[3260],{"type":51,"value":3261},"Input",{"type":45,"tag":444,"props":3263,"children":3264},{},[3265],{"type":45,"tag":60,"props":3266,"children":3268},{"className":3267},[],[3269],{"type":51,"value":1244},{"type":45,"tag":444,"props":3271,"children":3272},{},[3273],{"type":51,"value":3274},"Old behavior",{"type":45,"tag":444,"props":3276,"children":3277},{},[3278],{"type":51,"value":3279},"Match?",{"type":45,"tag":455,"props":3281,"children":3282},{},[3283,3312,3339,3367,3406,3441],{"type":45,"tag":440,"props":3284,"children":3285},{},[3286,3294,3302,3307],{"type":45,"tag":462,"props":3287,"children":3288},{},[3289],{"type":45,"tag":60,"props":3290,"children":3292},{"className":3291},[],[3293],{"type":51,"value":1370},{"type":45,"tag":462,"props":3295,"children":3296},{},[3297],{"type":45,"tag":60,"props":3298,"children":3300},{"className":3299},[],[3301],{"type":51,"value":406},{"type":45,"tag":462,"props":3303,"children":3304},{},[3305],{"type":51,"value":3306},"Varies",{"type":45,"tag":462,"props":3308,"children":3309},{},[3310],{"type":51,"value":3311},"❓",{"type":45,"tag":440,"props":3313,"children":3314},{},[3315,3323,3331,3335],{"type":45,"tag":462,"props":3316,"children":3317},{},[3318],{"type":45,"tag":60,"props":3319,"children":3321},{"className":3320},[],[3322],{"type":51,"value":1378},{"type":45,"tag":462,"props":3324,"children":3325},{},[3326],{"type":45,"tag":60,"props":3327,"children":3329},{"className":3328},[],[3330],{"type":51,"value":406},{"type":45,"tag":462,"props":3332,"children":3333},{},[3334],{"type":51,"value":3306},{"type":45,"tag":462,"props":3336,"children":3337},{},[3338],{"type":51,"value":3311},{"type":45,"tag":440,"props":3340,"children":3341},{},[3342,3353,3358,3362],{"type":45,"tag":462,"props":3343,"children":3344},{},[3345,3351],{"type":45,"tag":60,"props":3346,"children":3348},{"className":3347},[],[3349],{"type":51,"value":3350},"\"C:\\\"",{"type":51,"value":3352}," (root)",{"type":45,"tag":462,"props":3354,"children":3355},{},[3356],{"type":51,"value":3357},"Valid",{"type":45,"tag":462,"props":3359,"children":3360},{},[3361],{"type":51,"value":3357},{"type":45,"tag":462,"props":3363,"children":3364},{},[3365],{"type":51,"value":3366},"✅ usually",{"type":45,"tag":440,"props":3368,"children":3369},{},[3370,3379,3390,3401],{"type":45,"tag":462,"props":3371,"children":3372},{},[3373],{"type":45,"tag":60,"props":3374,"children":3376},{"className":3375},[],[3377],{"type":51,"value":3378},"\".\"",{"type":45,"tag":462,"props":3380,"children":3381},{},[3382,3388],{"type":45,"tag":60,"props":3383,"children":3385},{"className":3384},[],[3386],{"type":51,"value":3387},"\"C:\\repo\\.\"",{"type":51,"value":3389}," (not canonical)",{"type":45,"tag":462,"props":3391,"children":3392},{},[3393,3399],{"type":45,"tag":60,"props":3394,"children":3396},{"className":3395},[],[3397],{"type":51,"value":3398},"\"C:\\repo\"",{"type":51,"value":3400}," if GetFullPath",{"type":45,"tag":462,"props":3402,"children":3403},{},[3404],{"type":51,"value":3405},"❌ maybe",{"type":45,"tag":440,"props":3407,"children":3408},{},[3409,3418,3427,3437],{"type":45,"tag":462,"props":3410,"children":3411},{},[3412],{"type":45,"tag":60,"props":3413,"children":3415},{"className":3414},[],[3416],{"type":51,"value":3417},"\"foo\\..\\bar\"",{"type":45,"tag":462,"props":3419,"children":3420},{},[3421],{"type":45,"tag":60,"props":3422,"children":3424},{"className":3423},[],[3425],{"type":51,"value":3426},"\"C:\\repo\\foo\\..\\bar\"",{"type":45,"tag":462,"props":3428,"children":3429},{},[3430,3436],{"type":45,"tag":60,"props":3431,"children":3433},{"className":3432},[],[3434],{"type":51,"value":3435},"\"C:\\repo\\bar\"",{"type":51,"value":3400},{"type":45,"tag":462,"props":3438,"children":3439},{},[3440],{"type":51,"value":3405},{"type":45,"tag":440,"props":3442,"children":3443},{},[3444,3449,3454,3458],{"type":45,"tag":462,"props":3445,"children":3446},{},[3447],{"type":51,"value":3448},"Already absolute",{"type":45,"tag":462,"props":3450,"children":3451},{},[3452],{"type":51,"value":3453},"Pass-through",{"type":45,"tag":462,"props":3455,"children":3456},{},[3457],{"type":51,"value":3453},{"type":45,"tag":462,"props":3459,"children":3460},{},[3461],{"type":51,"value":3462},"✅",{"type":45,"tag":54,"props":3464,"children":3465},{},[3466,3472,3474,3480],{"type":45,"tag":60,"props":3467,"children":3469},{"className":3468},[],[3470],{"type":51,"value":3471},"Path.GetDirectoryName",{"type":51,"value":3473}," → ",{"type":45,"tag":60,"props":3475,"children":3477},{"className":3476},[],[3478],{"type":51,"value":3479},"Path.Combine",{"type":51,"value":3481}," chains:",{"type":45,"tag":432,"props":3483,"children":3484},{},[3485,3515],{"type":45,"tag":436,"props":3486,"children":3487},{},[3488],{"type":45,"tag":440,"props":3489,"children":3490},{},[3491,3495,3506],{"type":45,"tag":444,"props":3492,"children":3493},{},[3494],{"type":51,"value":3261},{"type":45,"tag":444,"props":3496,"children":3497},{},[3498,3504],{"type":45,"tag":60,"props":3499,"children":3501},{"className":3500},[],[3502],{"type":51,"value":3503},"GetDirectoryName",{"type":51,"value":3505}," returns",{"type":45,"tag":444,"props":3507,"children":3508},{},[3509],{"type":45,"tag":60,"props":3510,"children":3512},{"className":3511},[],[3513],{"type":51,"value":3514},"Path.Combine(result, x)",{"type":45,"tag":455,"props":3516,"children":3517},{},[3518,3548,3581],{"type":45,"tag":440,"props":3519,"children":3520},{},[3521,3529,3537],{"type":45,"tag":462,"props":3522,"children":3523},{},[3524],{"type":45,"tag":60,"props":3525,"children":3527},{"className":3526},[],[3528],{"type":51,"value":3350},{"type":45,"tag":462,"props":3530,"children":3531},{},[3532],{"type":45,"tag":60,"props":3533,"children":3535},{"className":3534},[],[3536],{"type":51,"value":1370},{"type":45,"tag":462,"props":3538,"children":3539},{},[3540,3542],{"type":51,"value":3541},"Throws ",{"type":45,"tag":60,"props":3543,"children":3545},{"className":3544},[],[3546],{"type":51,"value":3547},"ArgumentNullException",{"type":45,"tag":440,"props":3549,"children":3550},{},[3551,3559,3576],{"type":45,"tag":462,"props":3552,"children":3553},{},[3554],{"type":45,"tag":60,"props":3555,"children":3557},{"className":3556},[],[3558],{"type":51,"value":1378},{"type":45,"tag":462,"props":3560,"children":3561},{},[3562,3567,3569,3574],{"type":45,"tag":60,"props":3563,"children":3565},{"className":3564},[],[3566],{"type":51,"value":1378},{"type":51,"value":3568}," (.NET Fx) \u002F ",{"type":45,"tag":60,"props":3570,"children":3572},{"className":3571},[],[3573],{"type":51,"value":1370},{"type":51,"value":3575}," (.NET Core+)",{"type":45,"tag":462,"props":3577,"children":3578},{},[3579],{"type":51,"value":3580},"Works \u002F Throws ⚠️",{"type":45,"tag":440,"props":3582,"children":3583},{},[3584,3595,3603],{"type":45,"tag":462,"props":3585,"children":3586},{},[3587,3593],{"type":45,"tag":60,"props":3588,"children":3590},{"className":3589},[],[3591],{"type":51,"value":3592},"\"file.resx\"",{"type":51,"value":3594}," (no dir)",{"type":45,"tag":462,"props":3596,"children":3597},{},[3598],{"type":45,"tag":60,"props":3599,"children":3601},{"className":3600},[],[3602],{"type":51,"value":1378},{"type":45,"tag":462,"props":3604,"children":3605},{},[3606],{"type":51,"value":3607},"Works",{"type":45,"tag":54,"props":3609,"children":3610},{},[3611,3613,3618],{"type":51,"value":3612},"Verify behavior on ",{"type":45,"tag":118,"props":3614,"children":3615},{},[3616],{"type":51,"value":3617},"both",{"type":51,"value":3619}," .NET Framework and .NET TFMs.",{"type":45,"tag":92,"props":3621,"children":3623},{"id":3622},"phase-3-downstream-impact",[3624],{"type":51,"value":3625},"Phase 3: Downstream Impact",{"type":45,"tag":870,"props":3627,"children":3628},{},[3629,3646,3656,3680],{"type":45,"tag":314,"props":3630,"children":3631},{},[3632,3637,3639,3644],{"type":45,"tag":118,"props":3633,"children":3634},{},[3635],{"type":51,"value":3636},"Output properties",{"type":51,"value":3638},": What consumes this task's ",{"type":45,"tag":60,"props":3640,"children":3642},{"className":3641},[],[3643],{"type":51,"value":341},{"type":51,"value":3645},"? Does it compare, display, or use as a path?",{"type":45,"tag":314,"props":3647,"children":3648},{},[3649,3654],{"type":45,"tag":118,"props":3650,"children":3651},{},[3652],{"type":51,"value":3653},"Written files",{"type":51,"value":3655},": Does file content change? (e.g., XML with embedded paths)",{"type":45,"tag":314,"props":3657,"children":3658},{},[3659,3664,3666,3671,3672,3678],{"type":45,"tag":118,"props":3660,"children":3661},{},[3662],{"type":51,"value":3663},"Helper methods",{"type":51,"value":3665},": Do ",{"type":45,"tag":60,"props":3667,"children":3669},{"className":3668},[],[3670],{"type":51,"value":1849},{"type":51,"value":675},{"type":45,"tag":60,"props":3673,"children":3675},{"className":3674},[],[3676],{"type":51,"value":3677},"ManifestWriter",{"type":51,"value":3679},", etc. internally resolve relative paths?",{"type":45,"tag":314,"props":3681,"children":3682},{},[3683,3688],{"type":45,"tag":118,"props":3684,"children":3685},{},[3686],{"type":51,"value":3687},"Error codes",{"type":51,"value":3689},": MSBuild error codes (MSBxxxx) must be identical.",{"type":45,"tag":92,"props":3691,"children":3693},{"id":3692},"phase-4-concurrency",[3694],{"type":51,"value":3695},"Phase 4: Concurrency",{"type":45,"tag":870,"props":3697,"children":3698},{},[3699,3712,3717],{"type":45,"tag":314,"props":3700,"children":3701},{},[3702,3704,3710],{"type":51,"value":3703},"Two tasks with different ",{"type":45,"tag":60,"props":3705,"children":3707},{"className":3706},[],[3708],{"type":51,"value":3709},"ProjectDirectory",{"type":51,"value":3711}," values don't interfere",{"type":45,"tag":314,"props":3713,"children":3714},{},[3715],{"type":51,"value":3716},"No writes to static fields (shared across threads)",{"type":45,"tag":314,"props":3718,"children":3719},{},[3720],{"type":51,"value":3721},"All file operations use absolutized paths",{"type":45,"tag":85,"props":3723,"children":3725},{"id":3724},"compatibility-test-matrix",[3726],{"type":51,"value":3727},"Compatibility Test Matrix",{"type":45,"tag":133,"props":3729,"children":3733},{"className":3730,"code":3732,"language":51},[3731],"language-text","[Task] × [Input Type] × [Assertion]\n\nInputs: relative path, absolute path, null, empty, \"..\" segments, root \"C:\\\",\n        forward slashes, trailing separator, UNC path, 260+ char path\n\nAssertions: Execute() return value, [Output] exact string, error message content,\n            exception type, file location, file content\n",[3734],{"type":45,"tag":60,"props":3735,"children":3736},{"__ignoreMap":138},[3737],{"type":51,"value":3732},{"type":45,"tag":85,"props":3739,"children":3741},{"id":3740},"sign-off-checklist",[3742],{"type":51,"value":3743},"Sign-Off Checklist",{"type":45,"tag":310,"props":3745,"children":3748},{"className":3746},[3747],"contains-task-list",[3749,3774,3802,3818,3846,3867,3882,3898,3907,3930,3953,3968,3984,4098,4143,4166,4182,4199,4215,4224],{"type":45,"tag":314,"props":3750,"children":3753},{"className":3751},[3752],"task-list-item",[3754,3759,3760,3765,3767,3773],{"type":45,"tag":3755,"props":3756,"children":3758},"input",{"disabled":1448,"type":3757},"checkbox",[],{"type":51,"value":116},{"type":45,"tag":60,"props":3761,"children":3763},{"className":3762},[],[3764],{"type":51,"value":214},{"type":51,"value":3766}," on every concrete class (not just base — ",{"type":45,"tag":60,"props":3768,"children":3770},{"className":3769},[],[3771],{"type":51,"value":3772},"Inherited=false",{"type":51,"value":383},{"type":45,"tag":314,"props":3775,"children":3777},{"className":3776},[3752],[3778,3781,3782,3787,3789,3794,3796],{"type":45,"tag":3755,"props":3779,"children":3780},{"disabled":1448,"type":3757},[],{"type":51,"value":116},{"type":45,"tag":60,"props":3783,"children":3785},{"className":3784},[],[3786],{"type":51,"value":81},{"type":51,"value":3788}," on classes that use ",{"type":45,"tag":60,"props":3790,"children":3792},{"className":3791},[],[3793],{"type":51,"value":73},{"type":51,"value":3795}," APIs, with default initializer ",{"type":45,"tag":60,"props":3797,"children":3799},{"className":3798},[],[3800],{"type":51,"value":3801},"= TaskEnvironment.Fallback",{"type":45,"tag":314,"props":3803,"children":3805},{"className":3804},[3752],[3806,3809,3811,3816],{"type":45,"tag":3755,"props":3807,"children":3808},{"disabled":1448,"type":3757},[],{"type":51,"value":3810}," Every ",{"type":45,"tag":60,"props":3812,"children":3814},{"className":3813},[],[3815],{"type":51,"value":341},{"type":51,"value":3817}," property: exact string value matches pre-migration (Sin 1)",{"type":45,"tag":314,"props":3819,"children":3821},{"className":3820},[3752],[3822,3825,3826,3831,3832,3837,3839,3844],{"type":45,"tag":3755,"props":3823,"children":3824},{"disabled":1448,"type":3757},[],{"type":51,"value":3810},{"type":45,"tag":60,"props":3827,"children":3829},{"className":3828},[],[3830],{"type":51,"value":1684},{"type":51,"value":1686},{"type":45,"tag":60,"props":3833,"children":3835},{"className":3834},[],[3836],{"type":51,"value":1692},{"type":51,"value":3838},": path in message matches pre-migration (use ",{"type":45,"tag":60,"props":3840,"children":3842},{"className":3841},[],[3843],{"type":51,"value":333},{"type":51,"value":3845},") (Sin 2)",{"type":45,"tag":314,"props":3847,"children":3849},{"className":3848},[3752],[3850,3853,3854,3859,3860,3865],{"type":45,"tag":3755,"props":3851,"children":3852},{"disabled":1448,"type":3757},[],{"type":51,"value":3810},{"type":45,"tag":60,"props":3855,"children":3857},{"className":3856},[],[3858],{"type":51,"value":1714},{"type":51,"value":1716},{"type":45,"tag":60,"props":3861,"children":3863},{"className":3862},[],[3864],{"type":51,"value":1722},{"type":51,"value":3866},": exception path sanitized to original input (Sin 2)",{"type":45,"tag":314,"props":3868,"children":3870},{"className":3869},[3752],[3871,3874,3875,3880],{"type":45,"tag":3755,"props":3872,"children":3873},{"disabled":1448,"type":3757},[],{"type":51,"value":3810},{"type":45,"tag":60,"props":3876,"children":3878},{"className":3877},[],[3879],{"type":51,"value":1244},{"type":51,"value":3881}," call: null\u002Fempty exception behavior matches old code path (Sin 3, 6)",{"type":45,"tag":314,"props":3883,"children":3885},{"className":3884},[3752],[3886,3889,3891,3896],{"type":45,"tag":3755,"props":3887,"children":3888},{"disabled":1448,"type":3757},[],{"type":51,"value":3890}," Every dictionary\u002Fset with path keys: canonicalization preserved (",{"type":45,"tag":60,"props":3892,"children":3894},{"className":3893},[],[3895],{"type":51,"value":365},{"type":51,"value":3897},") (Sin 5)",{"type":45,"tag":314,"props":3899,"children":3901},{"className":3900},[3752],[3902,3905],{"type":45,"tag":3755,"props":3903,"children":3904},{"disabled":1448,"type":3757},[],{"type":51,"value":3906}," Every try-catch: absolutized value available in catch block where needed (Sin 4)",{"type":45,"tag":314,"props":3908,"children":3910},{"className":3909},[3752],[3911,3914,3915,3920,3922,3928],{"type":45,"tag":3755,"props":3912,"children":3913},{"disabled":1448,"type":3757},[],{"type":51,"value":3810},{"type":45,"tag":60,"props":3916,"children":3918},{"className":3917},[],[3919],{"type":51,"value":1818},{"type":51,"value":3921}," or ",{"type":45,"tag":60,"props":3923,"children":3925},{"className":3924},[],[3926],{"type":51,"value":3927},"?.",{"type":51,"value":3929}," added: verified it doesn't swallow a previously-thrown exception (Sin 3)",{"type":45,"tag":314,"props":3931,"children":3933},{"className":3932},[3752],[3934,3937,3939,3944,3946,3951],{"type":45,"tag":3755,"props":3935,"children":3936},{"disabled":1448,"type":3757},[],{"type":51,"value":3938}," No ",{"type":45,"tag":60,"props":3940,"children":3942},{"className":3941},[],[3943],{"type":51,"value":2186},{"type":51,"value":3945}," short-circuits around ",{"type":45,"tag":60,"props":3947,"children":3949},{"className":3948},[],[3950],{"type":51,"value":1244},{"type":51,"value":3952}," — call unconditionally (Sin 7)",{"type":45,"tag":314,"props":3954,"children":3956},{"className":3955},[3752],[3957,3960,3961,3966],{"type":45,"tag":3755,"props":3958,"children":3959},{"disabled":1448,"type":3757},[],{"type":51,"value":3938},{"type":45,"tag":60,"props":3962,"children":3964},{"className":3963},[],[3965],{"type":51,"value":306},{"type":51,"value":3967}," leaks into user-visible strings unintentionally",{"type":45,"tag":314,"props":3969,"children":3971},{"className":3970},[3752],[3972,3975,3977,3982],{"type":45,"tag":3755,"props":3973,"children":3974},{"disabled":1448,"type":3757},[],{"type":51,"value":3976}," No behavior conditioned on \"MT mode on\u002Foff\" — ",{"type":45,"tag":60,"props":3978,"children":3980},{"className":3979},[],[3981],{"type":51,"value":1214},{"type":51,"value":3983}," handles single-process case",{"type":45,"tag":314,"props":3985,"children":3987},{"className":3986},[3752],[3988,3991,3992,3997,3999,4004,4006],{"type":45,"tag":3755,"props":3989,"children":3990},{"disabled":1448,"type":3757},[],{"type":51,"value":116},{"type":45,"tag":118,"props":3993,"children":3994},{},[3995],{"type":51,"value":3996},"Call chain traced end-to-end",{"type":51,"value":3998}," for every helper invoked from ",{"type":45,"tag":60,"props":4000,"children":4002},{"className":4001},[],[4003],{"type":51,"value":1334},{"type":51,"value":4005},":\n",{"type":45,"tag":310,"props":4007,"children":4008},{},[4009,4033,4052,4071],{"type":45,"tag":314,"props":4010,"children":4011},{},[4012,4014,4019,4020,4025,4026,4031],{"type":51,"value":4013},"No ",{"type":45,"tag":60,"props":4015,"children":4017},{"className":4016},[],[4018],{"type":51,"value":733},{"type":51,"value":1716},{"type":45,"tag":60,"props":4021,"children":4023},{"className":4022},[],[4024],{"type":51,"value":2335},{"type":51,"value":1716},{"type":45,"tag":60,"props":4027,"children":4029},{"className":4028},[],[4030],{"type":51,"value":2343},{"type":51,"value":4032}," without a base anywhere in the transitive call graph",{"type":45,"tag":314,"props":4034,"children":4035},{},[4036,4038,4044,4046,4051],{"type":51,"value":4037},"No direct ",{"type":45,"tag":60,"props":4039,"children":4041},{"className":4040},[],[4042],{"type":51,"value":4043},"Environment.Get\u002FSetEnvironmentVariable",{"type":51,"value":4045}," (route through ",{"type":45,"tag":60,"props":4047,"children":4049},{"className":4048},[],[4050],{"type":51,"value":73},{"type":51,"value":383},{"type":45,"tag":314,"props":4053,"children":4054},{},[4055,4056,4061,4063,4069],{"type":51,"value":4013},{"type":45,"tag":60,"props":4057,"children":4059},{"className":4058},[],[4060],{"type":51,"value":2376},{"type":51,"value":4062}," mutable fields seeded from process state; replace with ",{"type":45,"tag":60,"props":4064,"children":4066},{"className":4065},[],[4067],{"type":51,"value":4068},"ConcurrentDictionary",{"type":51,"value":4070}," keyed on inputs",{"type":45,"tag":314,"props":4072,"children":4073},{},[4074,4075,4080,4081,4086,4087,4092,4093],{"type":51,"value":4013},{"type":45,"tag":60,"props":4076,"children":4078},{"className":4077},[],[4079],{"type":51,"value":702},{"type":51,"value":675},{"type":45,"tag":60,"props":4082,"children":4084},{"className":4083},[],[4085],{"type":51,"value":673},{"type":51,"value":675},{"type":45,"tag":60,"props":4088,"children":4090},{"className":4089},[],[4091],{"type":51,"value":688},{"type":51,"value":675},{"type":45,"tag":60,"props":4094,"children":4096},{"className":4095},[],[4097],{"type":51,"value":681},{"type":45,"tag":314,"props":4099,"children":4101},{"className":4100},[3752],[4102,4105,4107,4113,4114,4120,4121,4127,4129,4134,4136,4141],{"type":45,"tag":3755,"props":4103,"children":4104},{"disabled":1448,"type":3757},[],{"type":51,"value":4106}," ToolTask overrides audited (",{"type":45,"tag":60,"props":4108,"children":4110},{"className":4109},[],[4111],{"type":51,"value":4112},"GenerateFullPathToTool",{"type":51,"value":675},{"type":45,"tag":60,"props":4115,"children":4117},{"className":4116},[],[4118],{"type":51,"value":4119},"SkipTaskExecution",{"type":51,"value":675},{"type":45,"tag":60,"props":4122,"children":4124},{"className":4123},[],[4125],{"type":51,"value":4126},"ValidateParameters",{"type":51,"value":4128},"); ",{"type":45,"tag":60,"props":4130,"children":4132},{"className":4131},[],[4133],{"type":51,"value":2697},{"type":51,"value":4135}," is absolute, tool ",{"type":45,"tag":1115,"props":4137,"children":4138},{},[4139],{"type":51,"value":4140},"arguments",{"type":51,"value":4142}," stay relative",{"type":45,"tag":314,"props":4144,"children":4146},{"className":4145},[3752],[4147,4150,4152,4157,4159,4164],{"type":45,"tag":3755,"props":4148,"children":4149},{"disabled":1448,"type":3757},[],{"type":51,"value":4151}," No nested tasks created via ",{"type":45,"tag":60,"props":4153,"children":4155},{"className":4154},[],[4156],{"type":51,"value":2607},{"type":51,"value":4158}," without explicit ",{"type":45,"tag":60,"props":4160,"children":4162},{"className":4161},[],[4163],{"type":51,"value":73},{"type":51,"value":4165}," propagation",{"type":45,"tag":314,"props":4167,"children":4169},{"className":4168},[3752],[4170,4173,4175,4180],{"type":45,"tag":3755,"props":4171,"children":4172},{"disabled":1448,"type":3757},[],{"type":51,"value":4174}," No mutations of engine-owned env vars (e.g. ",{"type":45,"tag":60,"props":4176,"children":4178},{"className":4177},[],[4179],{"type":51,"value":2890},{"type":51,"value":4181}," and the framework\u002FSDK discovery vars) in MT mode",{"type":45,"tag":314,"props":4183,"children":4185},{"className":4184},[3752],[4186,4189,4191,4197],{"type":45,"tag":3755,"props":4187,"children":4188},{"disabled":1448,"type":3757},[],{"type":51,"value":4190}," Tests for custom tasks set ",{"type":45,"tag":60,"props":4192,"children":4194},{"className":4193},[],[4195],{"type":51,"value":4196},"TaskEnvironment = TaskEnvironmentHelper.CreateForTest()",{"type":51,"value":4198}," (built-in tasks have a default)",{"type":45,"tag":314,"props":4200,"children":4202},{"className":4201},[3752],[4203,4206,4208,4213],{"type":45,"tag":3755,"props":4204,"children":4205},{"disabled":1448,"type":3757},[],{"type":51,"value":4207}," Migration test follows Pattern A (decoy-CWD) ",{"type":45,"tag":118,"props":4209,"children":4210},{},[4211],{"type":51,"value":4212},"or",{"type":51,"value":4214}," Pattern B (cross-instance independence), or PR explains why no test is meaningful",{"type":45,"tag":314,"props":4216,"children":4218},{"className":4217},[3752],[4219,4222],{"type":45,"tag":3755,"props":4220,"children":4221},{"disabled":1448,"type":3757},[],{"type":51,"value":4223}," CWD-mutating tests pinned to a non-parallel xUnit collection",{"type":45,"tag":314,"props":4225,"children":4227},{"className":4226},[3752],[4228,4231],{"type":45,"tag":3755,"props":4229,"children":4230},{"disabled":1448,"type":3757},[],{"type":51,"value":4232}," Cross-framework: tested on .NET Framework and .NET TFMs.",{"type":45,"tag":4234,"props":4235,"children":4236},"style",{},[4237],{"type":51,"value":4238},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"items":4240,"total":147},[4241],{"slug":4,"name":4,"fn":5,"description":6,"org":4242,"tags":4243,"stars":22,"repoUrl":23,"updatedAt":24},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4244,4245,4246],{"name":17,"slug":18,"type":15},{"name":20,"slug":21,"type":15},{"name":13,"slug":14,"type":15},{"items":4248,"total":4402},[4249,4255,4272,4286,4304,4318,4337,4347,4358,4368,4381,4392],{"slug":4,"name":4,"fn":5,"description":6,"org":4250,"tags":4251,"stars":22,"repoUrl":23,"updatedAt":24},{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4252,4253,4254],{"name":17,"slug":18,"type":15},{"name":20,"slug":21,"type":15},{"name":13,"slug":14,"type":15},{"slug":4256,"name":4256,"fn":4257,"description":4258,"org":4259,"tags":4260,"stars":4269,"repoUrl":4270,"updatedAt":4271},"analyzing-dotnet-performance","analyze .NET code for performance anti-patterns","Scans .NET code for ~50 performance anti-patterns across async, memory, strings, collections, LINQ, regex, serialization, and I\u002FO with tiered severity classification. Use when analyzing .NET code for optimization opportunities, reviewing hot paths, or auditing allocation-heavy patterns.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4261,4262,4265,4268],{"name":17,"slug":18,"type":15},{"name":4263,"slug":4264,"type":15},"Code Analysis","code-analysis",{"name":4266,"slug":4267,"type":15},"Debugging","debugging",{"name":13,"slug":14,"type":15},4576,"https:\u002F\u002Fgithub.com\u002Fdotnet\u002Fskills","2026-07-12T08:23:25.400375",{"slug":4273,"name":4273,"fn":4274,"description":4275,"org":4276,"tags":4277,"stars":4269,"repoUrl":4270,"updatedAt":4285},"android-tombstone-symbolication","symbolicate .NET runtime frames in Android tombstones","Symbolicate the .NET runtime frames in an Android tombstone file. Extracts BuildIds and PC offsets from the native backtrace, downloads debug symbols from the Microsoft symbol server, and runs llvm-symbolizer to produce function names with source file and line numbers. USE FOR triaging a .NET MAUI or Mono Android app crash from a tombstone, resolving native backtrace frames in libmonosgen-2.0.so or libcoreclr.so to .NET runtime source code, or investigating SIGABRT, SIGSEGV, or other native signals originating from the .NET runtime on Android. DO NOT USE FOR pure Java\u002FKotlin crashes, managed .NET exceptions that are already captured in logcat, or iOS crash logs. INVOKES Symbolicate-Tombstone.ps1 script, llvm-symbolizer, Microsoft symbol server.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4278,4279,4282,4283],{"name":17,"slug":18,"type":15},{"name":4280,"slug":4281,"type":15},"Android","android",{"name":4266,"slug":4267,"type":15},{"name":4284,"slug":31,"type":15},"Microsoft","2026-07-12T08:23:21.595572",{"slug":4287,"name":4287,"fn":4288,"description":4289,"org":4290,"tags":4291,"stars":4269,"repoUrl":4270,"updatedAt":4303},"apple-crash-symbolication","symbolicate .NET runtime frames in crash logs","Symbolicate .NET runtime frames in Apple platform .ips crash logs (iOS, tvOS, Mac Catalyst, macOS). Extracts UUIDs and addresses from the native backtrace, locates dSYM debug symbols, and runs atos to produce function names with source file and line numbers. Automatically downloads .dwarf symbols from the Microsoft symbol server using Mach-O UUIDs. USE FOR triaging a .NET MAUI or Mono app crash from an .ips file on any Apple platform, resolving native backtrace frames in libcoreclr or libmonosgen-2.0 to .NET runtime source code, retrieving .ips crash logs from a connected iOS device or iPhone, or investigating EXC_CRASH, EXC_BAD_ACCESS, SIGABRT, or SIGSEGV originating from the .NET runtime. DO NOT USE FOR pure Swift\u002FObjective-C crashes with no .NET components, or Android tombstone files. INVOKES Symbolicate-Crash.ps1 script, atos, dwarfdump, idevicecrashreport.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4292,4293,4294,4297,4300],{"name":17,"slug":18,"type":15},{"name":4266,"slug":4267,"type":15},{"name":4295,"slug":4296,"type":15},"iOS","ios",{"name":4298,"slug":4299,"type":15},"macOS","macos",{"name":4301,"slug":4302,"type":15},"Observability","observability","2026-07-12T08:23:20.369986",{"slug":4305,"name":4305,"fn":4306,"description":4307,"org":4308,"tags":4309,"stars":4269,"repoUrl":4270,"updatedAt":4317},"assertion-quality","evaluate assertion quality in test suites","Analyzes the variety and depth of assertions across test suites in any language. Use when the user asks to evaluate assertion quality, find shallow tests, identify assertion-free tests (no assertions or only trivial ones like Assert.IsNotNull \u002F toBeTruthy()), flag self-referential or tautological assertions, measure assertion diversity, or audit whether tests verify different facets of behavior. Polyglot: .NET, Python, TS\u002FJS, Java, Go, Ruby, Rust, Swift, Kotlin, PowerShell, C++. DO NOT USE FOR: writing new tests (use code-testing-agent \u002F writing-mstest-tests), mutation reasoning about whether tests would catch a bug (use test-gap-analysis), or a general severity-ranked anti-pattern audit (use test-anti-patterns), fixing or rewriting assertions, or writing, fixing, or modernizing MSTest tests, assertions, or attributes (use writing-mstest-tests).",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4310,4311,4314],{"name":4263,"slug":4264,"type":15},{"name":4312,"slug":4313,"type":15},"QA","qa",{"name":4315,"slug":4316,"type":15},"Testing","testing","2026-07-12T08:23:51.277743",{"slug":4319,"name":4319,"fn":4320,"description":4321,"org":4322,"tags":4323,"stars":4269,"repoUrl":4270,"updatedAt":4336},"author-component","create and review Blazor components","Create or review Blazor components (.razor files) with correct architecture. USE FOR: writing new Blazor components that do NOT involve JavaScript interop, implementing parameters and EventCallback, RenderFragment slots, component lifecycle (OnInitializedAsync, OnParametersSet), async patterns, IAsyncDisposable, CancellationToken, CSS isolation, code-behind. DO NOT USE FOR: creating new projects (use create-blazor-project), JavaScript interop or calling browser APIs from Blazor (use use-js-interop), forms and validation (use collect-user-input), prerendering issues (use support-prerendering), HTTP data fetching patterns (use fetch-and-send-data), coordinating state between unrelated components (use coordinate-components).\n",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4324,4325,4328,4330,4333],{"name":17,"slug":18,"type":15},{"name":4326,"slug":4327,"type":15},"Blazor","blazor",{"name":4329,"slug":137,"type":15},"C#",{"name":4331,"slug":4332,"type":15},"UI Components","ui-components",{"name":4334,"slug":4335,"type":15},"Web Development","web-development","2026-07-15T06:03:29.216359",{"slug":4338,"name":4338,"fn":4339,"description":4340,"org":4341,"tags":4342,"stars":4269,"repoUrl":4270,"updatedAt":4346},"binlog-failure-analysis","analyze MSBuild binary logs","Analyze MSBuild binary logs to diagnose build failures. USE FOR: build errors that are unclear from console output, diagnosing cascading failures across multi-project builds, tracing MSBuild target execution order, and generally any MSBuild build issues. Requires an existing .binlog file. DO NOT USE FOR: generating binlogs (use binlog-generation), non-MSBuild build systems.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4343,4344,4345],{"name":4263,"slug":4264,"type":15},{"name":4266,"slug":4267,"type":15},{"name":4284,"slug":31,"type":15},"2026-07-12T08:21:34.637923",{"slug":4348,"name":4348,"fn":4349,"description":4350,"org":4351,"tags":4352,"stars":4269,"repoUrl":4270,"updatedAt":4357},"binlog-generation","generate MSBuild binary logs for diagnostics","Generate MSBuild binary logs (binlogs) for build diagnostics and analysis. USE FOR: adding \u002Fbl:{} to any dotnet build, test, pack, publish, or restore command to capture a full build execution trace, prerequisite for binlog-failure-analysis and build-perf-diagnostics skills, enabling post-build investigation of errors or performance. Requires MSBuild 17.8+ \u002F .NET 8 SDK+ for {} placeholder; PowerShell needs -bl:{{}}. DO NOT USE FOR: non-MSBuild build systems (npm, Maven, CMake), analyzing an existing binlog (use binlog-failure-analysis instead).",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4353,4355,4356],{"name":4354,"slug":28,"type":15},"Build",{"name":4266,"slug":4267,"type":15},{"name":20,"slug":21,"type":15},"2026-07-19T05:38:19.340791",{"slug":4359,"name":4359,"fn":4360,"description":4361,"org":4362,"tags":4363,"stars":4269,"repoUrl":4270,"updatedAt":4367},"build-parallelism","optimize MSBuild build parallelism","Diagnose and fix under-parallelized MSBuild builds. USE WHEN a multi-project solution build is slower than expected, doesn't speed up when you add cores, pegs a single core while others idle, or you want to know why `-m` isn't helping. Note: `\u002Fmaxcpucount` default is 1 (sequential) — always pass `-m` for parallel builds. Covers finding the critical path (longest serial ProjectReference chain), graph build (`\u002Fgraph`), BuildInParallel, and solution filters (`.slnf`). DO NOT USE FOR: single-project builds, incremental issues (use incremental-build), compilation slowness inside one project (use build-perf-diagnostics), non-MSBuild build systems.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4364,4365,4366],{"name":17,"slug":18,"type":15},{"name":20,"slug":21,"type":15},{"name":13,"slug":14,"type":15},"2026-07-19T05:38:18.364937",{"slug":4369,"name":4369,"fn":4370,"description":4371,"org":4372,"tags":4373,"stars":4269,"repoUrl":4270,"updatedAt":4380},"build-perf-baseline","establish and optimize build performance baselines","Establish build performance baselines and apply systematic optimization techniques. USE FOR: diagnosing slow builds, establishing before\u002Fafter measurements (cold, warm, no-op scenarios), applying optimization strategies like MSBuild Server, static graph builds, artifacts output, and dependency graph trimming. Start here before diving into build-perf-diagnostics, incremental-build, or build-parallelism. DO NOT USE FOR: non-MSBuild build systems, detailed bottleneck analysis (use build-perf-diagnostics after baselining).",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4374,4375,4378,4379],{"name":20,"slug":21,"type":15},{"name":4376,"slug":4377,"type":15},"Monitoring","monitoring",{"name":13,"slug":14,"type":15},{"name":4315,"slug":4316,"type":15},"2026-07-12T08:21:35.865649",{"slug":4382,"name":4382,"fn":4383,"description":4384,"org":4385,"tags":4386,"stars":4269,"repoUrl":4270,"updatedAt":4391},"build-perf-diagnostics","diagnose MSBuild build performance bottlenecks","Diagnose MSBuild build performance bottlenecks using binary log analysis. USE FOR: identifying why builds are slow by analyzing binlog performance summaries, detecting ResolveAssemblyReference (RAR) taking >5s, Roslyn analyzers consuming >30% of Csc time, single targets dominating >50% of build time, node utilization below 80%, excessive Copy tasks, NuGet restore running every build. Covers timeline analysis, Target\u002FTask Performance Summary interpretation, and 7 common bottleneck categories. Use after build-perf-baseline has established measurements. DO NOT USE FOR: establishing initial baselines (use build-perf-baseline first), fixing incremental build issues (use incremental-build), parallelism tuning (use build-parallelism), non-MSBuild build systems.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4387,4388,4389,4390],{"name":17,"slug":18,"type":15},{"name":4266,"slug":4267,"type":15},{"name":20,"slug":21,"type":15},{"name":13,"slug":14,"type":15},"2026-07-12T08:21:40.961722",{"slug":4393,"name":4393,"fn":4394,"description":4395,"org":4396,"tags":4397,"stars":4269,"repoUrl":4270,"updatedAt":4401},"check-bin-obj-clash","detect MSBuild output path conflicts","Detects MSBuild projects with conflicting OutputPath or IntermediateOutputPath. USE FOR: builds failing with 'Cannot create a file when that file already exists', 'The process cannot access the file because it is being used by another process', intermittent build failures that succeed on retry, or missing\u002Foverwritten outputs in multi-project or multi-targeting builds where bin\u002Fobj (or project.assets.json) collide. Common causes: shared OutputPath, missing AppendTargetFrameworkToOutputPath, extra global properties (e.g. PublishReadyToRun), or SetTargetFramework on a ProjectReference to a single-targeting project. DO NOT USE FOR: file access errors unrelated to MSBuild (OS-level locking), single-project single-TFM builds, non-MSBuild build systems.",{"slug":8,"name":9,"logoUrl":10,"githubOrg":8},[4398,4399,4400],{"name":4266,"slug":4267,"type":15},{"name":20,"slug":21,"type":15},{"name":4312,"slug":4313,"type":15},"2026-07-19T05:38:14.336279",144]