diff --git a/msbuild-coexistence.experiment.yaml b/msbuild-coexistence.experiment.yaml
index 69ca9c118f..05cd953ed4 100644
--- a/msbuild-coexistence.experiment.yaml
+++ b/msbuild-coexistence.experiment.yaml
@@ -36,6 +36,7 @@ variants:
- plugins/dotnet-msbuild/skills/eliminate-redundant-restores
- plugins/dotnet-msbuild/skills/eval-performance
- plugins/dotnet-msbuild/skills/extension-points
+ - plugins/dotnet-msbuild/skills/find-repeated-work-within-one-build
- plugins/dotnet-msbuild/skills/including-generated-files
- plugins/dotnet-msbuild/skills/incremental-build
- plugins/dotnet-msbuild/skills/item-management
diff --git a/plugins/dotnet-msbuild/skills/build-parallelism/SKILL.md b/plugins/dotnet-msbuild/skills/build-parallelism/SKILL.md
index 776c44e64d..7f54657dbf 100644
--- a/plugins/dotnet-msbuild/skills/build-parallelism/SKILL.md
+++ b/plugins/dotnet-msbuild/skills/build-parallelism/SKILL.md
@@ -1,6 +1,6 @@
---
name: build-parallelism
-description: "Analyze an MSBuild solution, solution filter, or Build.proj that schedules multiple project files. USE FOR: measured idle worker nodes, `-m` that does not scale, a serial ProjectReference critical path, graph builds, or an MSBuild task that builds a Projects list. Requires at least two distinct project files whose scheduling or throughput must be analyzed. DO NOT USE for non-MSBuild build systems."
+description: "Analyze MSBuild scheduling across at least two distinct projects in a solution/filter/Build.proj. USE FOR: measured idle workers, -m not scaling, serial ProjectReference critical paths, graph builds, MSBuild Projects-list scheduling. DO NOT USE FOR: duplicate executions or invariant-generation races (find-repeated-work-within-one-build), non-MSBuild systems."
license: MIT
---
diff --git a/plugins/dotnet-msbuild/skills/build-perf-baseline/SKILL.md b/plugins/dotnet-msbuild/skills/build-perf-baseline/SKILL.md
index c2b472eb7b..100b2f06d1 100644
--- a/plugins/dotnet-msbuild/skills/build-perf-baseline/SKILL.md
+++ b/plugins/dotnet-msbuild/skills/build-perf-baseline/SKILL.md
@@ -1,6 +1,6 @@
---
name: build-perf-baseline
-description: "Establish MSBuild/.NET build performance baselines before optimizing. USE FOR: unranked slow builds; cold, warm, incremental or no-op measurements; before/after comparisons; CI output caching; static graph, artifacts output and dependency graph trimming. Start here before build-perf-diagnostics, incremental-build or build-parallelism. DO NOT USE FOR: known repeated-restore fixes (use eliminate-redundant-restores), non-MSBuild builds. For measured target/task/analyzer bottlenecks, use build-perf-diagnostics."
+description: "Establish MSBuild/.NET baselines before optimizing. USE FOR: unranked slow builds; cold/warm/incremental/no-op measurements; before/after comparisons; CI caching; static graph, artifacts output, dependency graph trimming. DO NOT USE FOR: measured bottlenecks (build-perf-diagnostics), known repeated restores (eliminate-redundant-restores), within-build duplicate calls (find-repeated-work-within-one-build), non-MSBuild builds."
license: MIT
---
diff --git a/plugins/dotnet-msbuild/skills/build-perf-diagnostics/SKILL.md b/plugins/dotnet-msbuild/skills/build-perf-diagnostics/SKILL.md
index 901bc6c592..49ccda35fe 100644
--- a/plugins/dotnet-msbuild/skills/build-perf-diagnostics/SKILL.md
+++ b/plugins/dotnet-msbuild/skills/build-perf-diagnostics/SKILL.md
@@ -1,6 +1,6 @@
---
name: build-perf-diagnostics
-description: "Diagnose MSBuild bottlenecks from binlog timelines and Target/Task Performance Summaries after build-perf-baseline establishes measurements. USE FOR: unknown build slowness, RAR >5s, analyzers >30% of Csc, one target >50% of build time, node utilization <80%, excessive Copy tasks, finding restore overhead. DO NOT USE FOR: known repeated-restore fixes (use eliminate-redundant-restores), initial baselines (use build-perf-baseline), incremental rebuilds (use incremental-build), parallelism tuning (use build-parallelism), non-MSBuild builds."
+description: "Diagnose MSBuild bottlenecks from binlog timelines and Target/Task Performance Summaries after baseline measurements. USE FOR: unknown build slowness, RAR >5s, analyzers >30% of Csc, a target >50% of build, node utilization below 80%, excessive Copy, restore overhead. DO NOT USE FOR: within-build duplicates (find-repeated-work-within-one-build), known repeated restores (eliminate-redundant-restores), initial baselines (build-perf-baseline), incremental rebuilds (incremental-build), parallelism tuning (build-parallelism), non-MSBuild builds."
license: MIT
---
diff --git a/plugins/dotnet-msbuild/skills/check-bin-obj-clash/SKILL.md b/plugins/dotnet-msbuild/skills/check-bin-obj-clash/SKILL.md
index 48be612705..4d287d0fa4 100644
--- a/plugins/dotnet-msbuild/skills/check-bin-obj-clash/SKILL.md
+++ b/plugins/dotnet-msbuild/skills/check-bin-obj-clash/SKILL.md
@@ -1,6 +1,6 @@
---
name: check-bin-obj-clash
-description: "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/overwritten outputs in multi-project or multi-targeting builds where bin/obj (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."
+description: "Find OutputPath/IntermediateOutputPath collisions between distinct projects or required property/TFM variants. USE FOR: missing/overwritten bin/obj or project.assets.json, parallel 'Cannot create a file when that file already exists' or 'file is being used by another process', shared paths, missing AppendTargetFrameworkToOutputPath, PublishReadyToRun, SetTargetFramework on single-target references. DO NOT USE FOR: duplicates or invariant-generation races (find-repeated-work-within-one-build), OS locking, single-instance builds, non-MSBuild systems."
license: MIT
---
diff --git a/plugins/dotnet-msbuild/skills/eval-performance/SKILL.md b/plugins/dotnet-msbuild/skills/eval-performance/SKILL.md
index f29516f0c2..2f37cb8f15 100644
--- a/plugins/dotnet-msbuild/skills/eval-performance/SKILL.md
+++ b/plugins/dotnet-msbuild/skills/eval-performance/SKILL.md
@@ -1,6 +1,6 @@
---
name: eval-performance
-description: "Guide for diagnosing and improving MSBuild project evaluation performance. USE FOR: builds slow before any compilation starts, high evaluation time in binlog analysis, expensive glob patterns walking large directories (node_modules, .git, bin/obj), deep import chains (>20 levels), preprocessed output >10K lines indicating heavy evaluation, property functions with file I/O ($([System.IO.File]::ReadAllText(...))), multiple evaluations per project. Covers the 5 MSBuild evaluation phases, glob optimization via DefaultItemExcludes, import chain analysis with /pp preprocessing. DO NOT USE FOR: compilation-time slowness (use build-perf-diagnostics), incremental build issues (use incremental-build), non-MSBuild build systems."
+description: "Diagnose measured MSBuild evaluation cost before targets run. USE FOR: evaluation-only repeats under global-property variants, expensive node_modules/.git/bin/obj globs, import depth >20, /pp >10K lines, file-I/O property functions, five-phase evaluation. DO NOT USE FOR: duplicate executions (find-repeated-work-within-one-build), execution bottlenecks (build-perf-diagnostics), incremental rebuilds (incremental-build), non-MSBuild systems."
license: MIT
---
diff --git a/plugins/dotnet-msbuild/skills/find-repeated-work-within-one-build/SKILL.md b/plugins/dotnet-msbuild/skills/find-repeated-work-within-one-build/SKILL.md
new file mode 100644
index 0000000000..cfb8973fd2
--- /dev/null
+++ b/plugins/dotnet-msbuild/skills/find-repeated-work-within-one-build/SKILL.md
@@ -0,0 +1,117 @@
+---
+name: find-repeated-work-within-one-build
+description: "Use ONLY for auditing executed-task counts or batching/hoisting costs within ONE MSBuild invocation: per-project Exec, duplicate dependency builds, per-item generators, run-once races. NOT for second/unchanged builds, restores across commands, required-variant bin/obj collisions, evaluation-only repeats, unranked slowness or idle workers. Do not load this skill just to check scope or substitute for an unavailable specialist; answer directly instead."
+license: MIT
+---
+
+# Find Repeated Work Within One Build
+
+MSBuild runs targets once per project instance. Work that does not depend on the project, but sits in `Directory.Build.targets` or a shared target, therefore runs once per project: a `git rev-parse` in 40 projects is 40 process launches. A project built with different global properties is a different instance, so a shared library can compile several times in one build. The binlog's `N calls` column shows both. This skill finds the repeats, separates them from work that is legitimately per project, and removes only what is safe to remove.
+
+## When to Use
+
+- The same `Exec` or task (version stamping, tool probes, file generation) runs in every project
+- Target or Task Performance Summary shows a call count far above the number of projects that need the work
+- The same project executes its build targets repeatedly under global-property sets that do not change its outputs
+- An `Exec` spawns one process per item in a list
+- A "run once per build" guard (`Exists` check, flag property) still runs repeatedly or fails with file-in-use errors under `-m`
+
+## When Not to Use
+
+- Targets that run again on a second, unchanged build: use `incremental-build`
+- Slow evaluation before any target runs: use `eval-performance`
+- Required project variants colliding in shared bin/obj directories: use `check-bin-obj-clash`
+- Idle nodes or a serial dependency chain: use `build-parallelism`
+- NuGet restore repeated across separate `dotnet restore`, `dotnet build` and `dotnet test` commands: use `eliminate-redundant-restores`
+- Unranked "why is my build slow": start with `build-perf-baseline`, then `build-perf-diagnostics`
+
+## Workflow
+
+### Step 1: Count executions
+
+Use a supplied performance summary with execution counts as valid input; a raw binlog is not required. If the user cannot run the build or forbids execution or new logs, work from the summary and repository files. Mark unavailable timing or overlap evidence `unknown` and proposed fixes `not validated`; do not execute a build or collect another log.
+
+If neither usable execution counts nor a binlog is supplied, capture one only when build execution is available and permitted. Use the `binlog-generation` skill: add `/bl:{}` to the original MSBuild invocation, preserving its configuration, global properties, parallelism and restore behavior. In PowerShell, quote the complete switch as `'-bl:{}'`. If a script drives the build, instrument its underlying MSBuild invocation rather than replace it with a different build command. Confirm the binlog exists and record its path. If capture is not permitted, report the missing evidence instead of running the build.
+
+For an available binlog, prefer the bundled **binlog MCP server** (`Microsoft.AITools.BinlogMcp`, exposed under the `binlog` MCP namespace). Discover its available tools and query project, target and task execution counts and timings from that binlog. Do not read the binary file as text.
+
+If MCP is unavailable or its tools do not expose execution counts, replay the captured binlog to a text performance summary and read the `N calls` column of the Target and Task Performance Summaries and the Project Performance Summary:
+
+```bash
+dotnet msbuild build.binlog -noconlog "-flp:v=diag;logfile=full.log;performancesummary"
+```
+
+Count required distinct computations, not just consumers. Derive the expected count separately for each kind of work: `Csc` normally runs once per project and target framework, whereas copy-local work scales with files and can run in several targets. Project count times TFMs is not a universal ceiling for every task; leave an unsupported expected count `unknown` rather than claiming that a larger count is redundant. A multi-targeted project can legitimately show one outer build plus one inner build per TFM, so four project calls for three TFMs are not automatically duplicates. Account for required TFMs and global properties before investigating extra instances.
+
+An invariant tool-version or revision probe needs one computation per equivalent input scope, even when several projects consume its result. A target condition restricting it to a subset of projects does not make the result per-project. Confirm that inputs, working directories, relevant properties and output semantics are equivalent; identical command text alone does not prove invariance. In particular, fresh per-project signing identities are intentionally nondeterministic: do not cache, reuse or share one identity across projects or builds. Keep each required execution unless a documented tool mode can produce all the distinct results without changing the contract.
+
+### Step 2: Classify each candidate
+
+| Pattern | Signal | Fix | Never |
+|---|---|---|---|
+| Invariant per-project `Exec` or task (commit hash, tool version, generated file) | Multiple consumers need the same deterministic result under equivalent inputs | Compute it once outside the per-project targets: the build script passes `-p:Name=value`, or a pre-build step or orchestrator target runs first | A flag property or `Exists(stamp)` guard: properties are per project instance and `-m` worker nodes are separate processes, so the guard does not dedupe and races |
+| Same project built several times | One project path with N calls and different global properties (for example `Properties="ComponentName=%(Component.Filename)"` on an `MSBuild` task). Any extra global property, even an unused one, creates a new instance | Stop passing the property, or add `GlobalPropertiesToRemove` metadata on the shared `ProjectReference` (`RemoveProperties` on the `MSBuild` task) | Removing a property the project actually reads |
+| One process per item (`Exec` batched on `%(Item.Identity)`) | `Exec` calls equal the item count | One invocation that takes a list or response file, after checking the tool supports it; keep `Inputs`/`Outputs` (with a 1:1 transform MSBuild still passes only the stale items into the target) | Dropping `Inputs`/`Outputs` while merging |
+| Legitimate per-project, per-TFM or per-item work | Count matches the required project, framework or output-item scopes | Leave it alone | Applying one count ceiling to unrelated task kinds |
+
+### Step 3: Size the win
+
+For eliminated invariant computations, estimate avoidable aggregate task time as `(calls - expected) x average ms`. Per-item batching does not eliminate the required input processing: estimate `(old process starts - new process starts) x startup ms`, retaining every input's processing cost and any required metadata groups. Account for additional list/batch overhead when known. If startup cost is unavailable, mark that estimate unknown rather than treating the entire average task duration as removable.
+
+Wall-clock saving depends on actual overlap and the critical path. Batched per-item `Exec` calls in one target run sequentially on one node, so avoided startup time is not divided by unrelated busy nodes. Only divide by effective parallelism when the removed work actually overlaps across nodes. Use the binlog timeline to establish overlap, and mark wall-clock savings unknown when that evidence is missing. Work off the critical path can yield no wall-clock gain. For performance-only changes, if even the aggregate upper bound is below about 1% of build wall-clock, report it as not worth changing. Correctness races still need a fix regardless of the timing gain.
+
+For a cost-based no-change recommendation, show the avoidable aggregate-time bound and, when build elapsed time is supplied, `100 x bound / elapsed` as an upper-bound percentage, not a measured wall-clock gain. Distinguish work that is technically redundant but too cheap to change from work that must remain distinct.
+
+### Step 4: Fix without changing behavior
+
+1. Keep the value available before its first consumer.
+2. Keep a fallback `Condition="'$(X)' == ''"` for builds without the shared value. Imported or environment properties can satisfy it too; the value need not be global. Child builds inherit existing global properties unless removed, but not ordinary properties computed in their parent. Forward a parent-computed result through the `MSBuild` task's `Properties`, or load the shared value in each child before the fallback; otherwise each child can execute its own fallback.
+3. Define the computation in one place.
+4. Preserve generated-file timestamps when content is unchanged. Use a write-if-different generator, or generate to a temporary file and replace only on content change; deduplication must not force dependent recompilation on an unchanged rerun.
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+If Source Link already supplies `SourceRevisionId`, it can replace an `Exec` only when it is available before the consumer and preserves the existing stamp representation. `SourceRevisionId` normally contains the full commit ID; do not substitute it for `git rev-parse --short HEAD` without retaining the original abbreviation contract.
+
+### Step 5: Validate and report truthfully
+
+1. When execution is available and permitted, capture a new binlog of the same invocation. Calls must have dropped to the expected count. Otherwise, do not run the build or collect another log; report the fix as proposed and not validated.
+2. Confirm outputs are unchanged (the stamped value, generated file contents). For generated sources, also check that an unchanged second build stays incremental.
+3. Report timing as the median of at least three runs; if you could not run the build, say "not validated".
+
+## Output
+
+For on-target requests, keep the answer proportional to the evidence: a compact findings table with at least one row, a short explanation of the decisive computation or preservation constraint, and a standalone, unformatted `Verdict:` line. Do not bold the line or append it to a paragraph. Use `unknown` for unavailable timing or critical-path evidence; distinguish estimated aggregate task time from measured wall-clock savings. If the contract requires every execution, report zero avoidable time even when task durations are unknown.
+
+| Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+|---|---|---|---|---|---|---|
+
+After verification: `Verdict: removed N repeated executions, measured wall-clock saving X s`.
+
+Without verification: `Verdict: proposed removal of N repeated executions; estimated task-time saving X s; wall-clock not validated`.
+
+When no change is worthwhile: `Verdict: no repeated work worth changing`.
+
+## Common Pitfalls
+
+| Pitfall | Solution |
+|---|---|
+| Treating `Csc` or copy-local counts as waste | Compare `Csc` with projects times TFMs, and copies with required files and targets; do not assume both have the same expected count |
+| Guarding with `Exists(file)` or a flag property | Move the work out of the per-project path |
+| Assuming parent-local properties are inherited by child builds | Forward the shared value explicitly or load it in each child before the fallback; otherwise the fallback repeats in those children. Existing global properties are already inherited unless removed. |
+| Claiming a saving from total task time | Check actual overlap and the critical path; do not divide sequential work by unrelated busy nodes |
+| Merging per-item `Exec` and losing incrementality | Keep `Inputs` and `Outputs` on the target |
diff --git a/plugins/dotnet-msbuild/skills/incremental-build/SKILL.md b/plugins/dotnet-msbuild/skills/incremental-build/SKILL.md
index 790387e26a..5b6ebbdeea 100644
--- a/plugins/dotnet-msbuild/skills/incremental-build/SKILL.md
+++ b/plugins/dotnet-msbuild/skills/incremental-build/SKILL.md
@@ -1,6 +1,6 @@
---
name: incremental-build
-description: "Guide for optimizing MSBuild incremental builds. USE FOR: builds slower than expected on subsequent runs, 'nothing changed but it rebuilds anyway', diagnosing why targets re-execute unnecessarily, fixing broken no-op builds. Covers 8 common causes: missing Inputs/Outputs on custom targets, volatile properties in output paths (timestamps/GUIDs), file writes outside tracked Outputs, missing FileWrites registration, glob changes, Visual Studio Fast Up-to-Date Check (FUTDC) issues. Key diagnostic: look for 'Building target completely' vs 'Skipping target' in binlog. DO NOT USE FOR: first-time build slowness (use build-perf-baseline), parallelism issues (use build-parallelism), evaluation-phase slowness (use eval-performance), non-MSBuild build systems."
+description: "Fix MSBuild incremental/no-op builds: 'nothing changed but it rebuilds anyway'. USE FOR: targets re-executing on subsequent builds, missing Inputs/Outputs or FileWrites, volatile output paths, untracked writes, glob changes, Visual Studio Fast Up-to-Date Check (FUTDC); 'Building target completely' vs 'Skipping target' in binlog. DO NOT USE FOR: within-build duplicates (find-repeated-work-within-one-build), first-build slowness (build-perf-baseline), parallelism (build-parallelism), slow evaluation (eval-performance), non-MSBuild systems."
license: MIT
---
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Api/Api.csproj b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Api/Api.csproj
new file mode 100644
index 0000000000..555ae434a7
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Api/Api.csproj
@@ -0,0 +1,5 @@
+
+
+ net10.0
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Build.proj b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Build.proj
new file mode 100644
index 0000000000..c40730f8e7
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Build.proj
@@ -0,0 +1,25 @@
+
+
+ $(MSBuildThisFileDirectory)obj/buildinfo.g.cs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Directory.Build.targets b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Directory.Build.targets
new file mode 100644
index 0000000000..cfb1981501
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Directory.Build.targets
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Worker/Worker.csproj b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Worker/Worker.csproj
new file mode 100644
index 0000000000..555ae434a7
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/Worker/Worker.csproj
@@ -0,0 +1,5 @@
+
+
+ net10.0
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/buildinfo.cs.in b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/buildinfo.cs.in
new file mode 100644
index 0000000000..6448c6ebb1
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/buildinfo.cs.in
@@ -0,0 +1,4 @@
+internal static class BuildInfo
+{
+ public const string Version = "3.2.0";
+}
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/perf-summary.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/perf-summary.txt
new file mode 100644
index 0000000000..2ae48fa300
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/already-shared-work/perf-summary.txt
@@ -0,0 +1,15 @@
+Performance summary from dotnet msbuild Build.proj -restore -m
+(Api and Worker each compile the same generated buildinfo.g.cs; Time Elapsed 00:00:05.20)
+
+Target Performance Summary:
+ 12 ms GenerateSharedBuildInfo 1 calls
+ 4310 ms CoreCompile 2 calls
+
+Task Performance Summary:
+ 8 ms Copy 1 calls
+ 4290 ms Csc 2 calls
+
+Project Performance Summary:
+ 5200 ms Build.proj 1 calls
+ 2140 ms Api/Api.csproj 1 calls
+ 2210 ms Worker/Worker.csproj 1 calls
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/cheap-race.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/cheap-race.txt
new file mode 100644
index 0000000000..85d45cb78a
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/cheap-race.txt
@@ -0,0 +1,18 @@
+Invocation: ./build.sh, which calls dotnet msbuild Build.proj -m:8
+Build wall-clock: 600000 ms.
+Task Performance Summary: Exec 80 ms, 8 calls.
+All eight stampgen calls have identical inputs; output bytes are build-invariant.
+Failure excerpt: CS2015: obj/shared.g.cs is a binary file instead of a text file.
+Concurrent writers intermittently truncate that file while another project reads it.
+
+Directory.Build.targets:
+
+
+
+
+
+Every consumer must compile this same source file. Only build.sh is used for
+the parallel build. The generator exits nonzero on failure and overwrites on
+every invocation. On an unchanged rerun its bytes are identical; preserving the
+source timestamp avoids recompiling consumers. No output change is permitted.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/failed-preflight.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/failed-preflight.txt
new file mode 100644
index 0000000000..721d31449c
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/failed-preflight.txt
@@ -0,0 +1,22 @@
+Invocation: ./build.sh -p:Configuration=Release
+Task events in the failing invocation:
+wrapper: contract-tool --version exits 17 with "required license not available"
+16 child projects: the fallback contract-tool --version also exits 17
+Each process takes 1000 ms. The version must be known before compilation.
+
+build.sh:
+#!/usr/bin/env bash
+set -euo pipefail
+version=$(contract-tool --version || true)
+dotnet msbuild Build.proj -m "-p:ContractToolVersion=$version" "$@"
+
+Directory.Build.targets:
+
+
+
+
+
+
+The tool has no meaningful default version. Direct project builds require the
+fallback. Caller arguments and failure diagnostics must remain intact.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/local-property-propagation.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/local-property-propagation.txt
new file mode 100644
index 0000000000..629e11edf3
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/local-property-propagation.txt
@@ -0,0 +1,25 @@
+Invocation: dotnet msbuild Build.proj -m
+Task events: one sdkprobe --version in Build.proj, then one in each of 20 children.
+All 21 calls have identical inputs and return 4.2.0, 400 ms each.
+
+Build.proj:
+
+
+
+
+
+
+
+
+
+
+Directory.Build.targets, imported by every child:
+
+
+
+
+
+
+Direct project and IDE builds, which do not use Build.proj, must still work.
+The probe implementation and output format cannot change. No timeline is supplied.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/metadata-batches.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/metadata-batches.txt
new file mode 100644
index 0000000000..1a9d1080b7
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/metadata-batches.txt
@@ -0,0 +1,18 @@
+Invocation: dotnet msbuild Models.csproj -m:8
+Task Performance Summary: Exec 72000 ms, 120 calls.
+All Exec calls run serially in GenerateModels.
+60 Model items have Namespace=Contoso.Public, 60 have Namespace=Contoso.Internal.
+
+
+
+
+
+
+modelgen command contract:
+--input-list PATH is an alternative to --input PATH.
+The UTF-8 list contains one input path per line; --output-dir is unchanged.
+--namespace applies to every input in that invocation, not to individual lines.
+All input stems are unique. Content is deterministic for (input bytes, namespace).
+Startup is 500 ms per process; schema processing is 100 ms per input.
+Both namespaces and all 120 outputs are required. No critical-path timeline exists.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/off-critical-path.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/off-critical-path.txt
new file mode 100644
index 0000000000..b35f623276
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/off-critical-path.txt
@@ -0,0 +1,18 @@
+Invocation: dotnet msbuild Build.proj -m:8
+Build wall-clock: 120000 ms.
+Task Performance Summary: Exec 20000 ms, 10 calls.
+The ten inventory-tool --version calls have identical inputs and outputs.
+They belong to the Inventory branch; their consumers finish at t=25000 ms.
+
+Timeline:
+Build starts both branches at t=0.
+Inventory branch: version probes and reporting, t=0..25000 ms.
+Compiler branch: required compile/link work, t=0..120000 ms.
+Final join waits for both branches. Inventory uses otherwise idle nodes and
+has no resource contention or dependency edge to the Compiler branch.
+No cache, artifact or value from Inventory is consumed by Compiler.
+
+The proposed wrapper probe takes 2000 ms before both branches start.
+An alternative computes it once at the start of the Inventory branch.
+Either way, all ten consumers need the unchanged version string.
+There has been no post-change build or measurement.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/orchestrator-removal.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/orchestrator-removal.txt
new file mode 100644
index 0000000000..63f4666ea0
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/orchestrator-removal.txt
@@ -0,0 +1,20 @@
+Invocation: dotnet msbuild Build.proj -m -p:TraceBucket=nightly
+Task/project events, all in this invocation:
+Build.proj invokes PrepareSlice three times with different Properties values.
+PrepareSlice runs Shared.csproj using the MSBuild task below.
+Shared.csproj Build executes three times, 6000 ms each.
+The only differing global property on Shared is TraceBucket=A, B or C.
+
+PrepareSlice.proj:
+
+
+
+
+
+
+
+Shared.csproj does not read TraceBucket anywhere, including imports.
+PrepareSlice requires it for diagnostic attribution. There is no ProjectReference
+between PrepareSlice and Shared. Required global Configuration=Release and
+TargetFramework=net10.0 match for all three builds.
+Only aggregate project time was exported, not critical-path information.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/overlapped-stage.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/overlapped-stage.txt
new file mode 100644
index 0000000000..94c8096dcc
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/overlapped-stage.txt
@@ -0,0 +1,16 @@
+Invocation: dotnet msbuild Build.proj -m:6
+Task Performance Summary: Exec 21600 ms, 72 calls.
+Each project performs identical repoprobe --version, 300 ms, before compilation.
+The value is build-invariant; a wrapper may provide it as a global property.
+
+Timeline extract:
+0-300 ms: calls 1-6 on workers 1-6
+300-600 ms: calls 7-12 on workers 1-6
+...
+3300-3600 ms: calls 67-72 on workers 1-6
+3600 ms: all 72 probes complete; the next phase can start
+There is a phase barrier before compilation, no other work overlaps this stage,
+and the stage is on the critical path. A single wrapper probe would take 300 ms
+before the graph starts, with no additional distribution/IO overhead in this estimate.
+The build wall-clock is 40000 ms. There has been no post-change build.
+Every project must receive exactly the same version and keep direct-build support.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/per-project-identity.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/per-project-identity.txt
new file mode 100644
index 0000000000..7d88002d77
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/per-project-identity.txt
@@ -0,0 +1,20 @@
+Invocation: dotnet msbuild Build.proj -m:8
+Task Performance Summary: Exec 36000 ms, 24 calls.
+Every project runs identitygen --new-id once, with identical command text.
+identitygen intentionally returns a fresh cryptographic random identifier on
+each call. These are per-project signing identities, not build identifiers.
+
+Directory.Build.targets excerpt:
+
+
+
+
+
+
+
+Product contract: each of the 24 projects must have its own distinct identity
+for this build. Two projects sharing an identity is a release correctness failure.
+All identifiers are consumed before packaging; none can be omitted.
+There is no supported tool mode that creates multiple identifiers per invocation.
+No critical-path timeline or alternate tool is available.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/project-local-flag.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/project-local-flag.txt
new file mode 100644
index 0000000000..a7330325c8
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/project-local-flag.txt
@@ -0,0 +1,20 @@
+Invocation: dotnet msbuild Product.proj -m:8
+Product.proj builds 32 projects, with four projects scheduled on each worker.
+Task Performance Summary: Exec 16000 ms, 32 calls.
+All calls run the same license-tool --catalog-version and return 2026.10.
+The catalog is immutable for the entire invocation and independent of project.
+
+Directory.Build.targets:
+
+
+
+
+ true
+
+
+
+The team expected one call per invocation, or at worst one per worker.
+Each project needs the version before compiling. Direct project builds are
+supported, and the catalog value must remain available there too.
+Only aggregate task time is available; overlap cannot be reconstructed.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/repository-cwd.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/repository-cwd.txt
new file mode 100644
index 0000000000..45c7b2c31a
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/repository-cwd.txt
@@ -0,0 +1,22 @@
+Invocation: dotnet msbuild Product.proj -m:8
+One invocation builds 24 projects: 12 in src/Main and 12 in vendor/Engine.
+Task Performance Summary: Exec 48000 ms, 24 calls.
+Each Exec takes 2000 ms and runs git rev-parse HEAD.
+Main and Engine are separate Git repositories with different HEAD commits.
+
+Directory.Build.targets excerpt:
+
+
+
+
+
+
+
+Observed revision.txt values:
+src/Main/*: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
+vendor/Engine/*: bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
+Only the first 12 projects share Main's repository. Engine is not a subdirectory
+of Main's Git worktree. There is no timeline or node-overlap information.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/required-property-partitions.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/required-property-partitions.txt
new file mode 100644
index 0000000000..187e7884f6
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/required-property-partitions.txt
@@ -0,0 +1,22 @@
+Invocation: dotnet msbuild Build.proj -m
+The graph builds Web, Api and Worker for net9.0 and net10.0.
+Each component's Brand is distinct and appears in its own AssemblyTitle.
+All components reference Shared.csproj, which never reads Brand.
+
+Project Performance Summary, Shared.csproj:
+net9.0, Brand=Web: 2010 ms, 1 call
+net9.0, Brand=Api: 2010 ms, 1 call
+net9.0, Brand=Worker: 2010 ms, 1 call
+net10.0, Brand=Web: 2010 ms, 1 call
+net10.0, Brand=Api: 2010 ms, 1 call
+net10.0, Brand=Worker: 2010 ms, 1 call
+All six calls execute Build and compile Shared, rather than just evaluate it.
+Each framework already has separate output and intermediate directories.
+
+Component project excerpt:
+Contoso $(Brand)
+
+
+Shared.csproj consumes TargetFramework for references, compilation and output
+layout. Both framework assemblies are distributed. Its source imports and
+targets do not read Brand. No overlap/timeline evidence was retained.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/sequential-startup.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/sequential-startup.txt
new file mode 100644
index 0000000000..2a5902ea3d
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/sequential-startup.txt
@@ -0,0 +1,17 @@
+Invocation: dotnet msbuild Assets.csproj -m:16
+The full build takes 180000 ms. Other projects use 15 worker nodes.
+Assets.csproj runs PackAssets before its package consumer on the critical path.
+Task events inside PackAssets: 200 consecutive, non-overlapping Exec calls,
+450 ms each; total 90000 ms. No item runs concurrently with another item here.
+
+
+
+
+
+assetpack supports --input-list PATH and produces identical per-input outputs.
+Measured per-process startup: 400 ms.
+Measured per-input processing: 50 ms in either mode.
+The list mode starts once and then processes all inputs sequentially.
+These figures describe this original invocation, not a post-change measurement.
+All 200 outputs and existing incremental input/output pairing are required.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/single-input-contract.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/single-input-contract.txt
new file mode 100644
index 0000000000..35f4d6d5f3
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/single-input-contract.txt
@@ -0,0 +1,18 @@
+Invocation: dotnet msbuild Contracts.csproj -m
+Task Performance Summary: Exec 30000 ms, 60 calls.
+All 60 calls are inside GenerateContracts on one project instance.
+Each input has different content and a different generated output.
+
+
+
+
+
+
+Installed contractc 2.4 command contract:
+ contractc --input PATH --output PATH
+Exactly one input and exactly one output are accepted.
+There is no response-file, directory, list, daemon or multi-command interface.
+The installed tool cannot be replaced in this release.
+Launching extra processes concurrently is not the requested change.
+No startup-versus-processing breakdown or critical-path timeline is available.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/stale-items.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/stale-items.txt
new file mode 100644
index 0000000000..320b3df835
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/stale-items.txt
@@ -0,0 +1,20 @@
+Invocation: dotnet msbuild Models.csproj -m
+Target GenerateModels has 120 input/output pairs. Only A.json, B.json and
+C.json are newer than their own generated outputs in this invocation.
+The other 117 generated files are present, current, and byte-correct.
+Task events: modelgen starts 3 times, 750 ms each.
+
+
+
+
+
+
+
+
+modelgen supports --input-list PATH, one path per UTF-8 line.
+It creates the same output for each input as the single-input mode.
+It rewrites any output it is asked to generate, including unchanged inputs.
+Each process spends 500 ms starting and 250 ms processing its one input.
+No actual post-change run or critical-path timeline is available.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/two-targets-one-instance.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/two-targets-one-instance.txt
new file mode 100644
index 0000000000..a0c2dd3736
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/two-targets-one-instance.txt
@@ -0,0 +1,21 @@
+Invocation: dotnet msbuild App.csproj -target:Build,Pack
+One project instance, one framework, one global-property set.
+Task events: git rev-parse HEAD runs twice, 1500 ms each, with identical cwd
+and full 40-character output. One call is in StampAssembly; one in StampPackage.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+Both consumers need the same value. Build-only and Pack-only invocations must
+remain valid. The repository cannot change during this invocation.
+No timeline was exported. There is no second project to share state with.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/variant-generator-inputs.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/variant-generator-inputs.txt
new file mode 100644
index 0000000000..4cdb1b4213
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/audit-scenarios/variant-generator-inputs.txt
@@ -0,0 +1,20 @@
+Invocation: dotnet msbuild Build.proj -m
+Six projects build Configuration=Debug and Configuration=Release.
+Task Performance Summary: Exec 24000 ms, 12 calls, 2000 ms each.
+All commands have the same text, but generated bytes differ by Configuration.
+All Debug projects produce identical Debug bytes; likewise for Release.
+The configuration files are immutable during this invocation.
+
+Directory.Build.targets:
+
+
+
+
+
+
+
+Debug and Release outputs are required and already use separate directories.
+settingsgen creates an output deterministically but overwrites it every call.
+It has no multi-configuration mode. All projects use Build.proj, but direct
+project builds must also remain possible. No timeline is available.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/eval.yaml b/tests/dotnet-msbuild/find-repeated-work-within-one-build/eval.yaml
new file mode 100644
index 0000000000..ca648ec7b4
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/eval.yaml
@@ -0,0 +1,1849 @@
+name: find-repeated-work-within-one-build
+tags:
+ executionShard: heavy
+description: Evaluates the dotnet-msbuild/find-repeated-work-within-one-build skill
+type: capability
+defaults:
+ timeout: 6m
+stimuli:
+ - name: Compute a per-project git lookup once for the whole build
+ prompt: |
+ git-stamp/perf-summary.txt is the performance summary of the CI build that runs git-stamp/build.sh. Cut the build
+ time that is not spent compiling, without changing the informational version stamped into each assembly. You
+ cannot run the build from here, so work from the repository files. Developers also run dotnet build inside a
+ single project directory.
+ tags:
+ capability: invariant-per-project-exec
+ risk: high
+ journey: apply-fix
+ environment:
+ files:
+ - src: ./git-stamp
+ dest: git-stamp
+ - src: ./graders/ReadAssemblyAttribute.targets
+ dest: .grader/ReadAssemblyAttribute.targets
+ golden_patch:
+ path: ./references/compute-once-pass-global-property.patch
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-git-stamp"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ The Task Performance Summary shows Exec with 38 calls for a solution of 38 projects, about 1970 ms each, so the git lookup in Directory.Build.targets starts a process in every project and gives the same answer each time. Removing 37 calls could avoid about 72.89 s of aggregate task time; the summary does not show actual overlap, so eight cores alone do not establish a wall-clock saving. I could not run the build here, so this is not validated. The commit id is now computed once in build.sh and passed as -p:GitCommit, which every project inherits as a global property. The Exec in Directory.Build.targets only runs when GitCommit is empty, so a single-project dotnet build still gets a stamp. The version assignment remains unconditional. I did not use a flag property or an Exists check to skip the Exec, because properties are per project instance and the worker nodes are separate processes. The informational version remains 3.2.0 plus the same short commit id.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Git lookup | 38 | 1 | 72890 estimated | Build-invariant command in every project | Compute once and pass GitCommit; keep fallback | unknown |
+
+ Verdict: proposed removal of 37 repeated executions; estimated task-time saving 72.89 s; wall-clock not validated
+ graders:
+ - type: run-command
+ config:
+ command: python
+ args:
+ - -c
+ - |
+ import json, os, pathlib, re, shutil, subprocess, sys, tempfile, uuid
+ import xml.etree.ElementTree as ET
+ bash = shutil.which('bash')
+ assert bash, 'Bash is required to exercise the submitted build.sh'
+ dotnet = shutil.which('dotnet')
+ assert dotnet, 'the .NET SDK is required to check the commit before its first consumer'
+ real_git = shutil.which('git')
+ assert real_git, 'Git is required to evaluate revision lookups'
+ git_env = dict(os.environ, GIT_CONFIG_GLOBAL=os.devnull, GIT_CONFIG_NOSYSTEM='1')
+ with tempfile.TemporaryDirectory() as tmp:
+ bin_dir = pathlib.Path(tmp) / 'bin'
+ bin_dir.mkdir()
+ log = bin_dir / 'git-calls'
+ log.mkdir()
+ tracer = bin_dir / 'trace-git.py'
+ tracer.write_text(
+ 'import json, os, pathlib, subprocess, sys, uuid\n'
+ 'result = subprocess.run([os.environ["REAL_GIT"], *sys.argv[1:]], capture_output=True)\n'
+ 'record = pathlib.Path(__file__).with_name("git-calls") / (uuid.uuid4().hex + ".json")\n'
+ 'record.write_text(json.dumps({"returncode": result.returncode, '
+ '"stdout": result.stdout.decode("utf-8", errors="replace")}), encoding="utf-8")\n'
+ 'sys.stdout.buffer.write(result.stdout)\n'
+ 'sys.stderr.buffer.write(result.stderr)\n'
+ 'sys.exit(result.returncode)\n',
+ encoding='utf-8', newline='\n',
+ )
+ git = bin_dir / 'git'
+ git.write_text(
+ '#!/usr/bin/env bash\nexec "$GRADER_PYTHON" "$MOCK_BIN/trace-git.py" "$@"\n',
+ encoding='utf-8', newline='\n',
+ )
+ git.chmod(0o755)
+ if os.name == 'nt':
+ (bin_dir / 'git.cmd').write_text(
+ '@echo off\n"' + sys.executable + '" "' + str(tracer) + '" %*\n',
+ encoding='utf-8', newline='\r\n',
+ )
+ def clear_calls():
+ for path in log.glob('*.json'):
+ path.unlink()
+ def revision_calls(full_commit):
+ calls = [json.loads(path.read_text(encoding='utf-8')) for path in log.glob('*.json')]
+ return sum(
+ call['returncode'] == 0 and any(
+ full_commit.startswith(value.lower()) for value in re.findall(
+ r'(?= minimum_nodes, 'preserve parallel execution, not a missing switch or single worker'
+ assert observed['parallel'].strip().lower() not in ['false', 'off', 'no', '0'], 'do not disable parallel execution through global properties'
+ for iteration in range(2):
+ work = pathlib.Path(tmp) / ('ci-' + str(iteration))
+ shutil.copytree('git-stamp', work, ignore=shutil.ignore_patterns('bin', 'obj', '.git'))
+ for args in [
+ ['init', '--quiet', '--template=', '--initial-branch=grading', str(work)],
+ ['-C', str(work), 'config', 'core.abbrev', '7'],
+ ['-C', str(work), 'add', '.'],
+ ['-C', str(work), '-c', 'user.name=Grader', '-c', 'user.email=grader@example.invalid',
+ '-c', 'commit.gpgsign=false', '-c', 'core.hooksPath=' + str(bin_dir),
+ 'commit', '--quiet', '-m', uuid.uuid4().hex],
+ ['-C', str(work), 'log', '-1', '--format=%H%n%h'],
+ ]:
+ result = subprocess.run([real_git, *args], env=git_env, capture_output=True, text=True, timeout=10)
+ assert result.returncode == 0, result.stdout + result.stderr
+ full_commit, commit = result.stdout.strip().splitlines()
+ solution = ET.Element('Solution')
+ records = []
+ for name in ['Api', 'Worker']:
+ project, record = create_probe(work, name, str(iteration) + '-' + name)
+ ET.SubElement(solution, 'Project', Path=project.relative_to(work).as_posix())
+ records.append(record)
+ ET.ElementTree(solution).write(work / 'Contoso.slnx', encoding='utf-8')
+ env = dict(git_env, MOCK_BIN=str(bin_dir), REAL_GIT=pathlib.Path(real_git).as_posix(),
+ GRADER_PYTHON=pathlib.Path(sys.executable).as_posix(),
+ PATH=str(bin_dir) + os.pathsep + os.environ['PATH'], MSBUILDDISABLENODEREUSE='1')
+ clear_calls()
+ result = subprocess.run(
+ [bash, '-c', 'export MOCK_BIN="$(cd "$MOCK_BIN" && pwd)"; export PATH="$MOCK_BIN:$PATH"; bash build.sh'],
+ cwd=work, env=env, capture_output=True, text=True, timeout=60,
+ )
+ assert result.returncode == 0, result.stdout + result.stderr
+ for record in records:
+ check_build(record, commit, ci=True)
+ assert revision_calls(full_commit) <= 1, 'the fallback must run once at most when no revision is supplied; compute the revision at most once for the solution build'
+ single = pathlib.Path(tmp) / ('single-' + str(iteration))
+ shutil.copytree('git-stamp', single, ignore=shutil.ignore_patterns('bin', 'obj', '.git'))
+ shutil.copytree(work / '.git', single / '.git')
+ project, record = create_probe(single, 'Standalone', str(iteration) + '-Standalone')
+ clear_calls()
+ result = subprocess.run(
+ [dotnet, 'build', str(project), '-nologo', '-verbosity:quiet', '-nodeReuse:false'],
+ cwd=project.parent, env=env, capture_output=True, text=True, timeout=30,
+ )
+ assert result.returncode == 0, result.stdout + result.stderr
+ check_build(record, commit, ci=False)
+ assert revision_calls(full_commit) <= 1, 'the fallback must run once at most when no revision is supplied'
+ expected_exit_code: 0
+ - type: output-matches
+ config:
+ pattern: '(?m)^\|\s*Work\s*\|\s*Calls\s*\|\s*Expected\s*\|\s*Avoidable ms\s*\|\s*Cause\s*\|\s*Fix\s*\|\s*On critical path\?\s*\|\r?\n\|(?:\s*:?-+:?\s*\|){7}\r?\n\|(?:[^|\r\n]*\|){7}\s*$'
+ - type: output-matches
+ config:
+ pattern: '(?m)^Verdict:[ \t]*\S[^\r\n]*$'
+ - type: prompt
+ rubric:
+ - Identified from the Exec call count (38 calls for 38 projects) that the same git lookup runs in every project
+ - Eliminated redundant external revision lookups, for example by computing the commit once and passing it as a
+ global property or using SDK revision metadata, while preserving the correct stamp in solution and single-project builds
+ - Did not rely on a flag property or Exists check to dedupe the work across projects
+ - Kept the informational version in the same format
+ - Did not claim the build was run or the saving measured
+
+ - name: Stop a shared project from being built once per caller
+ prompt: |
+ shared-project-instances/perf-summary.txt is the performance summary of the CI build that runs
+ shared-project-instances/build.sh. Most of the time in it is spent in Core. Reduce the build time without changing
+ what Web, Api and Worker produce: each must still get its own component name in its AssemblyTitle. You cannot run
+ the build from here, so work from the repository files.
+ tags:
+ capability: global-property-instances
+ risk: high
+ journey: apply-fix
+ environment:
+ files:
+ - src: ./shared-project-instances
+ dest: shared-project-instances
+ - src: ./graders/ReadAssemblyAttribute.targets
+ dest: .grader/ReadAssemblyAttribute.targets
+ golden_patch:
+ path: ./references/remove-property-from-shared-reference.patch
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-shared-project-instances"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Core.csproj shows 3 calls in the Project Performance Summary, about 21 s each, although it is one project. Build.proj passes Properties="ComponentName=%(Component.Filename)" to each component, and every component then builds its reference to Core with that global property inherited. A different global property makes a different project instance, so Core is built once per component even though it never reads ComponentName. I added GlobalPropertiesToRemove="ComponentName" to the Core ProjectReference in Web, Api and Worker, so Core is built with the same properties every time and only once. The components still receive ComponentName for their AssemblyTitle. Not validated: I could not run the build here. Rebuilding with a binlog should show Core.csproj with 1 call.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Core project | 3 | 1 | 42060 estimated | ComponentName creates distinct global-property instances | Remove ComponentName only on Core references | unknown |
+
+ Verdict: proposed removal of 2 repeated executions; estimated project-time saving 42.06 s; wall-clock not validated
+ graders:
+ - type: run-command
+ config:
+ command: python
+ args:
+ - -c
+ - |
+ import os, pathlib, shutil, subprocess, tempfile
+ import xml.etree.ElementTree as ET
+ base = pathlib.Path('shared-project-instances')
+ dotnet = shutil.which('dotnet')
+ bash = shutil.which('bash')
+ assert dotnet and bash, 'the .NET SDK and Bash are required to exercise the submitted build'
+ with tempfile.TemporaryDirectory() as tmp:
+ work = (pathlib.Path(tmp) / 'work').resolve()
+ shutil.copytree(base, work, ignore=shutil.ignore_patterns('bin', 'obj'))
+ logs = (pathlib.Path(tmp) / 'logs').resolve()
+ (logs / 'core').mkdir(parents=True)
+ core_path = work / 'Core' / 'Core.csproj'
+ core = ET.parse(core_path)
+ record = ET.SubElement(core.getroot(), 'Target', Name='_GraderRecordCoreBuild', BeforeTargets='Build')
+ ET.SubElement(record, 'WriteLinesToFile', Overwrite='true', Encoding='UTF-8',
+ File=str(logs / 'core' / '$([System.Guid]::NewGuid()).txt'), Lines='$(TargetPath)')
+ core.write(core_path, encoding='utf-8')
+ for component in ['Web', 'Api', 'Worker']:
+ path = work / component / (component + '.csproj')
+ project = ET.parse(path)
+ ET.SubElement(project.getroot(), 'Import',
+ Project=str(pathlib.Path('.grader/ReadAssemblyAttribute.targets').resolve()))
+ record = ET.SubElement(project.getroot(), 'Target', Name='_GraderRecordComponentBuild', AfterTargets='Build')
+ read = ET.SubElement(record, '_GraderReadAssemblyAttribute', AssemblyPath='$(TargetPath)',
+ AttributeType='System.Reflection.AssemblyTitleAttribute')
+ ET.SubElement(read, 'Output', TaskParameter='Value', PropertyName='_GraderCompiledTitle')
+ ET.SubElement(record, 'WriteLinesToFile', File=str(logs / (component + '.txt')),
+ Lines="$(AssemblyTitle);compiled:$(_GraderCompiledTitle);$(TargetPath);@(ReferencePathWithRefAssemblies->'%(FullPath)')",
+ Overwrite='true', Encoding='UTF-8')
+ project.write(path, encoding='utf-8')
+ result = subprocess.run(
+ [bash, 'build.sh'], cwd=work, env=dict(os.environ, MSBUILDDISABLENODEREUSE='1'),
+ capture_output=True, text=True, timeout=120,
+ )
+ assert result.returncode == 0, result.stdout + result.stderr
+ calls = list((logs / 'core').glob('*.txt'))
+ assert len(calls) == 1, 'the submitted BuildAll path must build Core exactly once'
+ core_output = pathlib.Path(calls[0].read_text(encoding='utf-8-sig').strip()).resolve()
+ assert core_output.is_relative_to(work) and core_output.is_file() and core_output.name == 'Core.dll', 'Core must produce its assembly on a clean build'
+ for component in ['Web', 'Api', 'Worker']:
+ lines = (logs / (component + '.txt')).read_text(encoding='utf-8-sig').splitlines()
+ assert lines[0] == 'Contoso ' + component, 'preserve the effective component AssemblyTitle'
+ assert lines[1] == 'compiled:Contoso ' + component, 'preserve the compiled component AssemblyTitle'
+ output = pathlib.Path(lines[2]).resolve()
+ assert output.is_relative_to(work) and output.is_file() and output.name == component + '.dll', 'each component must still produce its assembly'
+ references = [pathlib.Path(p).resolve() for p in lines[3:]]
+ assert any(p.name == 'Core.dll' and p.is_relative_to(work / 'Core') and p.is_file()
+ for p in references), 'Core must remain an actual compilation reference'
+ assert (output.parent / 'Core.dll').read_bytes() == core_output.read_bytes(), 'each component must retain the built Core copy'
+ expected_exit_code: 0
+ - type: output-matches
+ config:
+ pattern: (?i)(global propert|project instance)
+ - type: output-matches
+ config:
+ pattern: '(?m)^\|\s*Work\s*\|\s*Calls\s*\|\s*Expected\s*\|\s*Avoidable ms\s*\|\s*Cause\s*\|\s*Fix\s*\|\s*On critical path\?\s*\|\r?\n\|(?:\s*:?-+:?\s*\|){7}\r?\n\|(?:[^|\r\n]*\|){7}\s*$'
+ - type: output-matches
+ config:
+ pattern: '(?m)^Verdict:[ \t]*\S[^\r\n]*$'
+ - type: prompt
+ rubric:
+ - Identified from Core.csproj having several calls that the same project is built once per distinct set of global
+ properties
+ - Traced the cause to the ComponentName property passed by Build.proj and inherited through the ProjectReference
+ - Kept each component's own AssemblyTitle, for example by removing ComponentName only on the reference to Core with
+ GlobalPropertiesToRemove, instead of deleting the property from the components
+ - Did not claim the build was run or the saving measured
+
+ - name: Generate a shared file before the parallel build starts
+ prompt: |
+ Intermittently the Build step in exists-sentinel-race fails with the error in
+ exists-sentinel-race/failure-log.txt, and a re-run passes. Fix the cause so the failure cannot depend on timing.
+ Everyone builds through exists-sentinel-race/build.sh, and every project must still compile obj/buildinfo.g.cs.
+ An unchanged rerun must not force every project to compile again.
+ Preserve the generator's BUILDINFO_STAMP override and UTC default.
+ You cannot run the build from here, so work from the repository files.
+ tags:
+ capability: racy-run-once-guard
+ risk: high
+ journey: apply-fix
+ environment:
+ files:
+ - src: ./exists-sentinel-race
+ dest: exists-sentinel-race
+ golden_patch:
+ path: ./references/generate-before-build.patch
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-exists-sentinel-race"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ The target in Directory.Build.targets is meant to generate buildinfo.g.cs once, but its Exists check is evaluated separately in every project, and with -m the projects build in separate worker processes. Several can enter before the file exists, then truncate or append to the same output while other projects start compilation. Interleaved writes can leave an extra date suffix, consistent with the compiler error in failure-log.txt; Exec shows 9 calls for 24 projects. A flag property would not help because properties are per project instance. I removed the generation from the per-project target and build.sh now runs tools/make-buildinfo.sh once into a temporary file, before dotnet build. It publishes that file only when its content differs, keeping the compiled file's timestamp on an unchanged rerun and updating it when the daily stamp changes. The Compile item still includes it. Not validated: I could not run the build here.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Shared file generation | 9 | 1 | 3480 estimated | Per-project check-then-write race | Generate once and finish before the parallel build | unknown |
+
+ Verdict: proposed removal of 8 repeated executions; estimated task-time saving 3.48 s; wall-clock not validated
+ graders:
+ - type: run-command
+ config:
+ command: python
+ args:
+ - -c
+ - |
+ import datetime, os, pathlib, shutil, subprocess, tempfile, uuid
+ import xml.etree.ElementTree as ET
+ base = pathlib.Path('exists-sentinel-race')
+ bash = shutil.which('bash')
+ assert bash, 'Bash is required to exercise build.sh'
+ real_dotnet = shutil.which('dotnet')
+ assert real_dotnet, 'the .NET SDK is required to exercise per-project generation hooks'
+ with tempfile.TemporaryDirectory() as tmp:
+ work = pathlib.Path(tmp) / 'work'
+ shutil.copytree(base, work)
+ bin_dir = pathlib.Path(tmp) / 'bin'
+ bin_dir.mkdir()
+ solution = ET.Element('Solution')
+ observations = []
+ compile_observations = []
+ compiler_observations = []
+ boundaries = []
+ generated = (work / 'obj/buildinfo.g.cs').resolve()
+ expected = (bin_dir / 'expected-buildinfo').resolve()
+ for name in ['Api', 'Worker']:
+ project_dir = work / 'probes' / name
+ project_dir.mkdir(parents=True)
+ probe = ET.Element('Project')
+ ET.SubElement(probe, 'Import', Project='Sdk.props', Sdk='Microsoft.NET.Sdk')
+ properties = ET.SubElement(probe, 'PropertyGroup')
+ for property_name, value in [('Configuration', 'Release'), ('TargetFramework', 'net10.0'),
+ ('IsCrossTargetingBuild', 'false'), ('ProvideCommandLineArgs', 'true')]:
+ ET.SubElement(properties, property_name).text = value
+ ET.SubElement(probe, 'Import', Project='Sdk.targets', Sdk='Microsoft.NET.Sdk')
+ observation = bin_dir / (name + '.built')
+ observations.append((name, observation))
+ observer = ET.SubElement(probe, 'Target', Name='ObserveProbeBuild_' + uuid.uuid4().hex,
+ AfterTargets='Build')
+ ET.SubElement(observer, 'WriteLinesToFile', File=str(observation.resolve()),
+ Lines='$(MSBuildProjectName);$(TargetPath)', Overwrite='true')
+ # Csc outputs are empty when CoreCompile skips; its hooks still run.
+ compiler_output = bin_dir / (name + '.csc-args')
+ compiler_observations.append(compiler_output)
+ ET.SubElement(observer, 'WriteLinesToFile', File=str(compiler_output.resolve()),
+ Lines='@(CscCommandLineArgs)', Overwrite='false')
+ compiler = ET.SubElement(probe, 'Target', Name='ObserveCompile_' + uuid.uuid4().hex,
+ BeforeTargets='CoreCompile')
+ for phase, target in [('compile', compiler), ('built', observer)]:
+ items = bin_dir / (name + '.' + phase + '.compile-items')
+ compile_observations.append(items)
+ ET.SubElement(target, 'WriteLinesToFile', File=str(items.resolve()),
+ Lines="@(Compile->'%(FullPath)')", Overwrite='true')
+ hashes = '_GraderGenerated_' + phase
+ expected_hashes = '_GraderExpected_' + phase
+ for file, item_name in [(generated, hashes), (expected, expected_hashes)]:
+ task = ET.SubElement(target, 'GetFileHash', Files=str(file))
+ ET.SubElement(task, 'Output', TaskParameter='Items', ItemName=item_name)
+ ET.SubElement(
+ target, 'Error',
+ Condition="'@(" + hashes + "->'%(FileHash)')' != '@(" + expected_hashes + "->'%(FileHash)')'",
+ Text='preserve shared source bytes at the ' + phase + ' boundary',
+ )
+ mtime = bin_dir / (name + '.' + phase + '.mtime')
+ boundaries.append(mtime)
+ ET.SubElement(
+ target, 'WriteLinesToFile', File=str(mtime.resolve()), Overwrite='true',
+ Lines="@(" + hashes + "->'%(ModifiedTime)')",
+ )
+ project_path = project_dir / (name + '.csproj')
+ ET.ElementTree(probe).write(project_path, encoding='utf-8')
+ ET.SubElement(solution, 'Project', Path=project_path.relative_to(work).as_posix())
+ ET.ElementTree(solution).write(work / 'Contoso.slnx', encoding='utf-8')
+ real_date = subprocess.run([bash, '-c', 'command -v date'], check=True,
+ capture_output=True, text=True).stdout.strip()
+ assert real_date, 'GNU date is required to exercise the original generator'
+ date = bin_dir / 'date'
+ # Widen partial writes without depending on the generator's implementation or location.
+ date.write_text(
+ '#!/usr/bin/env sh\nset -eu\n'
+ 'sleep 1\n'
+ '"$REAL_DATE" "$@"\n',
+ encoding='utf-8', newline='\n',
+ )
+ date.chmod(0o755)
+ dotnet = bin_dir / 'dotnet'
+ dotnet.write_text(
+ '#!/usr/bin/env sh\nset -eu\n'
+ '"$REAL_DOTNET" "$@" -nologo -verbosity:quiet\n',
+ encoding='utf-8', newline='\n',
+ )
+ dotnet.chmod(0o755)
+ env = dict(os.environ, MOCK_BIN=str(bin_dir), REAL_DATE=real_date,
+ REAL_DOTNET=pathlib.Path(real_dotnet).as_posix(),
+ MSBUILDDISABLENODEREUSE='1')
+ def source_for_date(value):
+ return ('internal static class BuildInfo { public const string Stamp = "' +
+ value.strftime('%Y%m%d') + '"; }\n').encode('utf-8')
+ def set_stamp(value):
+ env['BUILDINFO_STAMP'] = value.strftime('%Y%m%d')
+ expected.write_bytes(source_for_date(value))
+ class UtcDateRollover(RuntimeError):
+ pass
+ def require_day(day):
+ if day is not None and datetime.datetime.now(datetime.timezone.utc).date() != day:
+ raise UtcDateRollover('UTC date changed during the default build probe')
+ def run_build(phase, should_compile, day=None):
+ require_day(day)
+ for path in ([path for _, path in observations] + compile_observations + compiler_observations +
+ boundaries):
+ path.unlink(missing_ok=True)
+ result = subprocess.run(
+ [bash, '-c', 'export MOCK_BIN="$(cd "$MOCK_BIN" && pwd)"; export PATH="$MOCK_BIN:$PATH"; bash build.sh'],
+ cwd=work, env=env, timeout=60,
+ )
+ require_day(day)
+ result.check_returncode()
+ for name, path in observations:
+ assert path.is_file(), 'exercise both project Build paths and their submitted target hooks'
+ lines = path.read_text(encoding='utf-8-sig').splitlines()
+ assert lines[0] == name, 'exercise both project Build paths and their submitted target hooks'
+ output = pathlib.Path(lines[1]).resolve()
+ assert output.is_relative_to(work.resolve()) and output.is_file(), 'each project must produce its compiled assembly'
+ assert all(path.is_file() and (should_compile is None or
+ bool(path.read_text(encoding='utf-8-sig').strip()) == should_compile)
+ for path in compiler_observations), phase + ': compile changed inputs but never force compilation on an unchanged build'
+ assert all(path.is_file() and generated.resolve() in [
+ pathlib.Path(line).resolve() for line in path.read_text(encoding='utf-8-sig').splitlines()
+ ] for path in compile_observations), 'each project must retain buildinfo.g.cs in effective Compile items at the compile and build boundaries'
+ assert generated.read_bytes() == expected.read_bytes(), 'preserve the original daily build-info output bytes'
+ final_ns = generated.stat().st_mtime_ns
+ expected_mtime = datetime.datetime.fromtimestamp(final_ns // 1_000_000_000).strftime('%Y-%m-%d %H:%M:%S')
+ expected_mtime += '.' + format(final_ns % 1_000_000_000 // 100, '07d')
+ for mtime in boundaries:
+ assert mtime.is_file() and mtime.read_text(encoding='utf-8-sig').strip() == expected_mtime, 'publish the final source timestamp before compilation and preserve it through Build'
+ return generated.stat().st_mtime_ns
+ stamp = datetime.date(2040, 1, 1) + datetime.timedelta(days=uuid.uuid4().int % 20000)
+ set_stamp(stamp)
+ run_build('clean', True)
+ unchanged_mtime = generated.stat().st_mtime_ns
+ assert run_build('unchanged', False) == unchanged_mtime, 'an identical second build must preserve the generated-source timestamp'
+ set_stamp(stamp + datetime.timedelta(days=1))
+ assert run_build('changed', True) > unchanged_mtime, 'refresh the generated source when the stamp changes, preserving the existing warm cache'
+ for phase, value in [('unset', None), ('empty', '')]:
+ for attempt in range(2):
+ day = datetime.datetime.now(datetime.timezone.utc).date()
+ set_stamp(day - datetime.timedelta(days=1))
+ # A retry may reuse a completed seed, so its compiler state is not predetermined.
+ seed_mtime = run_build('seed-' + phase, None)
+ if value is None:
+ env.pop('BUILDINFO_STAMP', None)
+ else:
+ env['BUILDINFO_STAMP'] = value
+ expected.write_bytes(source_for_date(day))
+ try:
+ refreshed = run_build(phase, True, day)
+ assert refreshed > seed_mtime, 'the UTC default must replace the prior-day cached source'
+ assert run_build(phase + '-unchanged', False, day) == refreshed, 'an unchanged UTC-default build must preserve the generated-source timestamp'
+ except UtcDateRollover as error:
+ if attempt:
+ raise RuntimeError('UTC date changed during both attempts to check the ' + phase + ' default') from error
+ continue
+ break
+ expected_exit_code: 0
+ - type: output-matches
+ config:
+ pattern: '(?m)^\|\s*Work\s*\|\s*Calls\s*\|\s*Expected\s*\|\s*Avoidable ms\s*\|\s*Cause\s*\|\s*Fix\s*\|\s*On critical path\?\s*\|\r?\n\|(?:\s*:?-+:?\s*\|){7}\r?\n\|(?:[^|\r\n]*\|){7}\s*$'
+ - type: output-matches
+ config:
+ pattern: '(?m)^Verdict:[ \t]*\S[^\r\n]*$'
+ - type: prompt
+ rubric:
+ - Explained that the Exists check runs separately in every project and, with -m, in separate worker processes, so
+ several projects can run the generator at the same time
+ - Arranged any required generation once before the parallel build, for example a step in build.sh before
+ dotnet build, allowing a current cached output but not a flag property or another check-then-write guard inside the per-project target
+ - Kept obj/buildinfo.g.cs compiled by every project
+ - Preserved the generated-source timestamp on an identical second build, while still publishing changed generated content
+ - Preserved the BUILDINFO_STAMP override and UTC default, including cache refresh when the effective stamp changes
+ - Did not claim the failure was reproduced or the fix verified
+
+ - name: Replace one generator process per file with one process for the list
+ prompt: |
+ per-file-exec/perf-summary.txt shows where the build of Contoso.Schemas spends its time, and
+ per-file-exec/tools/schemagen.md describes the generator it uses. The real schemas directory has about 120 files;
+ three samples are checked in here. Reduce the generation cost. The generated output must stay identical. You cannot
+ run the build from here, so work from the repository files.
+ tags:
+ capability: per-item-exec
+ risk: medium
+ journey: apply-fix
+ environment:
+ files:
+ - src: ./per-file-exec
+ dest: per-file-exec
+ golden_patch:
+ path: ./references/single-exec-input-list.patch
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-per-file-exec"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Exec has 120 calls in one project, about 1400 ms each, so the generator starts once per schema, and its startup cost does not depend on the file. Batching on %(SchemaFile.FullPath) in Schemas.targets causes that. schemagen.md says --input-list takes a list of paths and produces the same output as one run per schema, so the target now writes the list with WriteLinesToFile and runs schemagen once with --input-list and --output-dir. That removes about 119 launches, roughly 2.7 minutes of 3.8 total. I kept Inputs and Outputs on the target, so a build after changing one schema still only regenerates the stale files, because MSBuild passes only the out-of-date items into the target. Not validated: I could not run the build here.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | schemagen startup | 120 | 1 | 166838 estimated | Sequential per-item Exec on one node | Write stale paths to a list and invoke once | yes, before compilation |
+
+ Verdict: proposed removal of 119 repeated executions; estimated task-time saving 166.84 s; wall-clock not validated
+ graders:
+ - type: run-command
+ config:
+ command: python
+ args:
+ - -c
+ - |
+ import hashlib, json, os, pathlib, shlex, shutil, subprocess, sys, tempfile, time, uuid
+ import xml.etree.ElementTree as ET
+ base = pathlib.Path('per-file-exec')
+ expected = {
+ 'perf-summary.txt': 'a7a87a9f72d2a3ea5e94a0b98ce895e08c6b73b7c41ad772da5c67dc96e018ba',
+ 'schemas/Customer.json': '67eb0f87e3e7d30e93c695f6309869ad756ec9d3a794fea1f579b53e76588276',
+ 'schemas/Invoice.json': '84fda3aa966679bf270dcb52a962638ccfb0a98a9f8a98fd8412bfb514a7f73d',
+ 'schemas/Order.json': '62e76a2120a81ebf31bd338c87f570619c3f98ceeee8378c139b678cf274297e',
+ 'tools/schemagen.md': '5f1e323d5f5b121037fc94b50a8a7b318b8c91e133efd2d1fa26eff45097d75b',
+ }
+ actual = {p.relative_to(base).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
+ for p in base.rglob('*') if p.is_file()
+ and p.relative_to(base).as_posix() not in ['Schemas.targets', 'Contoso.Schemas.csproj']
+ and p.relative_to(base).parts[0] not in ['bin', 'obj']}
+ assert expected.items() <= actual.items(), 'preserve all original schema contents and evidence files'
+ dotnet = shutil.which('dotnet')
+ assert dotnet, 'the .NET SDK is required to query evaluated Compile items'
+ with tempfile.TemporaryDirectory() as tmp:
+ work = (pathlib.Path(tmp) / 'work').resolve()
+ shutil.copytree('per-file-exec', work, ignore=shutil.ignore_patterns('bin', 'obj'))
+ bin_dir = pathlib.Path(tmp) / 'bin'
+ bin_dir.mkdir()
+ compile_items = pathlib.Path(tmp) / 'compile-items.txt'
+ project_path = work / 'Contoso.Schemas.csproj'
+ probe_project = ET.parse(project_path)
+ observer = ET.SubElement(
+ probe_project.getroot(), 'Target', Name='_GraderObserveCompile_' + uuid.uuid4().hex,
+ BeforeTargets='CoreCompile',
+ )
+ ET.SubElement(observer, 'WriteLinesToFile', File=str(compile_items.resolve()),
+ Lines="@(Compile->'%(FullPath)')", Overwrite='true', Encoding='UTF-8')
+ probe_project.write(project_path, encoding='utf-8')
+ log = pathlib.Path(tmp) / 'schemagen-calls.jsonl'
+ script = bin_dir / 'schemagen.py'
+ script.write_text(
+ 'import argparse, hashlib, json, os, pathlib\n'
+ 'parser = argparse.ArgumentParser()\n'
+ 'parser.add_argument("--input-list", required=True)\n'
+ 'parser.add_argument("--output-dir", required=True)\n'
+ 'args = parser.parse_args()\n'
+ 'work = pathlib.Path(os.environ["SCHEMA_WORK"]).resolve()\n'
+ 'source_list = pathlib.Path(args.input_list).resolve()\n'
+ 'output_dir = pathlib.Path(args.output_dir).resolve()\n'
+ 'assert source_list.is_relative_to(work) and output_dir.is_relative_to(work)\n'
+ 'assert output_dir.is_dir()\n'
+ 'inputs = [pathlib.Path(line).resolve() for line in source_list.read_text(encoding="utf-8-sig").splitlines() if line.strip()]\n'
+ 'assert inputs and all(p.is_relative_to(work) and p.is_file() for p in inputs)\n'
+ 'outputs = [output_dir / (p.stem + ".g.cs") for p in inputs]\n'
+ 'for source, output in zip(inputs, outputs):\n'
+ ' digest = hashlib.sha256(source.read_bytes()).hexdigest()\n'
+ ' relative = source.relative_to(work).as_posix()\n'
+ ' output.write_bytes(("// generated schema: " + relative + "\\n// sha256: " + digest + "\\n").encode("utf-8"))\n'
+ 'with open(os.environ["SCHEMA_LOG"], "a", encoding="utf-8") as stream:\n'
+ ' stream.write(json.dumps({"inputs": [str(p) for p in inputs], "outputs": [str(p) for p in outputs], "output_dir": str(output_dir)}) + "\\n")\n',
+ encoding='utf-8', newline='\n',
+ )
+ if os.name == 'nt':
+ (bin_dir / 'schemagen.cmd').write_text(
+ '@echo off\n"' + sys.executable + '" "' + str(script) + '" %*\n',
+ encoding='utf-8', newline='\r\n',
+ )
+ else:
+ launcher = bin_dir / 'schemagen'
+ launcher.write_text(
+ '#!/bin/sh\nexec ' + shlex.quote(sys.executable) + ' ' + shlex.quote(str(script)) + ' "$@"\n',
+ encoding='utf-8', newline='\n',
+ )
+ launcher.chmod(0o755)
+ env = dict(os.environ, PATH=str(bin_dir) + os.pathsep + os.environ['PATH'],
+ SCHEMA_WORK=str(work), SCHEMA_LOG=str(log))
+ sources = set(work.joinpath('schemas').rglob('*.json'))
+ required = {work / 'schemas' / (name + '.json') for name in ['Customer', 'Invoice', 'Order']}
+ assert required == sources, 'preserve exactly the original fixture schema inputs'
+ source_bytes = {source: source.read_bytes() for source in sources}
+ # Separate past input/output mtimes without sleeps or future-dated outputs.
+ clock = time.time_ns() - 120_000_000_000
+ for source in sources:
+ os.utime(source, ns=(clock, clock))
+ def probe(phase, stale, previous_calls):
+ expected_content = {
+ source: (
+ '// generated schema: ' + source.relative_to(work).as_posix() +
+ '\n// sha256: ' + hashlib.sha256(content).hexdigest() + '\n'
+ ).encode('utf-8')
+ for source, content in source_bytes.items()
+ }
+ compile_items.unlink(missing_ok=True)
+ result = subprocess.run(
+ [dotnet, 'msbuild', 'Contoso.Schemas.csproj', '-nologo', '-verbosity:quiet',
+ '-nodeReuse:false', '-restore', '-target:Build', '-getItem:Compile',
+ '-getProperty:GeneratedAssemblyInfoFile,TargetFrameworkMonikerAssemblyAttributesPath,GeneratedGlobalUsingsFile'],
+ cwd=work, env=env, capture_output=True, text=True, timeout=60,
+ )
+ assert result.returncode == 0, phase + ': ' + result.stdout + result.stderr
+ calls = [json.loads(line) for line in log.read_text().splitlines()] if log.is_file() else []
+ assert calls[:len(previous_calls)] == previous_calls, phase + ': preserve generator observations'
+ new_calls = calls[len(previous_calls):]
+ assert len(new_calls) == (1 if stale else 0), phase + ': schema generation must execute exactly once for stale inputs and never for current inputs'
+ assert set(work.joinpath('schemas').rglob('*.json')) == sources and all(
+ source.is_file() and source.read_bytes() == content for source, content in source_bytes.items()
+ ), phase + ': generation must not modify, add or delete schema inputs'
+ evaluated = json.loads(result.stdout)
+ compiled = {pathlib.Path(item['FullPath']).resolve() for item in evaluated['Items']['Compile']}
+ output_dir = pathlib.Path(calls[0]['output_dir'])
+ expected_outputs = {
+ output_dir / (source.stem + '.g.cs'): expected_content[source]
+ for source in sources
+ }
+ assert compile_items.is_file(), phase + ': exercise the actual compilation boundary'
+ consumed = {
+ pathlib.Path(line).resolve()
+ for line in compile_items.read_text(encoding='utf-8-sig').splitlines()
+ }
+ if stale:
+ assert len(new_calls[0]['inputs']) == len(stale) and {
+ pathlib.Path(p) for p in new_calls[0]['inputs']
+ } == stale, phase + ': generate only the stale schemas, exactly once each'
+ assert {pathlib.Path(p) for p in new_calls[0]['outputs']} == {
+ output_dir / (source.stem + '.g.cs') for source in stale
+ }, phase + ': preserve the per-schema output mapping'
+ assert expected_outputs.keys() <= compiled, phase + ': every generated output must remain a Compile input'
+ assert expected_outputs.keys() <= consumed, phase + ': every generated output must be a Compile input at compilation'
+ assert all(p.is_file() for p in compiled if p.name.endswith('.g.cs')), phase + ': generated Compile inputs must exist'
+ assert all(path.is_file() and path.read_bytes() == content for path, content in expected_outputs.items()), phase + ': preserve every distinct content-dependent schema output byte-for-byte'
+ sdk_sources = {
+ (work / value).resolve() for value in evaluated['Properties'].values() if value
+ }
+ generated_sources = {
+ path.resolve() for path in output_dir.rglob('*')
+ if path.is_file() and path.suffix.lower() == '.cs'
+ }
+ assert generated_sources - sdk_sources == expected_outputs.keys(), phase + ': preserve exactly the generated C# output set'
+ allowed_sources = expected_outputs.keys() | sdk_sources
+ assert compiled <= allowed_sources and consumed <= allowed_sources, phase + ': do not introduce additional C# compiler inputs'
+ return expected_outputs, calls
+ outputs, calls = probe('clean', sources, [])
+ for output in outputs:
+ stamp = clock + 40_000_000_000
+ os.utime(output, ns=(stamp, stamp))
+ timestamps = {output: output.stat().st_mtime_ns for output in outputs}
+ unchanged, calls = probe('unchanged', set(), calls)
+ assert unchanged == outputs, 'unchanged: preserve every output mapping and byte'
+ assert all(output.stat().st_mtime_ns == stamp for output, stamp in timestamps.items()), 'unchanged: preserve every generated output timestamp'
+ changed_source = work / 'schemas' / 'Order.json'
+ source_bytes[changed_source] += b'\n'
+ changed_source.write_bytes(source_bytes[changed_source])
+ dirty_stamp = clock + 80_000_000_000
+ os.utime(changed_source, ns=(dirty_stamp, dirty_stamp))
+ partial, calls = probe('single-stale', {changed_source}, calls)
+ assert partial.keys() == outputs.keys(), 'single-stale: preserve the full output mapping'
+ changed_output = next(output for output in partial if output.name == 'Order.g.cs')
+ assert changed_output.stat().st_mtime_ns >= changed_source.stat().st_mtime_ns, 'single-stale: refreshed output must be current'
+ assert partial[changed_output] != outputs[changed_output], 'single-stale: changed input must refresh its output bytes'
+ assert all(
+ output.stat().st_mtime_ns == stamp
+ for output, stamp in timestamps.items() if output != changed_output
+ ), 'single-stale: preserve current outputs and their timestamps'
+ timestamps = {output: output.stat().st_mtime_ns for output in partial}
+ final, calls = probe('final-unchanged', set(), calls)
+ assert final == partial, 'final-unchanged: preserve every output mapping and byte'
+ assert all(output.stat().st_mtime_ns == stamp for output, stamp in timestamps.items()), 'final-unchanged: preserve every generated output timestamp'
+ expected_exit_code: 0
+ - type: output-matches
+ config:
+ pattern: '(?m)^\|\s*Work\s*\|\s*Calls\s*\|\s*Expected\s*\|\s*Avoidable ms\s*\|\s*Cause\s*\|\s*Fix\s*\|\s*On critical path\?\s*\|\r?\n\|(?:\s*:?-+:?\s*\|){7}\r?\n\|(?:[^|\r\n]*\|){7}\s*$'
+ - type: output-matches
+ config:
+ pattern: '(?m)^Verdict:[ \t]*\S[^\r\n]*$'
+ - type: prompt
+ rubric:
+ - Identified from Exec having 120 calls in one project that the generator is started once per schema file and that
+ startup dominates
+ - Replaced the per-item Exec with one invocation that uses the list option documented in schemagen.md
+ - Kept Inputs and Outputs on the target so unchanged schemas are not regenerated
+ - Preserved every generated output byte-for-byte
+ - Did not claim the build was run or the saving measured
+
+ - name: Preserve a build that already shares incremental generation
+ prompt: |
+ already-shared-work/perf-summary.txt is the performance summary from
+ dotnet msbuild already-shared-work/Build.proj -restore -m. Reduce unnecessary build work without changing what Api
+ and Worker compile. Apply a fix if one is needed. You cannot run the build from here, so work from the repository
+ files. Do not execute dotnet/MSBuild commands or collect another log; use the supplied summary and repository files.
+ tags:
+ capability: already-shared-incremental-work
+ risk: unnecessary-rewrite
+ journey: apply-fix
+ environment:
+ files:
+ - src: ./already-shared-work
+ dest: already-shared-work
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-already-shared-work"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ No fix is needed, and I left all files unchanged. GenerateSharedBuildInfo runs once in Build.proj before either project starts, and its Inputs and Outputs skip generation when the template has not changed. Both projects receive the same BuildInfoPath and compile the shared file. Copy has one call, while Csc has two calls for two projects, which is expected. Moving the generation into Directory.Build.targets would duplicate it; another run-once guard adds no benefit. I could not run the build here, so I have not measured any saving.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Shared generation | 1 | 1 | 0 | Already shared and incremental | Leave unchanged | yes, before both projects |
+ | Csc | 2 | 2 | 0 | One compilation per project | Leave unchanged | unknown |
+
+ Verdict: no repeated work worth changing
+ graders:
+ - type: run-command
+ config:
+ command: python
+ args:
+ - -c
+ - |
+ import hashlib, pathlib
+ base = pathlib.Path('already-shared-work')
+ expected = {
+ 'Api/Api.csproj': 'e2c08643e21eba9123e4b319840fec508efc44b473a7cab491d6188e5f6905ca',
+ 'Worker/Worker.csproj': 'e2c08643e21eba9123e4b319840fec508efc44b473a7cab491d6188e5f6905ca',
+ 'Build.proj': 'd3f26267e84dde321e3c4dd735f265ca477f236723b998342ba7f6f236451781',
+ 'Directory.Build.targets': '8b97e565f7189b13ac0f83a0a1278bd5e8c65eb66c371e423274bee3c33047c7',
+ 'buildinfo.cs.in': '556a905eec7a796982c0720755536df1798787bb7e05866bc1bd4fd9a722193d',
+ 'perf-summary.txt': '1d6e09453c0776384f64e432938ca515476e14b63af02f328d38dedd8a39107f',
+ }
+ actual = {p.relative_to(base).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
+ for p in base.rglob('*') if p.is_file()}
+ assert actual == expected, 'preserve every fixture file without edits, additions or deletions'
+ expected_exit_code: 0
+ - type: output-matches
+ config:
+ pattern: '(?m)^\|\s*Work\s*\|\s*Calls\s*\|\s*Expected\s*\|\s*Avoidable ms\s*\|\s*Cause\s*\|\s*Fix\s*\|\s*On critical path\?\s*\|\r?\n\|(?:\s*:?-+:?\s*\|){7}\r?\n\|(?:[^|\r\n]*\|){7}\s*$'
+ - type: output-matches
+ config:
+ pattern: '(?m)^Verdict:[ \t]*\S[^\r\n]*$'
+ - type: prompt
+ rubric:
+ - Recognized that shared generation already runs once before either project builds and is skipped when its input
+ has not changed
+ - Concluded that two compiler calls for two projects are legitimate and no repeated work needs a fix
+ - Left every file unchanged rather than adding guards or moving generation into each project
+ - Did not claim the build was run or a saving measured
+
+ - name: Recognize multi-targeting call counts as expected work
+ prompt: |
+ legit-per-project-calls/perf-summary.txt is the performance summary of the CI build. Tell me whether any task runs
+ more often than it should and is worth changing. Do not modify any file.
+ tags:
+ capability: legitimate-per-project-work
+ risk: medium
+ journey: audit
+ environment:
+ files:
+ - src: ./legit-per-project-calls
+ dest: legit-per-project-calls
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-legit-per-project-calls"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Nothing runs more often than expected. The solution has 12 projects and Directory.Build.props sets three target frameworks, so Csc, ResolveAssemblyReferences, GenerateDepsFile and ResolvePackageAssets each run 12 x 3 = 36 times, once per target framework of each project. Each project shows 4 calls, which is one outer build plus three inner builds. Copy is higher at 211 calls, which is normal because it runs per project for several targets. These counts are legitimate per-project work, so I would not change anything.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Per-TFM tasks | 36 | 36 | 0 | 12 projects times 3 TFMs | Leave unchanged | unknown |
+ | Calls per project | 4 | 4 | 0 | One outer plus three inner builds | Leave unchanged | unknown |
+
+ Verdict: no repeated work worth changing
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args:
+ - -c
+ - |
+ import hashlib, pathlib
+ base = pathlib.Path('legit-per-project-calls')
+ expected = {
+ 'Directory.Build.props': '25501be4407cbdb202defb3848da024dc276d2b67d5eea0636dbb1322ab1d634',
+ 'perf-summary.txt': 'a1bc920051a847021358ad164239be568240e1ea120c67f967da039949c6953a',
+ }
+ actual = {p.relative_to(base).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
+ for p in base.rglob('*') if p.is_file()}
+ assert actual == expected, 'preserve every fixture file without edits, additions or deletions'
+ expected_exit_code: 0
+ - type: output-matches
+ config:
+ pattern: (?i)(12\s*[x×*]\s*3|36\s*=\s*12|three target frameworks|3 target frameworks|per target framework|nothing runs more often|no repeated|not repeated|legitimate)
+ - type: output-matches
+ config:
+ pattern: '(?m)^\|\s*Work\s*\|\s*Calls\s*\|\s*Expected\s*\|\s*Avoidable ms\s*\|\s*Cause\s*\|\s*Fix\s*\|\s*On critical path\?\s*\|\r?\n\|(?:\s*:?-+:?\s*\|){7}\r?\n\|(?:[^|\r\n]*\|){7}\s*$'
+ - type: output-matches
+ config:
+ pattern: '(?m)^Verdict:[ \t]*\S[^\r\n]*$'
+ - type: prompt
+ rubric:
+ - Compared the 36 calls of Csc and the other tasks with the 12 projects and three target frameworks and concluded the
+ counts are expected
+ - Concluded that no task is repeated unnecessarily
+ - Did not invent a speedup or suggest removing multi-targeting
+
+ - name: Leave a repeated task alone when it costs almost nothing
+ prompt: |
+ negligible-repeat/perf-summary.txt is the performance summary of the nightly build, and
+ negligible-repeat/Directory.Build.targets is the repository's shared build logic. Tell me whether any work in this
+ build is done more often than necessary and whether it is worth changing. Do not modify any file.
+ tags:
+ capability: negligible-win-stop-condition
+ risk: medium
+ journey: audit
+ environment:
+ files:
+ - src: ./negligible-repeat
+ dest: negligible-repeat
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-negligible-repeat"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ The only repeat is the Exec in Directory.Build.targets, which runs 4 times because four deployment-test projects stamp the tool version. It costs 6.1 s in total. Making it run once could avoid about 4.59 s of task time. Even if all of that reduced wall-clock time, it is at most 0.43% of an 18 minute build; the 16-node count alone does not establish actual overlap. Csc and the other tasks run 212 times for 212 projects. I would not change anything: the added complexity is not worth the gain.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Tool-version probe | 4 | 1 | 4590 estimated | Invariant per-project probe | Leave unchanged: upper bound below 1% | unknown |
+
+ Verdict: no repeated work worth changing
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args:
+ - -c
+ - |
+ import hashlib, pathlib
+ base = pathlib.Path('negligible-repeat')
+ expected = {
+ 'Directory.Build.targets': '04ec3ab2ea98c4814dcf0646404177408b7940c635dde20d97ce35cd245c7c50',
+ 'perf-summary.txt': 'c9f2e67662ba7a144ab8aad46affbc247ad9d9acdc727b2e72ae94d8ad1f04f0',
+ }
+ actual = {p.relative_to(base).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
+ for p in base.rglob('*') if p.is_file()}
+ assert actual == expected, 'preserve every fixture file without edits, additions or deletions'
+ expected_exit_code: 0
+ - type: output-matches
+ config:
+ pattern: (?im)^Verdict:[ \t]*no repeated work worth changing[.!]?[ \t]*$
+ - type: output-matches
+ config:
+ pattern: '(?m)^\|\s*Work\s*\|\s*Calls\s*\|\s*Expected\s*\|\s*Avoidable ms\s*\|\s*Cause\s*\|\s*Fix\s*\|\s*On critical path\?\s*\|\r?\n\|(?:\s*:?-+:?\s*\|){7}\r?\n\|(?:[^|\r\n]*\|){7}\s*$'
+ - type: output-matches
+ config:
+ pattern: '(?m)^Verdict:[ \t]*\S[^\r\n]*$'
+ - type: prompt
+ rubric:
+ - Found that the Exec runs 4 times for about 6 s in total
+ - Compared the aggregate saving upper bound with the 18 minute wall-clock and concluded it is not worth changing
+ without assuming every busy node divides that saving
+ - Did not recommend a change to the Exec or invent other repeated work
+
+ - name: Keep revisions separate across repository roots
+ prompt: |
+ Review audit/evidence.txt from our product build. Can we reduce the Git process launches without changing the
+ revision stamped into any project? Explain the safe scope and likely benefit. Do not edit files or run a build.
+ tags: { capability: repository-working-directory-partition, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/repository-cwd.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-repository-cwd
+ agent: &audit-agent { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ The command text matches, but its working directories belong to two different Git repositories. Compute one full revision per repository and pass the matching value to that repository's 12 projects, retaining the direct-build fallback and write-if-different behavior. A single product-wide revision would incorrectly stamp Engine with Main's commit. Two calls rather than 24 could avoid 44,000 ms of aggregate task time; overlap and wall-clock gain are unknown. This is a proposal, not a measured result.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Git revision | 24 | 2 | 44000 estimated | Two repository roots | Compute once per repository | unknown |
+
+ Verdict: proposed removal of 22 executions; wall-clock not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args:
+ - -c
+ - &preserve-audit |
+ import hashlib, pathlib, sys
+ base = pathlib.Path('audit')
+ actual = {p.relative_to(base).as_posix(): hashlib.sha256(p.read_bytes()).hexdigest()
+ for p in base.rglob('*') if p.is_file()}
+ assert actual == {'evidence.txt': sys.argv[1]}, 'preserve the complete supplied evidence without edits, additions or deletions'
+ - 76dadff9d21382a7ec513453c2cca7120dc0b62ccc3cb55ad616f4125339dc61
+ expected_exit_code: 0
+ - &audit-table
+ type: output-matches
+ config:
+ pattern: '(?m)^\|\s*Work\s*\|\s*Calls\s*\|\s*Expected\s*\|\s*Avoidable ms\s*\|\s*Cause\s*\|\s*Fix\s*\|\s*On critical path\?\s*\|\r?\n\|(?:\s*:?-+:?\s*\|){7}\r?\n\|(?:[^|\r\n]*\|){7}\s*$'
+ - &audit-verdict
+ type: output-matches
+ config:
+ pattern: '(?m)^Verdict:[ \t]*\S[^\r\n]*$'
+ - type: prompt
+ rubric:
+ - Distinguished the two repository roots despite identical command text and retained each repository's original full revision
+ - Proposed two computations distributed to the matching projects, not one product-wide value
+ - Estimated 22 avoided calls and 44000 ms of aggregate task time without asserting unobserved wall-clock savings
+ - Respected the read-only request and did not claim to execute or validate a build
+
+ - name: Remove only the unused dimension of a shared project
+ prompt: |
+ audit/evidence.txt lists the Shared builds in one invocation. Which of those builds can be avoided while still
+ shipping both framework assemblies and preserving component branding? Recommend a change, without editing or building.
+ tags: { capability: required-property-partition, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/required-property-partitions.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-required-property-partition
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Shared needs net9.0 and net10.0, but Brand creates three instances inside each framework partition without affecting Shared's behavior. Remove Brand only at the component-to-Shared reference, for example with GlobalPropertiesToRemove metadata. Keep TargetFramework on Shared and Brand on the components. This leaves two Shared compilations, not one, and preserves both framework outputs and component AssemblyTitle values.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Shared build | 6 | 2 | 8040 estimated | Unused Brand within each required TFM | Remove Brand only on Shared references | unknown |
+
+ Verdict: proposed removal of 4 executions; estimated project-time saving 8.04 s; wall-clock not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 458e872dca2f02cd8ad673b41574e2c1b92f457a64741196ab747eaeb1344d67]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Kept two Shared compilations because both TargetFramework variants are required
+ - Removed only Brand at the Shared reference while preserving component branding and required framework properties
+ - Estimated four avoided executions and 8040 ms aggregate project time without inventing a wall-clock result
+ - Did not turn an advisory question into file changes or a build
+
+ - name: Trim inherited properties on an orchestrator task edge
+ prompt: |
+ The Shared builds in audit/evidence.txt look identical. How should we avoid unnecessary work without losing
+ the slice diagnostics or the Release framework settings? Do not change files or execute the build.
+ tags: { capability: msbuild-task-property-removal, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/orchestrator-removal.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-task-property-removal
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ TraceBucket is inherited by Shared as a global property, producing three executed instances although Shared does not read it. On PrepareSlice's MSBuild task, use RemoveProperties="TraceBucket". There is no ProjectReference to annotate here. Keep TraceBucket in PrepareSlice for its message, and retain Configuration=Release and TargetFramework=net10.0 on the child task. Shared can then use one common instance. Only the aggregate project-time estimate is supported.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Shared build | 3 | 1 | 12000 estimated | Inherited TraceBucket | RemoveProperties on the MSBuild task | unknown |
+
+ Verdict: proposed removal of 2 executions; wall-clock not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 157f6aad821a3bec251718e92d989235c9ce4baef28c9f069513fc40d875c79a]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Located the task edge where TraceBucket reaches Shared and proposed a valid task-level removal rather than nonexistent ProjectReference metadata
+ - Preserved parent diagnostics and the required Configuration and TargetFramework globals
+ - Reduced the expected Shared executions to one with 12000 ms estimated aggregate savings and no measured wall-clock claim
+ - Did not edit files or run a build
+
+ - name: Respect a generator that cannot accept multiple inputs
+ prompt: |
+ Can we replace the sixty generator launches described in audit/evidence.txt with one process in this release?
+ Review the available interface and advise; do not modify files or run commands against the build.
+ tags: { capability: unsupported-generator-batching, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/single-input-contract.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-single-input-contract
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Not with this installed tool contract. Sixty distinct inputs need sixty distinct outputs, and contractc accepts only one pair per process. Inventing --input-list, passing a glob, or concatenating commands inside one Exec does not create a one-process generator. Retain the existing input/output transform and compilation items. A supported multi-input tool interface would be a prerequisite to this optimization, but replacing the tool is outside this release.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | contractc | 60 | 60 | 0 with current interface | Single-input tool contract | Leave unchanged | unknown |
+
+ Verdict: no supported reduction in generator processes under the supplied constraints
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 5286fae0faefa563fac100327b951c27e1cfa125a61016ea67fc04cf24c627d3]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Concluded that the supplied tool contract cannot perform all sixty inputs in one process
+ - Did not invent list or response-file support, substitute a tool, or misrepresent one shell Exec containing sixty processes as one generator process
+ - Preserved every required output and the incremental input/output pairing without claiming a speedup
+ - Kept the response advisory without edits or build execution
+
+ - name: Preserve metadata partitions when batching a generator
+ prompt: |
+ Suggest a lower-startup-cost version of the generation step in audit/evidence.txt without changing generated APIs.
+ Work only from the supplied evidence; do not edit files or execute a build.
+ tags: { capability: generator-metadata-partition, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/metadata-batches.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-metadata-partition
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Batch stale inputs by Namespace and invoke modelgen once for each of the two namespaces with a separate overwritten UTF-8 input list. One combined list with one --namespace would change half the generated APIs. Keep each item's output mapping and the Inputs/Outputs relationship so unchanged files remain current. Processing all 120 schemas still costs 12,000 ms; only 118 process startups disappear, an estimated 59,000 ms rather than the whole original 72,000 ms.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | modelgen | 120 | 2 | 59000 estimated | Per-input process startup | One input list per namespace | unknown |
+
+ Verdict: proposed removal of 118 startups; wall-clock not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, ba9899833681fff82485e72628764d8fb1c361123c431ef951d59f8e7d8b895c]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Kept Public and Internal namespaces separate using two valid list invocations rather than flattening metadata
+ - Preserved all per-input outputs, compilation inclusion and stale-item input/output behavior
+ - Estimated 59000 ms from 118 avoided 500 ms startups while retaining unavoidable per-input processing
+ - Did not claim to modify or measure the build
+
+ - name: Batch only the stale subset of an incremental target
+ prompt: |
+ How can the generation in audit/evidence.txt start fewer processes on this invocation without touching the files
+ that are already current? Recommend the change without editing files or running the build.
+ tags: { capability: partial-incremental-batching, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/stale-items.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-stale-item-batching
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Keep the one-to-one target Inputs/Outputs transform. MSBuild supplies the stale subset to that target, so write an overwritten input list inside it from the current Model items, then run modelgen once on A, B and C. Do not build the list at evaluation time from all 120 inputs: that would rewrite the other 117 outputs. Keep CollectModels outside the incremental generation body so all required generated files are compiled even when generation is skipped. The new invocation still processes three schemas.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | modelgen for stale inputs | 3 | 1 | 1000 estimated | Three 500 ms startups | List only the stale items inside the target | unknown |
+
+ Verdict: proposed removal of 2 startups; 117 current outputs unchanged; wall-clock not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 93e66a4eb778b04742539d1516a4f9fd3b36c2fcf90d20ebc5652956a99e89c3]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Limited the list to the three stale inputs using the preserved one-to-one incremental target mapping
+ - Did not regenerate the 117 current outputs or populate the list too early from the full item set
+ - Kept all 120 generated files in compilation even on a skipped or partially incremental generation target
+ - Estimated two avoided startups and 1000 ms rather than eliminating required processing or claiming a measured build
+
+ - name: Pass an orchestrator result into child project instances
+ prompt: |
+ Our new pre-build probe did not reduce child process launches. Diagnose audit/evidence.txt and suggest a fix
+ that also works when a developer builds a single project. Do not edit files or run the build.
+ tags: { capability: orchestrator-local-property-propagation, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/local-property-propagation.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-local-property-propagation
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ BuildAll sets a local ToolVersion property, which the MSBuild task does not automatically turn into a child global property. The children therefore still see an empty value. Pass Properties="ToolVersion=$(ToolVersion)" on that task after the successful probe, preserving parallelism and the existing empty-value fallback for direct and IDE builds. Keep the probe implementation and value unchanged; do not simply delete the fallback.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | sdkprobe | 21 | 1 | 8000 estimated | Local property is not distributed to children | Pass the result as a child global property | unknown |
+
+ Verdict: proposed removal of 20 executions; wall-clock not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 1ceeeba7517d0bb70429320597aa1531d4996a01668e95bd0f61dfce2b5495fa]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Explained why a property computed locally in the orchestrator does not automatically reach the child instances
+ - Passed the computed version before child builds as a global property while retaining the direct-project fallback
+ - Preserved the probe implementation, value and parallel build behavior
+ - Estimated 20 avoided calls and 8000 ms without claiming an unperformed build or known critical path
+
+ - name: Explain why a project flag is not a worker cache
+ prompt: |
+ The team expected the flag in audit/evidence.txt to cache the catalog version for the whole invocation. Explain
+ why the counts disagree and how to reduce them safely. This is a read-only review; do not build or edit.
+ tags: { capability: project-instance-flag-scope, risk: medium, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/project-local-flag.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-project-local-flag
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ CatalogWasRead is state on each project instance, not an invocation-wide or worker-wide cache. Reusing a worker does not make another project's properties visible. Compute the immutable catalog version once before dispatch and pass the value globally to all 32 projects. Retain an empty-value fallback for direct project builds and make the version available before its consumer; merely distributing a true flag would omit the value. Do not replace this with a shared file Exists race.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Catalog version | 32 | 1 | 15500 estimated | Per-instance flag state | Distribute one computed version | unknown |
+
+ Verdict: proposed removal of 31 executions; wall-clock not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, b2cbb89d86eae4d8314ca652991be5a56f0cc448d6793cec1df1c5b4aa875493]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Located the flag state on individual project instances rather than assuming shared state on one worker or all workers
+ - Proposed distributing the actual version from one pre-dispatch computation with a working direct-project fallback
+ - Did not substitute a shared Exists guard or pass only a flag without the required value
+ - Estimated 31 avoided executions and 15500 ms aggregate task time without fabricating overlap or measurements
+
+ - name: Share generated settings only inside equivalent configurations
+ prompt: |
+ Can the settings generator in audit/evidence.txt do less work while retaining both build configurations?
+ Explain the necessary ordering and output handling. Do not edit files or run a build.
+ tags: { capability: generated-input-identity-partition, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/variant-generator-inputs.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-generator-input-partition
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ The effective config input changes with Configuration, so one output for the entire graph would be wrong. Generate Debug once and Release once before dispatching their consumers, retaining separate output paths and both Compile includes. Keep direct-build support with safe ordering and synchronization rather than concurrent Exists checks. Generate to a temporary file and replace on content change, or use equivalent write-if-different behavior, so sharing does not force an unchanged rerun to recompile.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | settingsgen | 12 | 2 | 20000 estimated | Six repeats per effective config input | Pre-generate each required configuration | unknown |
+
+ Verdict: proposed removal of 10 executions while preserving both variants; wall-clock not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, a4fba76b775ec0540b5f3b1b5b4c65f8b637ab1cf1c8f2cdd485127aced7c0ac]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Used the effective configuration file identity to require two generations rather than trusting identical command text
+ - Preserved both configuration outputs and compilation inputs, with generation completed before the relevant parallel consumers
+ - Addressed unchanged-content timestamps and safe direct-project behavior without an unsynchronized shared guard
+ - Estimated 10 avoided executions and 20000 ms aggregate savings without claiming validation
+
+ - name: Estimate serial startup savings without dividing by all workers
+ prompt: |
+ Use audit/evidence.txt to estimate the benefit of the tool's list mode. How much time is actually removable,
+ and does the worker count change that estimate? No edits or new build runs, please.
+ tags: { capability: sequential-startup-cost-accounting, risk: medium, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/sequential-startup.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-sequential-startup
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Current duration is 200 x (400 + 50) = 90,000 ms. List mode still processes every input: 400 + 200 x 50 = 10,400 ms. The modeled saving is 79,600 ms from 199 avoided startups. Do not divide by 16: these particular calls are consecutive on one node and precede the critical-path package consumer. With the supplied unchanged-scheduling assumptions the wall-clock reduction could be about 79.6 s, not a measured result. Keep the target's incremental input/output pairing and all outputs.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | assetpack startups | 200 | 1 | 79600 estimated | Serial startup overhead | Use supported list mode | yes, supplied timeline |
+
+ Verdict: proposed removal of 199 startups; modeled wall-clock saving about 79.6 s, not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 10a45a3e8e4870d88aab9489468ba7d0375acdd00f999584fc2d78c03d9086d4]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Retained 10000 ms required processing plus one 400 ms startup, giving 79600 ms estimated removable time
+ - Did not divide serial savings by the 16 available workers or count the entire old Exec duration as removable
+ - Connected the estimate to the supplied critical path without representing it as a measured post-change result
+ - Preserved all input/output pairs and respected the read-only request
+
+ - name: Convert overlapping probe time into a stage estimate
+ prompt: |
+ The total probe time in audit/evidence.txt is large. Estimate what computing the value in the wrapper would save,
+ using the supplied timeline rather than just the task total. Do not edit or run the build.
+ tags: { capability: observed-overlap-stage-accounting, risk: medium, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/overlapped-stage.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-overlapped-stage
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Aggregate probe time falls from 21,600 to 300 ms, saving 21,300 ms of task time. The observed stage is only 3,600 ms because six calls overlap in each wave. Replacing it with a 300 ms wrapper probe therefore models a 3,300 ms wall-clock reduction, about 8.25% of the 40 s build, under the stated no-overhead assumptions. Pass the unchanged value globally and retain direct-build fallback support. This estimate is not post-change measurement.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | repoprobe | 72 | 1 | 21300 aggregate, 3300 wall-clock estimated | Six concurrent calls per wave | One 300 ms wrapper probe | yes, phase barrier |
+
+ Verdict: proposed removal of 71 executions; modeled wall-clock saving 3.3 s, not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, f7d3a768b3017db0089efd0ad1d55d591ed9feb0b2f794c9efb841e1423ce9fc]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Separated 21300 ms aggregate savings from the 3300 ms stage reduction supported by the six-way overlapping timeline
+ - Included the remaining 300 ms wrapper call rather than treating all 3600 ms as removable
+ - Preserved the version value and direct-build fallback while distinguishing modeled savings from measured results
+ - Did not execute or modify the build
+
+ - name: Avoid moving noncritical work onto the critical path
+ prompt: |
+ Would moving the inventory probe to the wrapper make the build in audit/evidence.txt faster? Compare it with
+ computing the value inside Inventory. Explain your recommendation without editing or running anything.
+ tags: { capability: noncritical-work-hoisting-regression, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/off-critical-path.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-off-critical-path
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Inventory is not on the critical path and has no contention with Compiler. Removing nine probes could save 18,000 ms of aggregate task time, but computing once within Inventory still leaves the 120 s Compiler branch as the final join. It provides no modeled wall-clock saving here. A 2 s wrapper probe delays the start of Compiler and instead models a 122 s build. Do not make that move as a build-speed optimization; branch-local sharing is only a resource-cost improvement and must preserve the version for all consumers.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Inventory probe | 10 | 1 | 18000 aggregate, 0 wall-clock estimated | Repeats on a noncritical branch | Leave off the global preflight; optionally share inside Inventory | no |
+
+ Verdict: wrapper hoisting risks a 2 s regression; branch-local deduplication has no modeled wall-clock benefit
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 520fde95f44a400204b6bbf069dc232a0a5697ff4ab2a184debe971e82ce1413]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Recognized that removing Inventory work cannot shorten the independent 120 s Compiler critical path under the supplied assumptions
+ - Identified the 2 s regression from running a wrapper probe before Compiler starts
+ - Distinguished 18000 ms aggregate resource savings from zero modeled wall-clock improvement for branch-local sharing
+ - Preserved all consumers and made no edits, executions or measured-result claims
+
+ - name: Stop a failed shared preflight before child fallbacks repeat it
+ prompt: |
+ Why does the failing invocation in audit/evidence.txt run the tool so many times after the preflight? Recommend
+ safe failure behavior while keeping direct project builds supported. Do not modify files or invoke the build.
+ tags: { capability: failed-preflight-error-propagation, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/failed-preflight.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-failed-preflight
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ The wrapper's || true hides exit 17 and passes an empty version, so every child's fallback runs the same failing command. Remove the suppression or explicitly propagate the probe's exit status and diagnostics before starting MSBuild. There is no valid default version. Preserve "$@" forwarding and the fallback for direct project builds; on successful wrapper runs pass the real value globally. On this failing invocation, one failed preflight should stop all child dispatch, not produce a successful-looking build.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Failing version probe | 17 | 1 | 16000 aggregate estimated | Suppressed failure yields empty global value | Fail before dispatch with exit 17 | unknown |
+
+ Verdict: proposed removal of 16 redundant failed attempts; preserve the original failure, not a success fallback
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 424b1e79205d8ba7ead84a60b343e078516fd74bf1bd7a0a9209c71e957dcd8e]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Traced the repeated child attempts to the swallowed preflight failure and empty passed value
+ - Stopped before child dispatch with the original error status and diagnostics rather than inventing a default or silently succeeding
+ - Retained caller arguments, successful global value propagation and direct-project fallback support
+ - Distinguished an estimated reduction in failing attempts from validated build performance and did not execute or edit
+
+ - name: Preserve distinct identities despite matching command lines
+ prompt: |
+ Review the twenty-four process launches in audit/evidence.txt. Are any removable under the release contract,
+ and what would be unsafe to share? Do not change files or execute the build.
+ tags: { capability: intentionally-nondeterministic-project-work, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/per-project-identity.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-per-project-identity
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ These calls are intentionally not equivalent: identitygen creates a fresh per-project signing identity on every invocation. One product-wide cached result would give projects the same identity and violate the release contract. All 24 outputs remain necessary, and this tool cannot generate several identities in one process. Leave the step unchanged under the supplied constraints rather than infer invariance from command text or promise a 34.5 s saving.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Signing identity creation | 24 | 24 | 0 | Required distinct random identities | Leave unchanged | unknown |
+
+ Verdict: no safely removable identity-generation work with the current tool contract
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 09dfde3c236d57cf5513e05899353e7337e38687d26aa9ebeb9ffb594bb0d4fa]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Recognized nondeterministic per-project identity creation as required distinct work despite identical command text
+ - Preserved 24 distinct identities and rejected product-wide value caching or unsupported multi-identity tool modes
+ - Did not claim any removable work or performance gain under the given contract
+ - Kept the review read-only without builds or edits
+
+ - name: Fix a correctness race even when timing savings are negligible
+ prompt: |
+ The source corruption in audit/evidence.txt is rare and the task time is tiny. Should we change this step?
+ Recommend safe ordering and rerun behavior without modifying files or running a build.
+ tags: { capability: correctness-race-overrides-timing-threshold, risk: high, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/cheap-race.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-cheap-correctness-race
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Change it for correctness, not speed. Exists is a check-then-write race: parallel projects can all enter and truncate the shared file. Generate once successfully in build.sh before starting the parallel build, retain the shared Compile include, and propagate generator errors. Generate to a temporary file and replace only on byte changes so an unchanged rerun preserves the timestamp. Avoiding seven 10 ms calls saves at most 70 ms, about 0.012% of this build, but that tiny timing gain does not justify leaving source corruption unfixed.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | stampgen | 8 | 1 | 70 aggregate estimated | Concurrent Exists-guarded writers | Generate before dispatch, replace only on change | unknown |
+
+ Verdict: proposed correctness fix removing 7 concurrent writers; negligible timing benefit, not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, ae891f69fb623f1c6cc90051801c84aa6fa8460ab2a358d3011f439141ed9c01]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Recommended fixing the demonstrated source-corruption race despite the negligible timing upper bound
+ - Ensured one successful generation finishes before parallel readers and retained the shared compilation input and failure propagation
+ - Preserved unchanged-content timestamps rather than unconditionally rewriting the generated source on every wrapper invocation
+ - Distinguished the 70 ms estimated aggregate saving from the correctness benefit without claiming a measured build
+
+ - name: Share a value between different targets in one project instance
+ prompt: |
+ Both consumers in audit/evidence.txt need the revision. Explain why it is computed twice and recommend how
+ to compute it once without breaking Build-only or Pack-only use. Do not modify files or execute the build.
+ tags: { capability: same-instance-cross-target-sharing, risk: medium, journey: audit }
+ environment:
+ files:
+ - { src: ./audit-scenarios/two-targets-one-instance.txt, dest: audit/evidence.txt }
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: repeated-work-cross-target-sharing
+ agent: *audit-agent
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ The two Exec tasks live in different targets; target-once-per-instance semantics do not merge their bodies. Extract one revision-producing target and make both StampAssembly and StampPackage depend on it. In this single project instance, the shared dependency executes once before either consumer and exposes the same full revision to both. Keep both consumer hooks so either entry path obtains the value, rather than depending on Build to run before Pack. No cross-project global-state mechanism is required.
+
+ | Work | Calls | Expected | Avoidable ms | Cause | Fix | On critical path? |
+ |---|---|---|---|---|---|---|
+ | Git revision | 2 | 1 | 1500 estimated | Separate target bodies repeat one lookup | Shared dependency target for both consumers | unknown |
+
+ Verdict: proposed removal of 1 execution; both standalone entry paths retained; wall-clock not validated
+ graders:
+ - type: tool-calls
+ config:
+ disallowed: [edit, create]
+ - type: run-command
+ config:
+ command: python
+ args: [-c, *preserve-audit, 710ea3d8de55e5b2305ba8e2dac4d45956616681c55b76fb95059e530046ef62]
+ expected_exit_code: 0
+ - *audit-table
+ - *audit-verdict
+ - type: prompt
+ rubric:
+ - Explained that identical tasks in different targets can execute twice in one project instance
+ - Proposed one shared computation ordered before both consumers, valid for either standalone entry path
+ - Preserved both full-revision consumers without assuming cross-project property sharing or requiring Build before Pack
+ - Estimated one avoided call and 1500 ms without editing, executing or claiming measured performance
+
+ - name: Decline evaluation-only global-property instances
+ prompt: |
+ A binlog shows Shared.csproj evaluated five times under different global properties. Evaluation accounts for
+ almost all the delay, but its targets and tasks execute only once. How should I investigate that cost?
+ tags:
+ expect_activation: "false"
+ capability: dormancy-property-variant-evaluation
+ risk: routing
+ journey: boundary-condition
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-property-evaluation-dormancy"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ The measured cost is project evaluation, not repeated target execution. Compare the evaluated global-property sets, then inspect evaluation timings, import chains, globs and file-I/O property functions. Use preprocessed output to locate expensive imports and preserve property variants that are actually required. Target run-once guards cannot remove this pre-target work.
+ graders:
+ - type: output-matches
+ config:
+ pattern: (?i)(evaluat|preprocess|import|glob)
+ - type: prompt
+ rubric:
+ - Identified evaluation as the measured bottleneck rather than duplicate target or task execution
+ - Investigated evaluation work and distinct global-property sets without proposing a per-target run-once guard
+ - Preserved required property variants
+
+ - name: Decline collisions between required project variants
+ prompt: |
+ Build.proj builds Shared.csproj concurrently with PublishReadyToRun=true and PublishReadyToRun=false.
+ We need both variants, but they use the same bin and obj directories and overwrite each other's files.
+ How should I stop the failures without dropping either variant?
+ tags:
+ expect_activation: "false"
+ capability: dormancy-property-variant-output-collision
+ risk: routing
+ journey: boundary-condition
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-property-collision-dormancy"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ These are intentionally different variants, not redundant builds. Give each variant separate output and intermediate paths, setting the intermediate base before SDK imports so restore assets are isolated too. Verify that both configurations still produce their intended assemblies. Do not remove PublishReadyToRun or collapse the builds merely because they share a project path.
+ graders:
+ - type: output-matches
+ config:
+ pattern: (?i)(OutputPath|IntermediateOutputPath|separate|isolat|distinct.*director)
+ - type: prompt
+ rubric:
+ - Identified shared output and intermediate paths as the cause of the collision
+ - Isolated both required variants' outputs and restore intermediates
+ - Did not remove a required global property or collapse the variants into one build
+
+ - name: Decline an unranked slow-build investigation
+ prompt: |
+ Our .NET solution builds slowly in CI, but we do not have timings or logs that show where the time goes.
+ What should we collect first before deciding what to change?
+ tags:
+ expect_activation: "false"
+ capability: dormancy-unranked-build-slowness
+ risk: routing
+ journey: boundary-condition
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-unranked-slowness-dormancy"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Establish a baseline before choosing a fix. Record the SDK, original build command, configuration, machine and restore state. Measure comparable cold, warm and unchanged builds at least three times each, resetting the intended state before every sample and reporting medians and ranges. Capture a separate binary log for each sample without changing the original invocation's restore or parallelism behavior. Use those timings and logs to locate the dominant phase; do not assume compilation, restore or duplicate work is the bottleneck.
+ graders:
+ - type: output-matches
+ config:
+ pattern: (?i)(measur|baseline|timing|profil|binlog|binary log)
+ - type: prompt
+ rubric:
+ - Recommended collecting repeatable build timings and diagnostic logs before choosing an optimization
+ - Kept the original invocation and build environment comparable across measurements
+ - Did not assume a specific bottleneck or propose deduplication without evidence
+
+ - name: Decline second-build rerun questions
+ prompt: |
+ On a second dotnet build of an unchanged checkout, CoreCompile still runs for every project instead of being skipped.
+ Why does MSBuild not treat the projects as up to date?
+ tags:
+ expect_activation: "false"
+ capability: dormancy-no-op-rebuild
+ risk: routing
+ journey: boundary-condition
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-no-op-rebuild-dormancy"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ CoreCompile is skipped only when every input is older than the outputs it declares. A second build that still compiles means an input is newer than an output or a declared output is missing. Typical causes are a generator that rewrites a source file on every build, a Compile glob that picks up files in obj or bin, a timestamp written to an input, or an Inputs and Outputs list that does not match what the target really reads and writes. Build with a detailed log and look for the message that names the input or output that made the target run again.
+ graders:
+ - type: output-matches
+ config:
+ pattern: (?i)(incremental|up-to-date|up to date|timestamp|inputs|outputs|FileWrites)
+ - type: prompt
+ rubric:
+ - Recognized that the question is about a target rerunning on a second unchanged build, not duplicate executions
+ within one build
+ - Explained the incremental build mechanism, such as inputs and outputs, timestamps, or files that change on every build
+ - Did not apply within-build deduplication fixes, such as computing a value outside the projects or removing global
+ properties, to an incremental-build question
+
+ - name: Decline repeated restore questions across CI commands
+ prompt: |
+ Our CI runs dotnet restore, dotnet build and dotnet test for the same solution. The logs show NuGet restore three
+ times. How can we avoid the unnecessary restore work without skipping required package downloads?
+ tags:
+ expect_activation: "false"
+ capability: dormancy-repeated-restores
+ risk: routing
+ journey: boundary-condition
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-restore-dormancy"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ This is NuGet restore repeated across separate CI commands, not duplicate task execution inside one build. Restore once for the required project graph, frameworks, runtimes and configuration, then use dotnet build --no-restore and dotnet test --no-restore with matching inputs. Keep restore when package or project inputs change or assets are missing. The restore guidance belongs to eliminate-redundant-restores; changing per-project targets or removing global properties is not needed here.
+ graders:
+ - type: output-matches
+ config:
+ pattern: (?i)(--no-restore|separate.*restore|restore once)
+ - type: prompt
+ rubric:
+ - Recognized implicit NuGet restores across separate commands rather than repeated custom work inside one build
+ - Recommended reusing a valid restore with matching project, framework, runtime and configuration inputs
+ - Redirected restore analysis to the restore-performance guidance
+ - Did not apply per-project deduplication fixes or remove global properties
+
+ - name: Decline slow evaluation before target execution
+ prompt: |
+ dotnet build spends about 35 seconds before the first target starts, then compilation finishes in two seconds.
+ Directory.Build.props has recursive globs and the repository imports many props and targets files. How should I
+ investigate that startup delay?
+ tags:
+ expect_activation: "false"
+ capability: dormancy-slow-evaluation
+ risk: routing
+ journey: boundary-condition
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-evaluation-dormancy"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ Time before targets start points to project evaluation, not repeated target or task execution. Capture a binlog of the original build and inspect evaluation durations and import/glob costs with the binlog MCP evaluation and imports tools; use eval-performance for that investigation. Preprocessing can show the expanded import structure, but measure first before rewriting globs or imports. Per-project task deduplication and run-once guards are not needed here.
+ graders:
+ - type: output-matches
+ config:
+ pattern: (?i)(evaluation|imports|globs)
+ - type: prompt
+ rubric:
+ - Distinguished evaluation before target execution from time spent compiling or running targets
+ - Recommended measuring evaluation and inspecting expensive imports or glob traversal before editing build files
+ - Redirected the startup-delay investigation to evaluation-performance analysis
+ - Did not apply task deduplication or run-once guards to an evaluation problem
+
+ - name: Decline idle-node questions on a serial project chain
+ prompt: |
+ Our CI runs dotnet msbuild Build.proj -m:16. Its MSBuild task schedules a Projects list of 12 distinct project
+ files with BuildInParallel enabled. The binlog shows each project compiling once, but most worker nodes are idle
+ while Core, Api and Web build one after another along their ProjectReference chain. Raising -m does not reduce
+ elapsed time. How should I investigate and improve this?
+ tags:
+ expect_activation: "false"
+ capability: dormancy-serial-project-chain
+ risk: routing
+ journey: boundary-condition
+ golden_trajectory:
+ inline:
+ schema_version: "ATIF-v1.6"
+ session_id: "find-repeated-build-work-parallelism-dormancy"
+ agent: { name: reference, version: "1.0.0", model_name: curated }
+ steps:
+ - step_id: 1
+ source: agent
+ message: |-
+ The idle nodes and Core -> Api -> Web dependency chain point to scheduling and a serial critical path, not repeated work: every project compiles once. Use build-parallelism to inspect the binlog node timeline and project timings, identify the longest dependency chain, and confirm which references are genuinely needed. More -m workers cannot run a dependent project before its prerequisite finishes. Keep legitimate work and required references; remove only proven unnecessary dependencies or reorganize independent work so it can run concurrently. Per-project run-once guards, merging Exec calls, or removing global properties do not address this evidence.
+ graders:
+ - type: output-matches
+ config:
+ pattern: (?i)(critical path|serial.*chain|dependency.*chain)
+ - type: output-matches
+ config:
+ pattern: (?i)(build-parallelism|parallelism analysis|scheduling analysis)
+ - type: prompt
+ rubric:
+ - Recognized idle workers and the serial ProjectReference chain as scheduling or critical-path evidence rather
+ than unnecessary repeated execution
+ - Explained why raising the worker count cannot parallelize required dependencies and recommended inspecting
+ the node timeline and project timings
+ - Did not apply repeated-work fixes such as run-once guards, merging Exec calls or removing global properties
+ to legitimate compilation, and did not remove required references
+ - Redirected the investigation to build-parallelism
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/Directory.Build.targets b/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/Directory.Build.targets
new file mode 100644
index 0000000000..ce10029da7
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/Directory.Build.targets
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/build.sh b/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/build.sh
new file mode 100644
index 0000000000..63ec76fdec
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/build.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+dotnet build Contoso.slnx -c Release -m
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/failure-log.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/failure-log.txt
new file mode 100644
index 0000000000..14aca68f65
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/failure-log.txt
@@ -0,0 +1,12 @@
+Failing step "Build" in 2 of the last 30 CI runs; the re-run passed each time.
+The solution has 24 projects and the agent builds with -m.
+
+obj/buildinfo.g.cs(2,9): error CS1010: Newline in constant [src/Contoso.Core/Contoso.Core.csproj]
+
+The captured generated source contained two date suffixes:
+ internal static class BuildInfo { public const string Stamp = "20261030"; }
+ 20261030"; }
+
+Task Performance Summary from a passing run of the same step:
+ 62810 ms Csc 24 calls 2617 ms avg
+ 3920 ms Exec 9 calls 435 ms avg
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/tools/make-buildinfo.sh b/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/tools/make-buildinfo.sh
new file mode 100644
index 0000000000..b576924933
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/exists-sentinel-race/tools/make-buildinfo.sh
@@ -0,0 +1,8 @@
+#!/usr/bin/env sh
+set -eu
+
+out="$1"
+mkdir -p "$(dirname "$out")"
+printf 'internal static class BuildInfo { public const string Stamp = "' > "$out"
+stamp="$(date -u +%Y%m%d)"
+printf '%s"; }\n' "${BUILDINFO_STAMP:-$stamp}" >> "$out"
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/Directory.Build.props b/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/Directory.Build.props
new file mode 100644
index 0000000000..31fb1202a6
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+ 3.2.0
+ net10.0
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/Directory.Build.targets b/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/Directory.Build.targets
new file mode 100644
index 0000000000..809f5ff252
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/Directory.Build.targets
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+ $(Version)+$(GitCommit)
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/build.sh b/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/build.sh
new file mode 100644
index 0000000000..63ec76fdec
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/build.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+dotnet build Contoso.slnx -c Release -m
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/perf-summary.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/perf-summary.txt
new file mode 100644
index 0000000000..4d90447e42
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/git-stamp/perf-summary.txt
@@ -0,0 +1,17 @@
+Performance summary from the CI run of ./build.sh
+(Contoso.slnx has 38 projects; the agent has 8 cores; Time Elapsed 00:06:12.40)
+
+Task Performance Summary:
+ 77210 ms Csc 38 calls 2032 ms avg
+ 74880 ms Exec 38 calls 1970 ms avg
+ 31204 ms ResolveAssemblyReferences 38 calls 821 ms avg
+ 12877 ms Copy 163 calls 79 ms avg
+ 5310 ms WriteLinesToFile 76 calls 70 ms avg
+ 2144 ms GenerateDepsFile 38 calls 56 ms avg
+
+Project Performance Summary:
+ 372400 ms Contoso.slnx 1 calls
+ 21930 ms src\Contoso.Core\Contoso.Core.csproj 1 calls
+ 14418 ms src\Contoso.Data\Contoso.Data.csproj 1 calls
+ 11702 ms src\Contoso.Web\Contoso.Web.csproj 1 calls
+ ... 35 more projects, 1 call each
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/graders/ReadAssemblyAttribute.targets b/tests/dotnet-msbuild/find-repeated-work-within-one-build/graders/ReadAssemblyAttribute.targets
new file mode 100644
index 0000000000..01c3c12b0e
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/graders/ReadAssemblyAttribute.targets
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/legit-per-project-calls/Directory.Build.props b/tests/dotnet-msbuild/find-repeated-work-within-one-build/legit-per-project-calls/Directory.Build.props
new file mode 100644
index 0000000000..71198780a3
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/legit-per-project-calls/Directory.Build.props
@@ -0,0 +1,6 @@
+
+
+ net8.0;net9.0;net10.0
+ latest
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/legit-per-project-calls/perf-summary.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/legit-per-project-calls/perf-summary.txt
new file mode 100644
index 0000000000..af05bd5c38
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/legit-per-project-calls/perf-summary.txt
@@ -0,0 +1,16 @@
+Performance summary from the CI run of Contoso.slnx
+(12 projects, every project multi-targets through Directory.Build.props; Time Elapsed 00:04:03.60)
+
+Task Performance Summary:
+ 143310 ms Csc 36 calls 3981 ms avg
+ 29964 ms ResolveAssemblyReferences 36 calls 832 ms avg
+ 14472 ms Copy 211 calls 69 ms avg
+ 9288 ms GenerateDepsFile 36 calls 258 ms avg
+ 2736 ms ResolvePackageAssets 36 calls 76 ms avg
+
+Project Performance Summary:
+ 243600 ms Contoso.slnx 1 calls
+ 31480 ms src\Contoso.Core\Contoso.Core.csproj 4 calls
+ 27915 ms src\Contoso.Data\Contoso.Data.csproj 4 calls
+ 22140 ms src\Contoso.Services\Contoso.Services.csproj 4 calls
+ ... 9 more projects, 4 calls each
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/negligible-repeat/Directory.Build.targets b/tests/dotnet-msbuild/find-repeated-work-within-one-build/negligible-repeat/Directory.Build.targets
new file mode 100644
index 0000000000..e6af4d8f37
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/negligible-repeat/Directory.Build.targets
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/negligible-repeat/perf-summary.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/negligible-repeat/perf-summary.txt
new file mode 100644
index 0000000000..ab7d602396
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/negligible-repeat/perf-summary.txt
@@ -0,0 +1,15 @@
+Performance summary from the nightly run of Contoso.slnx
+(212 projects; built with -m:16; Time Elapsed 00:18:07.40)
+
+Task Performance Summary:
+ 2713480 ms Csc 212 calls 12799 ms avg
+ 684910 ms ResolveAssemblyReferences 212 calls 3231 ms avg
+ 251622 ms Copy 1884 calls 134 ms avg
+ 18240 ms GenerateDepsFile 212 calls 86 ms avg
+ 6120 ms Exec 4 calls 1530 ms avg
+
+Project Performance Summary:
+ 1087400 ms Contoso.slnx 1 calls
+ 98220 ms src\Contoso.Platform\Contoso.Platform.csproj 1 calls
+ 91870 ms src\Contoso.Storage\Contoso.Storage.csproj 1 calls
+ ... 209 more projects, 1 call each
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/Contoso.Schemas.csproj b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/Contoso.Schemas.csproj
new file mode 100644
index 0000000000..f04b8ad640
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/Contoso.Schemas.csproj
@@ -0,0 +1,6 @@
+
+
+ net10.0
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/Schemas.targets b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/Schemas.targets
new file mode 100644
index 0000000000..a63d5cf2e7
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/Schemas.targets
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/perf-summary.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/perf-summary.txt
new file mode 100644
index 0000000000..5d4e0a97f3
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/perf-summary.txt
@@ -0,0 +1,11 @@
+Performance summary from the CI run of Contoso.Schemas.csproj
+(the real schemas/ directory holds 120 files; three samples are checked in here; Time Elapsed 00:03:51.20)
+
+Task Performance Summary:
+ 168240 ms Exec 120 calls 1402 ms avg
+ 21390 ms Csc 1 calls
+ 1180 ms MakeDir 1 calls
+
+Target Performance Summary:
+ 169610 ms GenerateSchemaCode 1 calls
+ 21402 ms CoreCompile 1 calls
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/schemas/Customer.json b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/schemas/Customer.json
new file mode 100644
index 0000000000..17d300a0fd
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/schemas/Customer.json
@@ -0,0 +1 @@
+{ "title": "Customer", "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" } } }
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/schemas/Invoice.json b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/schemas/Invoice.json
new file mode 100644
index 0000000000..07a55856ac
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/schemas/Invoice.json
@@ -0,0 +1 @@
+{ "title": "Invoice", "type": "object", "properties": { "id": { "type": "string" }, "orderId": { "type": "string" } } }
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/schemas/Order.json b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/schemas/Order.json
new file mode 100644
index 0000000000..4162ba847b
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/schemas/Order.json
@@ -0,0 +1 @@
+{ "title": "Order", "type": "object", "properties": { "id": { "type": "string" }, "total": { "type": "number" } } }
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/tools/schemagen.md b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/tools/schemagen.md
new file mode 100644
index 0000000000..5350a0ba89
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/per-file-exec/tools/schemagen.md
@@ -0,0 +1,13 @@
+# schemagen
+
+Generates C# classes from JSON schemas. Every process start loads the type catalog, which takes about 1.4 s
+no matter how many schemas are generated.
+
+```
+schemagen --input --output
+schemagen --input-list --output-dir
+```
+
+- `--input` converts one schema into one file.
+- `--input-list` reads one schema path per line and writes `.g.cs` for each into `--output-dir`.
+ The output is identical to running `--input` once per schema.
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/compute-once-pass-global-property.patch b/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/compute-once-pass-global-property.patch
new file mode 100644
index 0000000000..97880043c7
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/compute-once-pass-global-property.patch
@@ -0,0 +1,27 @@
+diff --git a/git-stamp/Directory.Build.targets b/git-stamp/Directory.Build.targets
+index 809f5ff..299fb2d 100644
+--- a/git-stamp/Directory.Build.targets
++++ b/git-stamp/Directory.Build.targets
+@@ -1,7 +1,9 @@
+
+-
++
+
+-
++
+
+
+
+diff --git a/git-stamp/build.sh b/git-stamp/build.sh
+index 63ec76f..d93299e 100644
+--- a/git-stamp/build.sh
++++ b/git-stamp/build.sh
+@@ -1,4 +1,5 @@
+ #!/usr/bin/env bash
+ set -euo pipefail
+
+-dotnet build Contoso.slnx -c Release -m
++commit="$(git rev-parse --short HEAD)"
++dotnet build Contoso.slnx -c Release -m -p:GitCommit="$commit"
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/generate-before-build.patch b/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/generate-before-build.patch
new file mode 100644
index 0000000000..a76ef91964
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/generate-before-build.patch
@@ -0,0 +1,37 @@
+diff --git a/exists-sentinel-race/Directory.Build.targets b/exists-sentinel-race/Directory.Build.targets
+index ce10029..080d63c 100644
+--- a/exists-sentinel-race/Directory.Build.targets
++++ b/exists-sentinel-race/Directory.Build.targets
+@@ -1,9 +1,5 @@
+
+-
+-
+-
+-
+-
++
+
+
+
+diff --git a/exists-sentinel-race/build.sh b/exists-sentinel-race/build.sh
+--- a/exists-sentinel-race/build.sh
++++ b/exists-sentinel-race/build.sh
+@@ -1,4 +1,18 @@
+ #!/usr/bin/env bash
+ set -euo pipefail
+
++mkdir -p obj
++generated="$(mktemp obj/buildinfo.XXXXXX)"
++trap 'rm -f "$generated"' EXIT
++sh tools/make-buildinfo.sh "$generated"
++if [ ! -e obj/buildinfo.g.cs ]; then
++ mv "$generated" obj/buildinfo.g.cs
++elif cmp "$generated" obj/buildinfo.g.cs >/dev/null; then
++ :
++else
++ status=$?
++ [ "$status" -eq 1 ] || exit "$status"
++ mv "$generated" obj/buildinfo.g.cs
++fi
++
+ dotnet build Contoso.slnx -c Release -m
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/remove-property-from-shared-reference.patch b/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/remove-property-from-shared-reference.patch
new file mode 100644
index 0000000000..6afc312a98
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/remove-property-from-shared-reference.patch
@@ -0,0 +1,36 @@
+diff --git a/shared-project-instances/Api/Api.csproj b/shared-project-instances/Api/Api.csproj
+index d73ec1d..51733bb 100644
+--- a/shared-project-instances/Api/Api.csproj
++++ b/shared-project-instances/Api/Api.csproj
+@@ -4,6 +4,6 @@
+ Contoso $(ComponentName)
+
+
+-
++
+
+
+diff --git a/shared-project-instances/Web/Web.csproj b/shared-project-instances/Web/Web.csproj
+index d73ec1d..51733bb 100644
+--- a/shared-project-instances/Web/Web.csproj
++++ b/shared-project-instances/Web/Web.csproj
+@@ -4,6 +4,6 @@
+ Contoso $(ComponentName)
+
+
+-
++
+
+
+diff --git a/shared-project-instances/Worker/Worker.csproj b/shared-project-instances/Worker/Worker.csproj
+index d73ec1d..51733bb 100644
+--- a/shared-project-instances/Worker/Worker.csproj
++++ b/shared-project-instances/Worker/Worker.csproj
+@@ -4,6 +4,6 @@
+ Contoso $(ComponentName)
+
+
+-
++
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/single-exec-input-list.patch b/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/single-exec-input-list.patch
new file mode 100644
index 0000000000..fcb9b030a5
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/references/single-exec-input-list.patch
@@ -0,0 +1,14 @@
+diff --git a/per-file-exec/Schemas.targets b/per-file-exec/Schemas.targets
+index a63d5cf..1c3298c 100644
+--- a/per-file-exec/Schemas.targets
++++ b/per-file-exec/Schemas.targets
+@@ -7,7 +7,8 @@
+ Inputs="@(SchemaFile)"
+ Outputs="@(SchemaFile->'$(IntermediateOutputPath)gen/%(Filename).g.cs')">
+
+-
++
++
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Api/Api.csproj b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Api/Api.csproj
new file mode 100644
index 0000000000..d73ec1da5b
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Api/Api.csproj
@@ -0,0 +1,9 @@
+
+
+ net10.0
+ Contoso $(ComponentName)
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Build.proj b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Build.proj
new file mode 100644
index 0000000000..329a5de2d7
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Build.proj
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Core/Core.csproj b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Core/Core.csproj
new file mode 100644
index 0000000000..555ae434a7
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Core/Core.csproj
@@ -0,0 +1,5 @@
+
+
+ net10.0
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Web/Web.csproj b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Web/Web.csproj
new file mode 100644
index 0000000000..d73ec1da5b
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Web/Web.csproj
@@ -0,0 +1,9 @@
+
+
+ net10.0
+ Contoso $(ComponentName)
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Worker/Worker.csproj b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Worker/Worker.csproj
new file mode 100644
index 0000000000..d73ec1da5b
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/Worker/Worker.csproj
@@ -0,0 +1,9 @@
+
+
+ net10.0
+ Contoso $(ComponentName)
+
+
+
+
+
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/build.sh b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/build.sh
new file mode 100644
index 0000000000..8c8c411cfa
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/build.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+for p in Web Api Worker; do dotnet restore "$p/$p.csproj"; done
+dotnet msbuild Build.proj -m
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/perf-summary.txt b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/perf-summary.txt
new file mode 100644
index 0000000000..4f3663025b
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/shared-project-instances/perf-summary.txt
@@ -0,0 +1,13 @@
+Performance summary from the CI run of ./build.sh
+(Time Elapsed 00:02:48.10)
+
+Project Performance Summary:
+ 168100 ms Build.proj 1 calls
+ 63090 ms Core\Core.csproj 3 calls
+ 12840 ms Web\Web.csproj 1 calls
+ 10977 ms Api\Api.csproj 1 calls
+ 9302 ms Worker\Worker.csproj 1 calls
+
+Target Performance Summary:
+ 63010 ms CoreCompile 6 calls
+ 12460 ms ResolveAssemblyReferences 6 calls
diff --git a/tests/dotnet-msbuild/find-repeated-work-within-one-build/test_graders.py b/tests/dotnet-msbuild/find-repeated-work-within-one-build/test_graders.py
new file mode 100644
index 0000000000..1cf553d486
--- /dev/null
+++ b/tests/dotnet-msbuild/find-repeated-work-within-one-build/test_graders.py
@@ -0,0 +1,1311 @@
+import datetime
+import os
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import sys
+import tempfile
+import unittest
+import xml.etree.ElementTree as ET
+
+import yaml
+
+
+SUITE = Path(__file__).resolve().parent
+SPEC = yaml.safe_load((SUITE / "eval.yaml").read_text(encoding="utf-8"))
+
+
+class GraderTests(unittest.TestCase):
+ def workspace(self, capability):
+ stimulus = next(s for s in SPEC["stimuli"] if s["tags"]["capability"] == capability)
+ temporary = tempfile.TemporaryDirectory(prefix="repeated-work-grader-")
+ self.addCleanup(temporary.cleanup)
+ work = Path(temporary.name)
+ for entry in stimulus["environment"]["files"]:
+ source, target = SUITE / entry["src"], work / entry["dest"]
+ if source.is_dir():
+ shutil.copytree(source, target)
+ else:
+ target.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copy2(source, target)
+ if "golden_patch" in stimulus:
+ result = subprocess.run(
+ ["git", "apply", "--whitespace=nowarn", str(SUITE / stimulus["golden_patch"]["path"])],
+ cwd=work, capture_output=True, text=True,
+ )
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+ args = next(g["config"]["args"] for g in stimulus["graders"]
+ if g["type"] == "run-command")
+ self.assertEqual(args[0], "-c")
+ script = "import sys\nsys.argv = " + repr(["-c", *args[2:]]) + "\n" + args[1]
+ return work, script
+
+ def grade(self, work, script, *, accepted=True):
+ env = dict(os.environ)
+ additions = [str(Path(sys.executable).parent)]
+ if os.name == "nt":
+ additions.append(str(Path(os.environ.get("ProgramFiles", r"C:\Program Files")) / "Git" / "bin"))
+ env["PATH"] = os.pathsep.join([*additions, env["PATH"]])
+ result = subprocess.run(
+ [sys.executable, "-c", script], cwd=work, env=env,
+ capture_output=True, text=True, encoding="utf-8", timeout=240,
+ )
+ if accepted:
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+ else:
+ self.assertNotEqual(result.returncode, 0, "grader accepted the broken mutation")
+ return result
+
+ def test_all_golden_patches(self):
+ for capability in (
+ "invariant-per-project-exec", "global-property-instances",
+ "racy-run-once-guard", "per-item-exec",
+ ):
+ with self.subTest(capability=capability):
+ self.grade(*self.workspace(capability))
+
+ def test_git_stamp_conditions_are_evaluated_by_msbuild(self):
+ variants = (
+ ("Target", "'$(Version)' == '3.2.0'"),
+ ("Target/PropertyGroup", "'$(GitCommit)' != ''"),
+ ("Target/PropertyGroup/InformationalVersion", "'$(GitCommit)' != ''"),
+ )
+ for selector, enabled in variants:
+ for accepted, condition in ((True, enabled), (False, "'false' == 'true'")):
+ with self.subTest(element=selector, accepted=accepted):
+ work, script = self.workspace("invariant-per-project-exec")
+ path = work / "git-stamp" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ tree.getroot().find(selector).set("Condition", condition)
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=accepted)
+ if not accepted:
+ self.assertIn("the commit and version must be ready", result.stderr)
+
+ def test_equivalent_git_fallback_conditions_are_accepted(self):
+ for condition in ("('$(GitCommit)' == '')", "'' == '$(GitCommit)'"):
+ with self.subTest(condition=condition):
+ work, script = self.workspace("invariant-per-project-exec")
+ path = work / "git-stamp" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target/Exec").set("Condition", condition)
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_early_sdk_git_stamp_hooks_are_accepted(self):
+ for hook in ("BeforeBuild", "PrepareForBuild"):
+ with self.subTest(hook=hook):
+ work, script = self.workspace("invariant-per-project-exec")
+ path = work / "git-stamp" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ properties = ET.SubElement(tree.getroot(), "PropertyGroup")
+ ET.SubElement(properties, "IncludeSourceRevisionInInformationalVersion").text = "false"
+ tree.getroot().find("Target").set("BeforeTargets", hook)
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_early_git_stamp_preserves_the_final_sdk_version(self):
+ work, script = self.workspace("invariant-per-project-exec")
+ path = work / "git-stamp" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target").set("BeforeTargets", "BeforeBuild")
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("the commit and version must be ready", result.stderr)
+
+ def test_equivalent_git_stamp_rewrites_are_accepted(self):
+ for variant in ("renamed-property", "property-function"):
+ with self.subTest(variant=variant):
+ work, script = self.workspace("invariant-per-project-exec")
+ base = work / "git-stamp"
+ if variant == "renamed-property":
+ for filename in ("Directory.Build.targets", "build.sh"):
+ path = base / filename
+ path.write_text(
+ path.read_text(encoding="utf-8").replace("GitCommit", "BuildRevision"),
+ encoding="utf-8",
+ )
+ else:
+ path = base / "Directory.Build.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target/PropertyGroup/InformationalVersion").text = (
+ "$([System.String]::Concat('$(Version)', '+', '$(GitCommit)'))"
+ )
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_equivalent_git_lookup_commands_are_accepted(self):
+ for command in ("git rev-parse --verify --short HEAD", "git log -1 --format=%h"):
+ with self.subTest(command=command):
+ work, script = self.workspace("invariant-per-project-exec")
+ base = work / "git-stamp"
+ for filename in ("build.sh", "Directory.Build.targets"):
+ path = base / filename
+ replacement = command
+ if os.name == "nt" and filename.endswith(".targets"):
+ replacement = command.replace("%", "%%")
+ path.write_text(
+ path.read_text(encoding="utf-8").replace("git rev-parse --short HEAD", replacement),
+ encoding="utf-8",
+ )
+ self.grade(work, script)
+
+ def test_git_queries_must_return_the_current_revision(self):
+ work, script = self.workspace("invariant-per-project-exec")
+ base = work / "git-stamp"
+ for filename in ("build.sh", "Directory.Build.targets"):
+ path = base / filename
+ path.write_text(
+ path.read_text(encoding="utf-8").replace(
+ "git rev-parse --short HEAD", "git rev-parse --abbrev-ref HEAD",
+ ),
+ encoding="utf-8",
+ )
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("the commit and version must be ready", result.stderr)
+
+ def test_git_metadata_probes_do_not_count_as_revision_lookups(self):
+ work, script = self.workspace("invariant-per-project-exec")
+ base = work / "git-stamp"
+ wrapper = base / "build.sh"
+ wrapper.write_text(
+ wrapper.read_text(encoding="utf-8").replace(
+ 'commit="$(git rev-parse --short HEAD)"',
+ '[ "$(git rev-parse --is-inside-work-tree)" = true ]\n'
+ 'commit="$(git rev-parse --short HEAD)"',
+ ), encoding="utf-8",
+ )
+ path = base / "Directory.Build.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target").insert(
+ 0, ET.Element("Exec", Command="git rev-parse --is-inside-work-tree",
+ Condition="'$(GitCommit)' == ''"),
+ )
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_git_metadata_probes_cannot_hide_repeated_revision_lookups(self):
+ work, script = self.workspace("invariant-per-project-exec")
+ wrapper = work / "git-stamp" / "build.sh"
+ wrapper.write_text(
+ wrapper.read_text(encoding="utf-8").replace(
+ 'commit="$(git rev-parse --short HEAD)"',
+ 'git rev-parse --is-inside-work-tree >/dev/null\n'
+ 'commit="$(git rev-parse --short HEAD)"\n'
+ 'git rev-parse HEAD >/dev/null',
+ ), encoding="utf-8",
+ )
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("the fallback must run once", result.stderr)
+
+ def test_sdk_git_metadata_needs_no_external_revision_process(self):
+ work, script = self.workspace("invariant-per-project-exec")
+ base = work / "git-stamp"
+ shutil.copy2(SUITE / "git-stamp" / "build.sh", base / "build.sh")
+ path = base / "Directory.Build.targets"
+ tree = ET.parse(path)
+ target = tree.getroot().find("Target")
+ target.remove(target.find("Exec"))
+ revision = ET.Element("GitCommit", Condition="'$(GitCommit)' == ''")
+ revision.text = "$([System.String]::Copy('$(SourceRevisionId)').Substring(0, 7))"
+ target.find("PropertyGroup").insert(0, revision)
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_git_stamp_orchestrator_is_evaluated_by_msbuild(self):
+ for forwards_revision in (True, False):
+ with self.subTest(forwards_revision=forwards_revision):
+ work, script = self.workspace("invariant-per-project-exec")
+ base = work / "git-stamp"
+ (base / "build.sh").write_text(
+ "#!/usr/bin/env bash\nset -euo pipefail\n"
+ "dotnet msbuild Build.proj -p:Configuration=Release -m\n",
+ encoding="utf-8",
+ )
+ project = ET.Element("Project", DefaultTargets="BuildAll")
+ target = ET.SubElement(project, "Target", Name="BuildAll")
+ lookup = ET.SubElement(
+ target, "Exec", Command="git rev-parse --short HEAD", ConsoleToMSBuild="true",
+ )
+ ET.SubElement(lookup, "Output", TaskParameter="ConsoleOutput", PropertyName="Revision")
+ ET.SubElement(
+ target, "Exec", Command="dotnet restore Contoso.slnx -p:Configuration=Release",
+ )
+ properties = "Configuration=Release"
+ if forwards_revision:
+ properties += ";GitCommit=$(Revision)"
+ ET.SubElement(
+ target, "MSBuild", Projects="Contoso.slnx", Targets="Build",
+ BuildInParallel="true", Properties=properties,
+ )
+ ET.ElementTree(project).write(base / "Build.proj", encoding="utf-8")
+ result = self.grade(work, script, accepted=forwards_revision)
+ if not forwards_revision:
+ self.assertIn("the fallback must run once", result.stderr)
+
+ def test_missing_informational_version_attribute_is_rejected(self):
+ work, script = self.workspace("invariant-per-project-exec")
+ path = work / "git-stamp" / "Directory.Build.props"
+ tree = ET.parse(path)
+ ET.SubElement(
+ tree.getroot().find("PropertyGroup"), "GenerateAssemblyInformationalVersionAttribute",
+ ).text = "false"
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("compiled informational-version attribute", result.stderr)
+
+ def test_git_stamp_cannot_disable_parallel_builds(self):
+ work, script = self.workspace("invariant-per-project-exec")
+ path = work / "git-stamp" / "build.sh"
+ path.write_text(
+ path.read_text(encoding="utf-8").replace(
+ "dotnet build Contoso.slnx", "dotnet build Contoso.slnx -p:BuildInParallel=false",
+ ), encoding="utf-8",
+ )
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("parallel execution", result.stderr)
+
+ def test_git_fallback_requires_the_property_to_be_absent(self):
+ for condition in ("'true' == 'true'", "'false' == 'true'"):
+ with self.subTest(condition=condition):
+ work, script = self.workspace("invariant-per-project-exec")
+ path = work / "git-stamp" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target/Exec").set("Condition", condition)
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn(
+ "the fallback must run once" if condition == "'true' == 'true'"
+ else "the commit and version must be ready",
+ result.stderr,
+ )
+
+ def test_late_git_stamp_assignment_is_rejected(self):
+ work, script = self.workspace("invariant-per-project-exec")
+ path = work / "git-stamp" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ target = tree.getroot().find("Target")
+ target.attrib.pop("BeforeTargets")
+ target.set("AfterTargets", "GetAssemblyAttributes")
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("the commit and version must be ready", result.stderr)
+
+ def test_git_stamp_preserves_effective_build_arguments(self):
+ variants = (
+ (False, "", "'$(Configuration)' == 'Debug'"),
+ (False, "", "'$(Configuration)' == 'Release'"),
+ (False, "-p:StampMode=ci", "'$(StampMode)' != 'ci'"),
+ (True, "-p:StampMode=ci", "'$(Configuration)' == 'Debug' or '$(StampMode)' == 'ci'"),
+ )
+ for accepted, argument, condition in variants:
+ with self.subTest(argument=argument, condition=condition):
+ work, script = self.workspace("invariant-per-project-exec")
+ base = work / "git-stamp"
+ if argument:
+ wrapper = base / "build.sh"
+ wrapper.write_text(
+ wrapper.read_text(encoding="utf-8").replace(
+ "dotnet build Contoso.slnx", f"dotnet build Contoso.slnx {argument}",
+ ), encoding="utf-8",
+ )
+ path = base / "Directory.Build.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target").set("Condition", condition)
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=accepted)
+ if not accepted:
+ self.assertIn("the commit and version must be ready", result.stderr)
+
+ def test_conditional_reference_updates_are_evaluated_by_msbuild(self):
+ work, script = self.workspace("global-property-instances")
+ base = work / "shared-project-instances"
+ for component in ("Web", "Api", "Worker"):
+ path = base / component / f"{component}.csproj"
+ tree = ET.parse(path)
+ group = tree.getroot().find("ItemGroup")
+ group.set("Condition", "'$(Configuration)' == 'Debug'")
+ reference = group.find("ProjectReference")
+ reference.attrib.pop("GlobalPropertiesToRemove")
+ reference.set("Condition", "Exists('../Core/Core.csproj')")
+ tree.write(path, encoding="utf-8")
+ (base / "Directory.Build.targets").write_text(
+ """
+
+
+ ComponentName
+
+
+
+""", encoding="utf-8",
+ )
+ self.grade(work, script)
+
+ def test_false_reference_removal_condition_is_rejected(self):
+ work, script = self.workspace("global-property-instances")
+ for path in (work / "shared-project-instances").glob("*/*.csproj"):
+ tree = ET.parse(path)
+ for reference in tree.getroot().iter("ProjectReference"):
+ reference.attrib.pop("GlobalPropertiesToRemove")
+ ET.SubElement(
+ reference, "GlobalPropertiesToRemove",
+ Condition="'$(Configuration)' == 'Release'",
+ ).text = "ComponentName"
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("build Core exactly once", result.stderr)
+
+ def test_missing_compiler_reference_is_rejected(self):
+ work, script = self.workspace("global-property-instances")
+ path = work / "shared-project-instances" / "Api" / "Api.csproj"
+ tree = ET.parse(path)
+ tree.getroot().find("ItemGroup/ProjectReference").set("ReferenceOutputAssembly", "false")
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("actual compilation reference", result.stderr)
+
+ def test_missing_component_title_attributes_are_rejected(self):
+ for component, flag in (
+ ("Web", "GenerateAssemblyTitleAttribute"),
+ ("Api", "GenerateAssemblyInfo"),
+ ):
+ with self.subTest(component=component, flag=flag):
+ work, script = self.workspace("global-property-instances")
+ path = work / "shared-project-instances" / component / f"{component}.csproj"
+ tree = ET.parse(path)
+ ET.SubElement(tree.getroot().find("PropertyGroup"), flag).text = "false"
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("compiled component AssemblyTitle", result.stderr)
+
+ def test_csharp_only_compile_include_is_accepted(self):
+ work, script = self.workspace("racy-run-once-guard")
+ path = work / "exists-sentinel-race" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("ItemGroup").set(
+ "Condition", "'$(MSBuildProjectExtension)' == '.csproj'",
+ )
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_wrong_project_extension_compile_include_is_rejected(self):
+ work, script = self.workspace("racy-run-once-guard")
+ path = work / "exists-sentinel-race" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("ItemGroup").set(
+ "Condition", "'$(MSBuildProjectExtension)' == '.vbproj'",
+ )
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("effective Compile items", result.stderr)
+
+ def test_shared_generation_orchestrator_is_accepted(self):
+ for task_dispatch in (False, True):
+ with self.subTest(msbuild_task=task_dispatch):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ wrapper = base / "build.sh"
+ lines = wrapper.read_text(encoding="utf-8").splitlines()
+ dispatch = lines.pop()
+ self.assertTrue(dispatch.startswith("dotnet build Contoso.slnx"))
+ (base / "prepare.sh").write_text("\n".join(lines) + "\n", encoding="utf-8")
+ wrapper.write_text(
+ "#!/usr/bin/env bash\nset -euo pipefail\n"
+ "dotnet msbuild Build.proj -p:Configuration=Release -m\n",
+ encoding="utf-8",
+ )
+ project = ET.Element("Project", DefaultTargets="BuildAll")
+ target = ET.SubElement(project, "Target", Name="BuildAll")
+ ET.SubElement(target, "Exec", Command="bash prepare.sh")
+ if task_dispatch:
+ ET.SubElement(target, "Exec", Command="dotnet restore Contoso.slnx -p:Configuration=Release")
+ ET.SubElement(
+ target, "MSBuild", Projects="Contoso.slnx", Targets="Build",
+ BuildInParallel="true", Properties="Configuration=Release",
+ )
+ else:
+ ET.SubElement(target, "Exec", Command=dispatch)
+ ET.ElementTree(project).write(base / "Build.proj", encoding="utf-8")
+ self.grade(work, script)
+
+ def test_executable_shared_generator_keeps_its_entry_point(self):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ generator = base / "tools" / "make-buildinfo.sh"
+ generator.write_text(
+ generator.read_text(encoding="utf-8").replace(
+ "set -eu\n", 'set -eu\n[ "${0##*/}" = "make-buildinfo.sh" ]\n',
+ ), encoding="utf-8",
+ )
+ generator.chmod(0o755)
+ wrapper = base / "build.sh"
+ wrapper.write_text(
+ wrapper.read_text(encoding="utf-8").replace(
+ "sh tools/make-buildinfo.sh", "./tools/make-buildinfo.sh",
+ ), encoding="utf-8",
+ )
+ self.grade(work, script)
+
+ def test_relocated_shared_generators_are_accepted(self):
+ for variant in ("output-argument", "stdout", "delegating-helper"):
+ with self.subTest(variant=variant):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ directory = base / "build-tools"
+ directory.mkdir()
+ generator = directory / "generate-info.sh"
+ (base / "tools" / "make-buildinfo.sh").rename(generator)
+ command = "sh build-tools/generate-info.sh"
+ if variant == "stdout":
+ generator.write_text(
+ generator.read_text(encoding="utf-8").replace('out="$1"', "out=/dev/stdout"),
+ encoding="utf-8",
+ )
+ command += " >"
+ elif variant == "delegating-helper":
+ (directory / "prepare-info.sh").write_text(
+ '#!/usr/bin/env sh\nset -eu\nsh "$(dirname "$0")/generate-info.sh" "$@"\n',
+ encoding="utf-8",
+ )
+ command = "sh build-tools/prepare-info.sh"
+ wrapper = base / "build.sh"
+ wrapper.write_text(
+ wrapper.read_text(encoding="utf-8").replace("sh tools/make-buildinfo.sh", command),
+ encoding="utf-8",
+ )
+ self.grade(work, script)
+
+ def test_inlined_shared_generation_is_accepted(self):
+ for keep_unused_helper in (False, True):
+ with self.subTest(keep_unused_helper=keep_unused_helper):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ generator = base / "tools" / "make-buildinfo.sh"
+ body = generator.read_text(encoding="utf-8")
+ if not keep_unused_helper:
+ generator.unlink()
+ wrapper = base / "build.sh"
+ wrapper.write_text(
+ wrapper.read_text(encoding="utf-8").replace(
+ 'sh tools/make-buildinfo.sh "$generated"',
+ '(\nset -- "$generated"\n' + body + '\n)',
+ ), encoding="utf-8",
+ )
+ self.grade(work, script)
+
+ def test_inlined_shared_generation_orchestrator_is_accepted(self):
+ for clock in ("shell", "managed"):
+ with self.subTest(clock=clock):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ (base / "tools" / "make-buildinfo.sh").unlink()
+ (base / "build.sh").write_text(
+ "#!/usr/bin/env bash\nset -euo pipefail\n"
+ "dotnet msbuild Build.proj -p:Configuration=Release -m\n",
+ encoding="utf-8",
+ )
+ project = ET.Element("Project", DefaultTargets="BuildAll")
+ properties = ET.SubElement(project, "PropertyGroup")
+ ET.SubElement(properties, "DailyStamp").text = "$(BUILDINFO_STAMP)"
+ task = ET.SubElement(
+ project, "UsingTask", TaskName="WriteSharedBuildInfo", TaskFactory="RoslynCodeTaskFactory",
+ AssemblyFile="$(MSBuildToolsPath)/Microsoft.Build.Tasks.Core.dll",
+ )
+ parameters = ET.SubElement(task, "ParameterGroup")
+ ET.SubElement(parameters, "Stamp", ParameterType="System.String")
+ implementation = ET.SubElement(task, "Task")
+ for namespace in ("System.IO", "System.Text"):
+ ET.SubElement(implementation, "Using", Namespace=namespace)
+ value = "Stamp" if clock == "shell" else (
+ '(!System.String.IsNullOrEmpty(Stamp) ? Stamp : '
+ 'System.DateTime.UtcNow.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture))'
+ )
+ ET.SubElement(implementation, "Code", Type="Fragment", Language="cs").text = (
+ 'var content = "internal static class BuildInfo { public const string Stamp = \\"" '
+ '+ ' + value + ' + "\\"; }\\n";\n'
+ 'Directory.CreateDirectory("obj");\n'
+ 'var output = "obj/buildinfo.g.cs";\n'
+ 'if (!File.Exists(output) || File.ReadAllText(output) != content)\n'
+ ' File.WriteAllText(output, content, new UTF8Encoding(false));\n'
+ )
+ target = ET.SubElement(project, "Target", Name="BuildAll")
+ if clock == "shell":
+ command = 'bash -c "date -u +%Y%m%d"'
+ if os.name == "nt":
+ command = command.replace("%", "%%")
+ lookup = ET.SubElement(
+ target, "Exec", Command=command, ConsoleToMSBuild="true",
+ Condition="'$(DailyStamp)' == ''",
+ )
+ ET.SubElement(lookup, "Output", TaskParameter="ConsoleOutput", PropertyName="DailyStamp")
+ ET.SubElement(target, "WriteSharedBuildInfo", Stamp="$(DailyStamp)")
+ ET.SubElement(target, "Exec", Command="dotnet build Contoso.slnx -c Release -m")
+ ET.ElementTree(project).write(base / "Build.proj", encoding="utf-8")
+ self.grade(work, script)
+
+ def test_shared_generation_preserves_dependency_timestamps(self):
+ for tracks_stamp in (True, False):
+ with self.subTest(cache_tracks_stamp=tracks_stamp):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ os.utime(base / "tools" / "make-buildinfo.sh", None)
+ (base / "build.sh").write_text(
+ "#!/usr/bin/env bash\nset -euo pipefail\n"
+ "dotnet msbuild Build.proj -p:Configuration=Release -m\n",
+ encoding="utf-8",
+ )
+ project = ET.Element("Project", DefaultTargets="BuildAll")
+ output = "obj/buildinfo.g.cs"
+ if tracks_stamp:
+ properties = ET.SubElement(project, "PropertyGroup")
+ ET.SubElement(properties, "EffectiveStamp").text = "$(BUILDINFO_STAMP)"
+ ET.SubElement(properties, "EffectiveStamp", Condition="'$(EffectiveStamp)' == ''").text = (
+ "$([System.DateTime]::UtcNow.ToString('yyyyMMdd'))"
+ )
+ output = "obj/buildinfo.$(EffectiveStamp).stamp"
+ generate = ET.SubElement(
+ project, "Target", Name="Generate", BeforeTargets="BuildAll",
+ Inputs="tools/make-buildinfo.sh", Outputs=output,
+ )
+ ET.SubElement(generate, "Exec", Command="sh tools/make-buildinfo.sh obj/buildinfo.g.cs")
+ if tracks_stamp:
+ items = ET.SubElement(generate, "ItemGroup")
+ ET.SubElement(items, "PreviousStamp", Include="obj/buildinfo.*.stamp")
+ ET.SubElement(generate, "Delete", Files="@(PreviousStamp)")
+ ET.SubElement(generate, "Touch", Files=output, AlwaysCreate="true")
+ target = ET.SubElement(project, "Target", Name="BuildAll")
+ ET.SubElement(target, "Exec", Command="dotnet build Contoso.slnx -c Release -m")
+ ET.ElementTree(project).write(base / "Build.proj", encoding="utf-8")
+ result = self.grade(work, script, accepted=tracks_stamp)
+ if not tracks_stamp:
+ self.assertIn("preserve shared source bytes", result.stdout + result.stderr)
+
+ def test_shared_generation_retries_utc_default_rollover(self):
+ for boundary, offsets in (
+ ("during-build", (-1, -1)),
+ ("between-builds", (0, 0, 0, 1)),
+ ("unstable-clock", (0, 1, 0, 1)),
+ ):
+ with self.subTest(boundary=boundary):
+ work, script = self.workspace("racy-run-once-guard")
+ script = (
+ "import datetime\n"
+ "_real_datetime = datetime.datetime\n"
+ "_utc_day_offsets = iter(" + repr(offsets) + ")\n"
+ "class _RolloverClock(_real_datetime):\n"
+ " @classmethod\n"
+ " def now(cls, tz=None):\n"
+ " return _real_datetime.now(tz) + datetime.timedelta(days=next(_utc_day_offsets, 0))\n"
+ "datetime.datetime = _RolloverClock\n"
+ ) + script
+ result = self.grade(work, script, accepted=boundary != "unstable-clock")
+ if boundary == "unstable-clock":
+ self.assertIn("UTC date changed during both attempts", result.stderr)
+
+ def test_shared_generator_keeps_stamp_override_and_utc_default(self):
+ work, _ = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ output = base / "obj" / "fixture-buildinfo.g.cs"
+ for stamp in (None, "", "20400229"):
+ with self.subTest(stamp=stamp):
+ env = dict(os.environ)
+ env.pop("BUILDINFO_STAMP", None)
+ if stamp is not None:
+ env["BUILDINFO_STAMP"] = stamp
+ before = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d")
+ result = subprocess.run(
+ [shutil.which("bash"), "tools/make-buildinfo.sh", "obj/fixture-buildinfo.g.cs"],
+ cwd=base, env=env, capture_output=True, text=True, timeout=30,
+ )
+ self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
+ after = datetime.datetime.now(datetime.timezone.utc).strftime("%Y%m%d")
+ values = {stamp} if stamp else {before, after}
+ self.assertIn(output.read_bytes(), {
+ ('internal static class BuildInfo { public const string Stamp = "' +
+ value + '"; }\n').encode("utf-8") for value in values
+ })
+
+ def test_shared_generation_requires_utc_defaults(self):
+ for fallback in (
+ "${BUILDINFO_STAMP:-}",
+ "${BUILDINFO_STAMP:-19990101}",
+ "${BUILDINFO_STAMP-$stamp}",
+ ):
+ with self.subTest(fallback=fallback):
+ work, script = self.workspace("racy-run-once-guard")
+ generator = work / "exists-sentinel-race" / "tools" / "make-buildinfo.sh"
+ generator.write_text(
+ generator.read_text(encoding="utf-8").replace("${BUILDINFO_STAMP:-$stamp}", fallback),
+ encoding="utf-8",
+ )
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("preserve shared source bytes", result.stdout + result.stderr)
+
+ def test_inlined_shared_generation_cannot_rewrite_each_project_input(self):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ generator = base / "tools" / "make-buildinfo.sh"
+ body = generator.read_text(encoding="utf-8")
+ generator.unlink()
+ wrapper = base / "build.sh"
+ wrapper.write_text(
+ wrapper.read_text(encoding="utf-8").replace(
+ 'sh tools/make-buildinfo.sh "$generated"',
+ '(\nset -- "$generated"\n' + body + '\n)',
+ ), encoding="utf-8",
+ )
+ path = base / "Directory.Build.targets"
+ tree = ET.parse(path)
+ target = ET.SubElement(tree.getroot(), "Target", Name="RewriteBuildInfo", BeforeTargets="CoreCompile")
+ ET.SubElement(
+ target, "WriteLinesToFile", File="$(MSBuildThisFileDirectory)obj/buildinfo.g.cs",
+ Lines="@(Compile)", Overwrite="true",
+ )
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertNotIn("FileNotFoundError", result.stderr)
+ self.assertRegex(result.stdout + result.stderr, "error CS|preserve shared source bytes")
+
+ def test_unfixed_shared_writer_race_is_rejected(self):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ for filename in ("build.sh", "Directory.Build.targets"):
+ shutil.copy2(SUITE / "exists-sentinel-race" / filename, base / filename)
+ result = self.grade(work, script, accepted=False)
+ self.assertRegex(result.stdout + result.stderr, "error CS|preserve shared source bytes")
+
+ def test_relocated_per_project_writers_are_rejected(self):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ (base / "tools" / "make-buildinfo.sh").rename(base / "tools" / "generate-info.sh")
+ wrapper = base / "build.sh"
+ wrapper.write_text(
+ wrapper.read_text(encoding="utf-8").replace("tools/make-buildinfo.sh", "tools/generate-info.sh"),
+ encoding="utf-8",
+ )
+ path = base / "Directory.Build.targets"
+ tree = ET.parse(path)
+ target = ET.SubElement(tree.getroot(), "Target", Name="RepeatedGeneration", BeforeTargets="CoreCompile")
+ ET.SubElement(
+ target, "Exec", Command="sh tools/generate-info.sh obj/buildinfo.g.cs",
+ WorkingDirectory="$(MSBuildThisFileDirectory)",
+ )
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertNotIn("FileNotFoundError", result.stderr)
+ self.assertRegex(
+ result.stdout + result.stderr,
+ "unchanged|preserve shared source bytes|publish the final source timestamp|error CS1010",
+ )
+
+ def test_shared_fixture_reproduces_reported_compiler_failure(self):
+ work, _ = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ mock_bin = base / "mock-bin"
+ mock_bin.mkdir()
+ date = mock_bin / "date"
+ # Hold both writers at the date lookup to reproduce interleaving.
+ date.write_text(
+ "#!/usr/bin/env sh\nset -eu\n"
+ 'printf "ready\\n" >> "$RACE_READY"\n'
+ "attempt=0\n"
+ 'while [ "$(wc -l < "$RACE_READY")" -lt 2 ]; do\n'
+ " attempt=$((attempt + 1))\n"
+ ' [ "$attempt" -le 200 ] || exit 1\n'
+ " sleep 0.01\n"
+ "done\n"
+ "printf '20261030\\n'\n",
+ encoding="utf-8", newline="\n",
+ )
+ date.chmod(0o755)
+ writers = subprocess.run(
+ [shutil.which("bash"), "-c",
+ 'set -eu; export PATH="$(pwd)/mock-bin:$PATH"; export RACE_READY="$(pwd)/ready"; '
+ 'sh tools/make-buildinfo.sh obj/buildinfo.g.cs & first=$!; '
+ 'sh tools/make-buildinfo.sh obj/buildinfo.g.cs & second=$!; '
+ 'wait "$first"; wait "$second"'],
+ cwd=base, capture_output=True, text=True, timeout=30,
+ )
+ self.assertEqual(writers.returncode, 0, writers.stdout + writers.stderr)
+ project = ET.Element("Project", Sdk="Microsoft.NET.Sdk")
+ ET.SubElement(ET.SubElement(project, "PropertyGroup"), "TargetFramework").text = "net10.0"
+ ET.ElementTree(project).write(base / "Race.csproj", encoding="utf-8")
+ compiled = subprocess.run(
+ [shutil.which("dotnet"), "msbuild", "Race.csproj", "-restore", "-target:Build",
+ "-nologo", "-verbosity:quiet", "-nodeReuse:false"],
+ cwd=base, capture_output=True, text=True, timeout=60,
+ )
+ self.assertNotEqual(compiled.returncode, 0, "concurrent fixture writers did not corrupt the source")
+ self.assertIn("error CS1010", compiled.stdout + compiled.stderr)
+
+ def test_cached_shared_generation_refreshes_changed_inputs(self):
+ for refresh in (True, False):
+ with self.subTest(refresh_changed_inputs=refresh):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ wrapper = base / "build.sh"
+ lines = wrapper.read_text(encoding="utf-8").splitlines()
+ dispatch = lines.pop()
+ (base / "prepare.sh").write_text("\n".join(lines) + "\n", encoding="utf-8")
+ condition = "[ ! -f obj/buildinfo.g.cs ]"
+ if refresh:
+ condition += ' || [ ! -f obj/buildinfo.stamp ] || [ "$(cat obj/buildinfo.stamp)" != "$stamp" ]'
+ wrapper.write_text(
+ "#!/usr/bin/env bash\nset -euo pipefail\n"
+ 'stamp="${BUILDINFO_STAMP:-$(date -u +%Y%m%d)}"\n'
+ f"if {condition}; then\n"
+ " bash prepare.sh\n"
+ " printf '%s\\n' \"$stamp\" > obj/buildinfo.stamp\n"
+ "fi\n" + dispatch + "\n",
+ encoding="utf-8",
+ )
+ result = self.grade(work, script, accepted=refresh)
+ if not refresh:
+ self.assertIn("preserve shared source bytes", result.stdout + result.stderr)
+
+ def test_shared_generation_rejects_day_blind_cache(self):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ (base / "build.sh").write_text(
+ "#!/usr/bin/env bash\nset -euo pipefail\n"
+ "if [ ! -f obj/buildinfo.cached ]; then\n"
+ " sh tools/make-buildinfo.sh obj/buildinfo.g.cs\n"
+ " touch obj/buildinfo.cached\n"
+ "fi\n"
+ "dotnet build Contoso.slnx -c Release -m\n",
+ encoding="utf-8",
+ )
+ self.grade(work, script, accepted=False)
+
+ def test_late_shared_generation_is_rejected(self):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ wrapper = base / "build.sh"
+ lines = wrapper.read_text(encoding="utf-8").splitlines()
+ dispatch = lines.pop()
+ (base / "prepare.sh").write_text("\n".join(lines) + "\n", encoding="utf-8")
+ wrapper.write_text("#!/usr/bin/env bash\nset -euo pipefail\n" + dispatch + "\n", encoding="utf-8")
+ path = base / "Directory.Build.targets"
+ tree = ET.parse(path)
+ target = ET.SubElement(tree.getroot(), "Target", Name="LateGeneration", AfterTargets="CoreCompile")
+ ET.SubElement(
+ target, "Exec", Command='bash "$(MSBuildThisFileDirectory)prepare.sh"',
+ WorkingDirectory="$(MSBuildThisFileDirectory)",
+ )
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script, accepted=False)
+
+ def test_repeated_shared_generation_is_rejected(self):
+ work, script = self.workspace("racy-run-once-guard")
+ path = work / "exists-sentinel-race" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ target = ET.SubElement(tree.getroot(), "Target", Name="RepeatedGeneration", BeforeTargets="CoreCompile")
+ ET.SubElement(
+ target, "Exec", Command="sh tools/make-buildinfo.sh obj/buildinfo.g.cs",
+ WorkingDirectory="$(MSBuildThisFileDirectory)",
+ )
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script, accepted=False)
+
+ def test_global_property_keeps_shared_fallback_dormant(self):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ wrapper = base / "build.sh"
+ wrapper.write_text(
+ wrapper.read_text(encoding="utf-8").replace(
+ "dotnet build Contoso.slnx", "dotnet build Contoso.slnx -p:SharedInfoReady=true",
+ ), encoding="utf-8",
+ )
+ path = base / "Directory.Build.targets"
+ tree = ET.parse(path)
+ target = ET.SubElement(
+ tree.getroot(), "Target", Name="GuardedGeneration", BeforeTargets="CoreCompile",
+ Condition="'$(SharedInfoReady)' != 'true'",
+ )
+ ET.SubElement(
+ target, "Exec", Command="sh tools/make-buildinfo.sh obj/buildinfo.g.cs",
+ WorkingDirectory="$(MSBuildThisFileDirectory)",
+ )
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_forced_recompilation_on_unchanged_builds_is_rejected(self):
+ work, script = self.workspace("racy-run-once-guard")
+ path = work / "exists-sentinel-race" / "Directory.Build.targets"
+ tree = ET.parse(path)
+ target = ET.SubElement(
+ tree.getroot(), "Target", Name="ForceRecompile", BeforeTargets="CoreCompile",
+ )
+ ET.SubElement(target, "Delete", Files="@(IntermediateAssembly)")
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("unchanged", result.stderr)
+
+ def test_later_build_cannot_hide_unchanged_recompilation(self):
+ work, script = self.workspace("racy-run-once-guard")
+ base = work / "exists-sentinel-race"
+ wrapper = base / "build.sh"
+ lines = wrapper.read_text(encoding="utf-8").splitlines()
+ dispatch = lines.pop()
+ wrapper.write_text(
+ "\n".join(lines) + "\n"
+ 'stamp="${BUILDINFO_STAMP:-$(date -u +%Y%m%d)}"\n'
+ 'if [ -f obj/compiled-stamp ] && [ "$(cat obj/compiled-stamp)" = "$stamp" ]; then\n'
+ f" {dispatch} -p:ForceProbeCompile=true\n"
+ "fi\n"
+ f"{dispatch}\n"
+ 'printf "%s\\n" "$stamp" > obj/compiled-stamp\n',
+ encoding="utf-8",
+ )
+ path = base / "Directory.Build.targets"
+ tree = ET.parse(path)
+ target = ET.SubElement(
+ tree.getroot(), "Target", Name="ForceRecompile", BeforeTargets="CoreCompile",
+ Condition="'$(ForceProbeCompile)' == 'true'",
+ )
+ ET.SubElement(target, "Delete", Files="@(IntermediateAssembly)")
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("unchanged", result.stderr)
+
+ def test_additional_schema_sources_are_rejected(self):
+ variants = (
+ ("gen/Unexpected.cs", False, False),
+ ("gen/nested/Unexpected.g.cs", True, False),
+ ("extra/Unexpected.cs", True, False),
+ ("extra/Unexpected.cs", True, True),
+ )
+ for relative, compile_source, cleanup in variants:
+ with self.subTest(path=relative, compiled=compile_source, cleanup=cleanup):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ source = "$(IntermediateOutputPath)" + relative
+ target = ET.SubElement(
+ tree.getroot(), "Target", Name="AddUnexpectedSource", BeforeTargets="CoreCompile",
+ )
+ ET.SubElement(target, "MakeDir", Directories=source.rsplit("/", 1)[0])
+ ET.SubElement(
+ target, "WriteLinesToFile", File=source,
+ Lines="public sealed class UnexpectedPublicType {}", Overwrite="true",
+ )
+ if compile_source:
+ ET.SubElement(ET.SubElement(target, "ItemGroup"), "Compile", Include=source)
+ if cleanup:
+ after = ET.SubElement(
+ tree.getroot(), "Target", Name="RemoveUnexpectedSource", AfterTargets="CoreCompile",
+ )
+ ET.SubElement(after, "Delete", Files=source)
+ ET.SubElement(ET.SubElement(after, "ItemGroup"), "Compile", Remove=source)
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn(
+ "preserve exactly the generated C# output set"
+ if relative.startswith("gen/") else "do not introduce additional C# compiler inputs",
+ result.stderr,
+ )
+
+ def test_submitted_generated_schema_sources_are_not_hidden(self):
+ for relative in ("Unexpected.g.cs", "nested/Unexpected.g.cs"):
+ with self.subTest(path=relative):
+ work, script = self.workspace("per-item-exec")
+ source = work / "per-file-exec" / relative
+ source.parent.mkdir(parents=True, exist_ok=True)
+ source.write_text("public sealed class UnexpectedPublicType {}\n", encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("do not introduce additional C# compiler inputs", result.stderr)
+
+ def test_uncompiled_schema_helper_is_accepted(self):
+ for name in ("BatchHelper.cs", "BatchHelper.g.cs"):
+ with self.subTest(helper=name):
+ work, script = self.workspace("per-item-exec")
+ base = work / "per-file-exec"
+ helper = "tools/" + name
+ (base / helper).write_text("public sealed class BatchHelper {}\n", encoding="utf-8")
+ path = base / "Contoso.Schemas.csproj"
+ tree = ET.parse(path)
+ ET.SubElement(ET.SubElement(tree.getroot(), "ItemGroup"), "Compile", Remove=helper)
+ tree.write(path, encoding="utf-8")
+ path = base / "Schemas.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target").insert(0, ET.Element(
+ "Error", Condition=f"!Exists('{helper}')", Text="Preserve the submitted build helper",
+ ))
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_sdk_generated_schema_compile_inputs_are_accepted(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Contoso.Schemas.csproj"
+ tree = ET.parse(path)
+ ET.SubElement(tree.getroot().find("PropertyGroup"), "ImplicitUsings").text = "enable"
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_schema_output_can_share_sdk_intermediates(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ path.write_text(
+ path.read_text(encoding="utf-8").replace("$(IntermediateOutputPath)gen", "$(IntermediateOutputPath)."),
+ encoding="utf-8",
+ )
+ self.grade(work, script)
+
+ def test_cross_schema_copy_and_swap_are_rejected(self):
+ for swap in (False, True):
+ with self.subTest(swap=swap):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ target = tree.getroot().find("Target")
+ customer = "$(IntermediateOutputPath)gen/Customer.g.cs"
+ order = "$(IntermediateOutputPath)gen/Order.g.cs"
+ if swap:
+ saved = "$(IntermediateOutputPath)gen/saved.txt"
+ ET.SubElement(target, "Copy", SourceFiles=customer, DestinationFiles=saved)
+ ET.SubElement(target, "Copy", SourceFiles=order, DestinationFiles=customer)
+ ET.SubElement(target, "Copy", SourceFiles=saved, DestinationFiles=order)
+ else:
+ ET.SubElement(target, "Copy", SourceFiles=customer, DestinationFiles=order)
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("distinct content-dependent schema output", result.stderr)
+
+ def test_corruption_of_each_schema_is_rejected(self):
+ for name in ("Customer", "Invoice", "Order"):
+ with self.subTest(schema=name):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ ET.SubElement(
+ tree.getroot().find("Target"), "WriteLinesToFile",
+ File=f"$(IntermediateOutputPath)gen/{name}.g.cs",
+ Lines="// corrupt", Overwrite="true",
+ )
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script, accepted=False)
+
+ def test_schema_expectations_precede_submitted_target_execution(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target").insert(0, ET.Element(
+ "WriteLinesToFile", File="schemas/Order.json", Lines="{}", Overwrite="true",
+ ))
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("must not modify, add or delete schema inputs", result.stderr)
+
+ def test_enabled_schema_exec_condition_is_accepted(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target/Exec").set("Condition", "Exists('schemas/Customer.json')")
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_disabled_schema_exec_condition_is_rejected(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target/Exec").set("Condition", "Exists('schemas/Missing.json')")
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("clean: schema generation must execute exactly once", result.stderr)
+
+ def test_equivalent_schema_item_name_is_accepted(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ path.write_text(
+ path.read_text(encoding="utf-8").replace("SchemaFile", "SchemaInput"),
+ encoding="utf-8",
+ )
+ self.grade(work, script)
+
+ def test_added_schema_input_is_rejected(self):
+ work, script = self.workspace("per-item-exec")
+ (work / "per-file-exec" / "schemas" / "Unexpected.json").write_text(
+ '{"title": "Unexpected"}\n', encoding="utf-8",
+ )
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("preserve exactly the original fixture schema inputs", result.stderr)
+
+ def test_imported_schema_implementation_preserves_original_files(self):
+ for variant in ("preserved", "modified-evidence", "deleted-evidence"):
+ with self.subTest(variant=variant):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ path.with_name("BatchSchemas.targets").write_bytes(path.read_bytes())
+ path.write_text('', encoding="utf-8")
+ if variant == "modified-evidence":
+ evidence = path.parent / "perf-summary.txt"
+ evidence.write_bytes(evidence.read_bytes() + b"\nChanged evidence\n")
+ elif variant == "deleted-evidence":
+ (path.parent / "tools" / "schemagen.md").unlink()
+ result = self.grade(work, script, accepted=variant == "preserved")
+ if variant != "preserved":
+ self.assertIn("preserve all original schema contents and evidence files", result.stderr)
+
+ def test_property_backed_schema_generator_is_accepted(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ root = tree.getroot()
+ properties = ET.Element("PropertyGroup")
+ ET.SubElement(properties, "GeneratorExe").text = "schemagen"
+ root.insert(0, properties)
+ task = root.find("Target/Exec")
+ task.set("Command", task.get("Command").replace("schemagen", "$(GeneratorExe)"))
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_anchored_schema_output_directory_is_accepted(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ task = tree.getroot().find("Target/Exec")
+ task.set(
+ "Command",
+ task.get("Command").replace(
+ '--output-dir "$(IntermediateOutputPath)gen"',
+ '--output-dir "$(MSBuildProjectDirectory)/$(IntermediateOutputPath)gen"',
+ ),
+ )
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_nonincremental_schema_targets_are_rejected(self):
+ for variant in ("no-tracking", "missing-output"):
+ with self.subTest(variant=variant):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ target = tree.getroot().find("Target")
+ if variant == "no-tracking":
+ target.attrib.pop("Inputs")
+ target.attrib.pop("Outputs")
+ else:
+ target.set("Outputs", "$(IntermediateOutputPath)always-missing.output")
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("unchanged: schema generation", result.stderr)
+
+ def test_schema_compile_hooks_are_evaluated_by_msbuild(self):
+ for hook in ("property-before", "early-sdk", "property-dependency"):
+ with self.subTest(hook=hook):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ root = tree.getroot()
+ target = root.find("Target")
+ if hook == "property-before":
+ properties = ET.Element("PropertyGroup")
+ ET.SubElement(properties, "SchemaCompileHook").text = "CoreCompile"
+ root.insert(0, properties)
+ target.set("BeforeTargets", "$(SchemaCompileHook)")
+ elif hook == "early-sdk":
+ target.set("BeforeTargets", "PrepareForBuild")
+ else:
+ target.attrib.pop("BeforeTargets")
+ properties = ET.Element("PropertyGroup")
+ ET.SubElement(properties, "SchemaCompileDependencies").text = target.get("Name")
+ root.insert(0, properties)
+ ET.SubElement(
+ root, "Target", Name="PrepareSchemas", BeforeTargets="CoreCompile",
+ DependsOnTargets="$(SchemaCompileDependencies)",
+ )
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_unreachable_and_late_schema_generation_are_rejected(self):
+ for hook in ("unreachable", "after-compilation"):
+ with self.subTest(hook=hook):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ target = tree.getroot().find("Target")
+ if hook == "unreachable":
+ target.set("BeforeTargets", "_MissingCompileHook")
+ else:
+ target.attrib.pop("BeforeTargets")
+ target.set("AfterTargets", "CoreCompile")
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("clean:", result.stderr)
+
+ def test_anchored_schema_import_is_accepted(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Contoso.Schemas.csproj"
+ tree = ET.parse(path)
+ tree.getroot().find("Import").set("Project", "$(MSBuildThisFileDirectory)Schemas.targets")
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_schema_conditions_are_evaluated_by_msbuild(self):
+ variants = (
+ ("import", "Import", "Exists('Schemas.targets')", "Exists('Missing.targets')"),
+ ("target", "Target", "'$(TargetFramework)' == 'net10.0'", "'$(TargetFramework)' == 'net9.0'"),
+ ("writer", "Target/WriteLinesToFile", "'@(SchemaFile)' != ''", "'@(SchemaFile)' == ''"),
+ ("directory", "Target/MakeDir", "!Exists('$(IntermediateOutputPath)gen')", "'false' == 'true'"),
+ )
+ for kind, selector, enabled, disabled in variants:
+ for accepted, condition in ((True, enabled), (False, disabled)):
+ with self.subTest(operation=kind, accepted=accepted):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / (
+ "Contoso.Schemas.csproj" if kind == "import" else "Schemas.targets"
+ )
+ tree = ET.parse(path)
+ tree.getroot().find(selector).set("Condition", condition)
+ if kind == "directory" and not accepted:
+ # The list writer creates its parent, so keep it outside the missing output directory.
+ target = tree.getroot().find("Target")
+ writer = target.find("WriteLinesToFile")
+ old_list = writer.get("File")
+ new_list = "$(IntermediateOutputPath)schemas.list"
+ writer.set("File", new_list)
+ task = target.find("Exec")
+ self.assertIn(old_list, task.get("Command"))
+ task.set("Command", task.get("Command").replace(old_list, new_list))
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=accepted)
+ if not accepted:
+ self.assertIn("clean:", result.stderr)
+
+ def test_redundant_disabled_schema_makedir_is_accepted(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ tree.getroot().find("Target/MakeDir").set("Condition", "'false' == 'true'")
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_schema_list_writer_can_create_output_directory(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ target = tree.getroot().find("Target")
+ target.remove(target.find("MakeDir"))
+ tree.write(path, encoding="utf-8")
+ self.grade(work, script)
+
+ def test_after_compilation_schema_cleanup_is_rejected(self):
+ for phase in ("CoreCompile", "Build"):
+ with self.subTest(after=phase):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ target = ET.SubElement(tree.getroot(), "Target", Name="DeleteGenerated", AfterTargets=phase)
+ ET.SubElement(target, "RemoveDir", Directories="$(IntermediateOutputPath)gen")
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("clean:", result.stderr)
+
+ def test_generated_schemas_must_be_compile_inputs_at_compilation(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ for phase, operation in (("BeforeTargets", "Remove"), ("AfterTargets", "Include")):
+ target = ET.SubElement(tree.getroot(), "Target", Name=operation + "Compile", **{phase: "CoreCompile"})
+ group = ET.SubElement(target, "ItemGroup")
+ ET.SubElement(group, "Compile", **{operation: "$(IntermediateOutputPath)gen/*.g.cs"})
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("Compile input at compilation", result.stderr)
+
+ def test_generated_directory_cleanup_is_rejected(self):
+ for inside in (False, True):
+ with self.subTest(inside_incremental_target=inside):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ generator = tree.getroot().find("Target")
+ cleanup = ET.Element("RemoveDir", Directories="$(IntermediateOutputPath)gen")
+ if inside:
+ generator.insert(0, cleanup)
+ else:
+ ET.SubElement(
+ tree.getroot(), "Target", Name="_DeleteGenerated",
+ BeforeTargets=generator.get("Name"),
+ ).append(cleanup)
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("single-stale:" if inside else "unchanged:", result.stderr)
+
+ def test_schema_regeneration_is_limited_to_stale_inputs(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ group = ET.Element("ItemGroup")
+ ET.SubElement(group, "SchemaFile", Remove="@(SchemaFile)")
+ ET.SubElement(group, "SchemaFile", Include="schemas/**/*.json")
+ tree.getroot().find("Target").insert(0, group)
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("single-stale: generate only the stale schemas", result.stderr)
+
+ def test_unchanged_schema_timestamps_are_preserved(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ after = ET.SubElement(
+ tree.getroot(), "Target", Name="_RewriteCurrentOutput",
+ AfterTargets=tree.getroot().find("Target").get("Name"),
+ )
+ ET.SubElement(
+ after, "Touch", Files="$(IntermediateOutputPath)gen/Customer.g.cs", ForceTouch="true",
+ )
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("unchanged: preserve every generated output timestamp", result.stderr)
+
+ def test_single_stale_generation_preserves_current_timestamps(self):
+ work, script = self.workspace("per-item-exec")
+ path = work / "per-file-exec" / "Schemas.targets"
+ tree = ET.parse(path)
+ ET.SubElement(
+ tree.getroot().find("Target"), "Touch",
+ Files="$(IntermediateOutputPath)gen/Customer.g.cs", ForceTouch="true",
+ )
+ tree.write(path, encoding="utf-8")
+ result = self.grade(work, script, accepted=False)
+ self.assertIn("single-stale: preserve current outputs and their timestamps", result.stderr)
+
+ def test_suite_contract_and_inline_graders(self):
+ guards = [s for s in SPEC["stimuli"] if s.get("tags", {}).get("expect_activation") == "false"]
+ self.assertEqual(len(guards), 7)
+ self.assertEqual(len(SPEC["stimuli"]) - len(guards), 23)
+ self.assertEqual(SPEC["defaults"]["timeout"], "6m")
+ for stimulus in SPEC["stimuli"]:
+ with self.subTest(stimulus=stimulus["name"]):
+ for grader in stimulus["graders"]:
+ if grader["type"] == "run-command":
+ compile(grader["config"]["args"][1], stimulus["name"], "exec")
+ elif grader["type"] == "output-matches":
+ steps = stimulus["golden_trajectory"]["inline"]["steps"]
+ response = next(s["message"] for s in reversed(steps) if s["source"] == "agent")
+ self.assertRegex(response, re.compile(grader["config"]["pattern"]))
+
+ def test_audit_goldens_and_complete_evidence_preservation(self):
+ audits = [s for s in SPEC["stimuli"]
+ if any(f["dest"] == "audit/evidence.txt"
+ for f in s.get("environment", {}).get("files", []))]
+ self.assertEqual(len(audits), 16)
+ for stimulus in audits:
+ with self.subTest(stimulus=stimulus["name"]):
+ work, script = self.workspace(stimulus["tags"]["capability"])
+ self.grade(work, script)
+ extra = work / "audit" / "extra.txt"
+ extra.write_text("unexpected file", encoding="utf-8")
+ self.grade(work, script, accepted=False)
+ extra.unlink()
+ evidence = work / "audit" / "evidence.txt"
+ evidence.write_bytes(evidence.read_bytes()[:20])
+ self.grade(work, script, accepted=False)
+ evidence.unlink()
+ self.grade(work, script, accepted=False)
+
+
+if __name__ == "__main__":
+ unittest.main()